flint-orm 0.7.2 → 0.8.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 KavenLabs
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Flint ORM
2
2
 
3
- A type-safe SQLite ORM for JavaScript. One schema, any driver.
3
+ Type-safe SQLite ORM for JavaScript
4
4
 
5
5
  ## Features
6
6
 
@@ -1,4 +1,4 @@
1
1
  export { text, integer, boolean, json, date, real } from '../schema/columns';
2
2
  export type { ColumnDef, IntegerColumnDef, DateColumnDef, DateColumnDefWithDefault, ForeignKeyAction } from '../schema/columns';
3
- export { table, index, snakeCase } from '../schema/table';
4
- export type { InferRow, InsertRow, TableDef, AnyTable, IndexDef, IndexBuilder } from '../schema/table';
3
+ export { table, index, primaryKey, snakeCase } from '../schema/table';
4
+ export type { InferRow, InsertRow, TableDef, AnyTable, IndexDef, IndexBuilder, PrimaryKeyBuilder, PrimaryKeyDef } from '../schema/table';
package/dist/index.d.ts CHANGED
@@ -3,8 +3,8 @@ export type { SQLExpression, Executable, SelectBuilder, InsertStage1, UpdateStag
3
3
  export type { Executor } from './executor';
4
4
  export { text, integer, boolean, json, date, real } from './schema/columns';
5
5
  export type { ColumnDef, IntegerColumnDef, DateColumnDef, DateColumnDefWithDefault } from './schema/columns';
6
- export { table, index, snakeCase } from './schema/table';
7
- export type { InferRow, InsertRow, TableDef, AnyTable, IndexDef, IndexBuilder } from './schema/table';
6
+ export { table, index, primaryKey, snakeCase } from './schema/table';
7
+ export type { InferRow, InsertRow, TableDef, AnyTable, IndexDef, IndexBuilder, PrimaryKeyBuilder, PrimaryKeyDef } from './schema/table';
8
8
  export { eq, and, or, gt, gte, lt, lte, neq, isIn, isNotIn, isNull, isNotNull, like, glob, between } from './query/conditions';
9
9
  export type { Condition } from './query/conditions';
10
10
  export { count, countColumn, sum, avg, min, max } from './query/aggregates';
@@ -18,6 +18,7 @@ export interface SerializedTable {
18
18
  name: string;
19
19
  columns: SerializedColumn[];
20
20
  indexes: SerializedIndex[];
21
+ primaryKeyColumns?: string[];
21
22
  }
22
23
  /** Serialized index definition. */
23
24
  export interface SerializedIndex {
@@ -5,7 +5,7 @@ export type TableDef<T> = T & {
5
5
  readonly name: string;
6
6
  };
7
7
  };
8
- export declare function table<T extends Record<string, ColumnDef<any, any>>>(name: string, columns: T, indexFn?: (t: TableDef<T>) => IndexBuilder[]): TableDef<T>;
8
+ export declare function table<T extends Record<string, ColumnDef<any, any>>>(name: string, columns: T, indexFn?: (t: TableDef<T>) => (IndexBuilder | PrimaryKeyBuilder)[]): TableDef<T>;
9
9
  /**
10
10
  * Derive the row shape from a table's column definitions.
11
11
  * DateColumnDef → Date | null (nullable unless defaultNow() was called)
@@ -37,7 +37,7 @@ export type InsertRow<T extends TableDef<any>> = {
37
37
  * firstName: text(),
38
38
  * });
39
39
  */
40
- export declare function snakeCaseTable<T extends Record<string, ColumnDef<any, any>>>(tableName: string, columns: T, indexFn?: (t: TableDef<T>) => IndexBuilder[]): TableDef<T>;
40
+ export declare function snakeCaseTable<T extends Record<string, ColumnDef<any, any>>>(tableName: string, columns: T, indexFn?: (t: TableDef<T>) => (IndexBuilder | PrimaryKeyBuilder)[]): TableDef<T>;
41
41
  /** Provides `snakeCase.table()` for auto snake_case column naming. */
42
42
  export declare const snakeCase: {
43
43
  table: typeof snakeCaseTable;
@@ -55,6 +55,30 @@ export interface IndexBuilder {
55
55
  /** Mark the index as unique. */
56
56
  unique(): IndexBuilder;
57
57
  }
58
+ /** A composite primary key definition — used internally by the migration system. */
59
+ export interface PrimaryKeyDef {
60
+ columns: string[];
61
+ }
62
+ /** Public builder returned by `primaryKey()` — chain `.on(columns)`. */
63
+ export interface PrimaryKeyBuilder {
64
+ /** Add one or more columns to the composite primary key. */
65
+ on(...columns: ColumnDef<any, any>[]): PrimaryKeyBuilder;
66
+ }
67
+ /**
68
+ * Create a composite primary key definition using a chainable API.
69
+ *
70
+ * @param name - Optional name (reserved for future use, currently unused in SQL)
71
+ * @returns A PrimaryKeyBuilder — chain `.on(columns)`
72
+ *
73
+ * @example
74
+ * const userRoles = table("user_roles", {
75
+ * userId: text("user_id"),
76
+ * roleId: text("role_id"),
77
+ * }, (t) => [
78
+ * primaryKey().on(t.userId, t.roleId),
79
+ * ]);
80
+ */
81
+ export declare function primaryKey(): PrimaryKeyBuilderInternal;
58
82
  /**
59
83
  * Create an index definition using a chainable API.
60
84
  *
package/dist/src/cli.js CHANGED
@@ -326,6 +326,11 @@ function diffTable(tableName, prev, curr) {
326
326
  if (unsafe) {
327
327
  return [rebuildTable(tableName, prev, curr)];
328
328
  }
329
+ const prevPK = prev.primaryKeyColumns ?? [];
330
+ const currPK = curr.primaryKeyColumns ?? [];
331
+ if (JSON.stringify(prevPK) !== JSON.stringify(currPK)) {
332
+ return [rebuildTable(tableName, prev, curr)];
333
+ }
329
334
  const ops = [...columnOps];
330
335
  const prevIndexes = new Map(prev.indexes.map((i2) => [i2.name, i2]));
331
336
  const currIndexes = new Map(curr.indexes.map((i2) => [i2.name, i2]));
@@ -572,7 +577,17 @@ function serializeTable(table) {
572
577
  });
573
578
  }
574
579
  }
575
- return { name: tableName, columns, indexes };
580
+ let primaryKeyColumns;
581
+ if (tableObj.__primaryKey) {
582
+ const pkDef = tableObj.__primaryKey;
583
+ primaryKeyColumns = pkDef.columns;
584
+ for (const col of columns) {
585
+ if (col.isPrimaryKey) {
586
+ throw new Error(`Column "${col.name}" has primaryKey() but table "${tableName}" also defines a composite primaryKey(). Use one or the other.`);
587
+ }
588
+ }
589
+ }
590
+ return { name: tableName, columns, indexes, primaryKeyColumns };
576
591
  }
577
592
  function serializeSchema(tables) {
578
593
  const tableMap = {};
@@ -595,9 +610,9 @@ __export(exports_sql, {
595
610
  function sqlType(col) {
596
611
  return col.sqlType.toUpperCase();
597
612
  }
598
- function columnToDDL(col) {
613
+ function columnToDDL(col, isCompositePK = false) {
599
614
  const parts = [col.name, sqlType(col)];
600
- if (col.isPrimaryKey)
615
+ if (col.isPrimaryKey && !isCompositePK)
601
616
  parts.push("PRIMARY KEY");
602
617
  if (col.isAutoIncrement === true)
603
618
  parts.push("AUTOINCREMENT");
@@ -644,7 +659,12 @@ function rebuildTableToSQL(op) {
644
659
  const tempName = `_flint_rebuild_${tableName}`;
645
660
  const stmts = [];
646
661
  stmts.push("PRAGMA defer_foreign_keys = 1");
647
- const cols = newTable.columns.map(columnToDDL).join(`,
662
+ const isComposite = newTable.primaryKeyColumns && newTable.primaryKeyColumns.length > 0;
663
+ const colDefs = newTable.columns.map((c3) => columnToDDL(c3, isComposite));
664
+ if (isComposite) {
665
+ colDefs.push(`PRIMARY KEY(${newTable.primaryKeyColumns.join(", ")})`);
666
+ }
667
+ const cols = colDefs.join(`,
648
668
  `);
649
669
  stmts.push(`CREATE TABLE ${tempName} (
650
670
  ${cols}
@@ -671,7 +691,12 @@ function rebuildTableToSQL(op) {
671
691
  function operationToSQL(op) {
672
692
  switch (op.type) {
673
693
  case "addTable": {
674
- const cols = op.table.columns.map(columnToDDL).join(`,
694
+ const isComposite = op.table.primaryKeyColumns && op.table.primaryKeyColumns.length > 0;
695
+ const colDefs = op.table.columns.map((c3) => columnToDDL(c3, isComposite));
696
+ if (isComposite) {
697
+ colDefs.push(`PRIMARY KEY(${op.table.primaryKeyColumns.join(", ")})`);
698
+ }
699
+ const cols = colDefs.join(`,
675
700
  `);
676
701
  const stmts = [`CREATE TABLE ${op.table.name} (
677
702
  ${cols}
@@ -1315,12 +1340,16 @@ function decodeSelectedRow(raw, tbl, keys) {
1315
1340
  }
1316
1341
  return out;
1317
1342
  }
1318
- function findPKKey(tbl) {
1343
+ function findPKKeys(tbl) {
1344
+ const keys = [];
1319
1345
  for (const [key, col] of columnEntries(tbl)) {
1320
1346
  if (col.__internal.isPrimaryKey)
1321
- return key;
1347
+ keys.push(key);
1348
+ }
1349
+ if (keys.length === 0) {
1350
+ throw new FlintValidationError("Table has no primary key column");
1322
1351
  }
1323
- throw new FlintValidationError("Table has no primary key column");
1352
+ return keys;
1324
1353
  }
1325
1354
  function resolveForeignKeyCondition(parent, parentName, child, childName) {
1326
1355
  for (const [, col] of columnEntries(child)) {
@@ -1758,8 +1787,8 @@ class JoinBuilderImpl {
1758
1787
  }
1759
1788
  #decodeJoinRows(rows) {
1760
1789
  const parentEntries = columnEntries(this.#parent);
1761
- const pkKey = findPKKey(this.#parent);
1762
- const pkColName = getCol(this.#parent, pkKey).name;
1790
+ const pkKeys = findPKKeys(this.#parent);
1791
+ const pkColNames = pkKeys.map((k) => getCol(this.#parent, k).name);
1763
1792
  const childEntryMaps = [];
1764
1793
  for (const j of this.#joins) {
1765
1794
  childEntryMaps.push({
@@ -1770,7 +1799,7 @@ class JoinBuilderImpl {
1770
1799
  }
1771
1800
  const grouped = new Map;
1772
1801
  for (const row of rows) {
1773
- const pk = row[pkColName];
1802
+ const pk = pkColNames.length === 1 ? row[pkColNames[0]] : pkColNames.map((name) => String(row[name])).join("||");
1774
1803
  if (!grouped.has(pk)) {
1775
1804
  const parentRow = {};
1776
1805
  for (const [key, col] of parentEntries) {
@@ -1888,8 +1917,8 @@ class SingleJoinBuilderImpl {
1888
1917
  }
1889
1918
  #decodeJoinRow(rows) {
1890
1919
  const parentEntries = columnEntries(this.#parent);
1891
- const pkKey = findPKKey(this.#parent);
1892
- const pkColName = getCol(this.#parent, pkKey).name;
1920
+ const pkKeys = findPKKeys(this.#parent);
1921
+ const pkColNames = pkKeys.map((k) => getCol(this.#parent, k).name);
1893
1922
  const childEntryMaps = [];
1894
1923
  for (const j of this.#joins) {
1895
1924
  childEntryMaps.push({
@@ -1900,7 +1929,7 @@ class SingleJoinBuilderImpl {
1900
1929
  }
1901
1930
  const grouped = new Map;
1902
1931
  for (const r2 of rows) {
1903
- const pk = r2[pkColName];
1932
+ const pk = pkColNames.length === 1 ? r2[pkColNames[0]] : pkColNames.map((name) => String(r2[name])).join("||");
1904
1933
  if (!grouped.has(pk)) {
1905
1934
  const parentRow = {};
1906
1935
  for (const [key, col] of parentEntries) {
@@ -976,12 +976,16 @@ function decodeSelectedRow(raw, tbl, keys) {
976
976
  }
977
977
  return out;
978
978
  }
979
- function findPKKey(tbl) {
979
+ function findPKKeys(tbl) {
980
+ const keys = [];
980
981
  for (const [key, col] of columnEntries(tbl)) {
981
982
  if (col.__internal.isPrimaryKey)
982
- return key;
983
+ keys.push(key);
983
984
  }
984
- throw new FlintValidationError("Table has no primary key column");
985
+ if (keys.length === 0) {
986
+ throw new FlintValidationError("Table has no primary key column");
987
+ }
988
+ return keys;
985
989
  }
986
990
  function resolveForeignKeyCondition(parent, parentName, child, childName) {
987
991
  for (const [, col] of columnEntries(child)) {
@@ -1419,8 +1423,8 @@ class JoinBuilderImpl {
1419
1423
  }
1420
1424
  #decodeJoinRows(rows) {
1421
1425
  const parentEntries = columnEntries(this.#parent);
1422
- const pkKey = findPKKey(this.#parent);
1423
- const pkColName = getCol(this.#parent, pkKey).name;
1426
+ const pkKeys = findPKKeys(this.#parent);
1427
+ const pkColNames = pkKeys.map((k) => getCol(this.#parent, k).name);
1424
1428
  const childEntryMaps = [];
1425
1429
  for (const j of this.#joins) {
1426
1430
  childEntryMaps.push({
@@ -1431,7 +1435,7 @@ class JoinBuilderImpl {
1431
1435
  }
1432
1436
  const grouped = new Map;
1433
1437
  for (const row of rows) {
1434
- const pk = row[pkColName];
1438
+ const pk = pkColNames.length === 1 ? row[pkColNames[0]] : pkColNames.map((name) => String(row[name])).join("||");
1435
1439
  if (!grouped.has(pk)) {
1436
1440
  const parentRow = {};
1437
1441
  for (const [key, col] of parentEntries) {
@@ -1549,8 +1553,8 @@ class SingleJoinBuilderImpl {
1549
1553
  }
1550
1554
  #decodeJoinRow(rows) {
1551
1555
  const parentEntries = columnEntries(this.#parent);
1552
- const pkKey = findPKKey(this.#parent);
1553
- const pkColName = getCol(this.#parent, pkKey).name;
1556
+ const pkKeys = findPKKeys(this.#parent);
1557
+ const pkColNames = pkKeys.map((k) => getCol(this.#parent, k).name);
1554
1558
  const childEntryMaps = [];
1555
1559
  for (const j of this.#joins) {
1556
1560
  childEntryMaps.push({
@@ -1561,7 +1565,7 @@ class SingleJoinBuilderImpl {
1561
1565
  }
1562
1566
  const grouped = new Map;
1563
1567
  for (const r of rows) {
1564
- const pk = r[pkColName];
1568
+ const pk = pkColNames.length === 1 ? r[pkColNames[0]] : pkColNames.map((name) => String(r[name])).join("||");
1565
1569
  if (!grouped.has(pk)) {
1566
1570
  const parentRow = {};
1567
1571
  for (const [key, col] of parentEntries) {
@@ -211,12 +211,16 @@ function decodeSelectedRow(raw, tbl, keys) {
211
211
  }
212
212
  return out;
213
213
  }
214
- function findPKKey(tbl) {
214
+ function findPKKeys(tbl) {
215
+ const keys = [];
215
216
  for (const [key, col] of columnEntries(tbl)) {
216
217
  if (col.__internal.isPrimaryKey)
217
- return key;
218
+ keys.push(key);
218
219
  }
219
- throw new FlintValidationError("Table has no primary key column");
220
+ if (keys.length === 0) {
221
+ throw new FlintValidationError("Table has no primary key column");
222
+ }
223
+ return keys;
220
224
  }
221
225
  function resolveForeignKeyCondition(parent, parentName, child, childName) {
222
226
  for (const [, col] of columnEntries(child)) {
@@ -654,8 +658,8 @@ class JoinBuilderImpl {
654
658
  }
655
659
  #decodeJoinRows(rows) {
656
660
  const parentEntries = columnEntries(this.#parent);
657
- const pkKey = findPKKey(this.#parent);
658
- const pkColName = getCol(this.#parent, pkKey).name;
661
+ const pkKeys = findPKKeys(this.#parent);
662
+ const pkColNames = pkKeys.map((k) => getCol(this.#parent, k).name);
659
663
  const childEntryMaps = [];
660
664
  for (const j of this.#joins) {
661
665
  childEntryMaps.push({
@@ -666,7 +670,7 @@ class JoinBuilderImpl {
666
670
  }
667
671
  const grouped = new Map;
668
672
  for (const row of rows) {
669
- const pk = row[pkColName];
673
+ const pk = pkColNames.length === 1 ? row[pkColNames[0]] : pkColNames.map((name) => String(row[name])).join("||");
670
674
  if (!grouped.has(pk)) {
671
675
  const parentRow = {};
672
676
  for (const [key, col] of parentEntries) {
@@ -784,8 +788,8 @@ class SingleJoinBuilderImpl {
784
788
  }
785
789
  #decodeJoinRow(rows) {
786
790
  const parentEntries = columnEntries(this.#parent);
787
- const pkKey = findPKKey(this.#parent);
788
- const pkColName = getCol(this.#parent, pkKey).name;
791
+ const pkKeys = findPKKeys(this.#parent);
792
+ const pkColNames = pkKeys.map((k) => getCol(this.#parent, k).name);
789
793
  const childEntryMaps = [];
790
794
  for (const j of this.#joins) {
791
795
  childEntryMaps.push({
@@ -796,7 +800,7 @@ class SingleJoinBuilderImpl {
796
800
  }
797
801
  const grouped = new Map;
798
802
  for (const r of rows) {
799
- const pk = r[pkColName];
803
+ const pk = pkColNames.length === 1 ? r[pkColNames[0]] : pkColNames.map((name) => String(r[name])).join("||");
800
804
  if (!grouped.has(pk)) {
801
805
  const parentRow = {};
802
806
  for (const [key, col] of parentEntries) {
@@ -211,12 +211,16 @@ function decodeSelectedRow(raw, tbl, keys) {
211
211
  }
212
212
  return out;
213
213
  }
214
- function findPKKey(tbl) {
214
+ function findPKKeys(tbl) {
215
+ const keys = [];
215
216
  for (const [key, col] of columnEntries(tbl)) {
216
217
  if (col.__internal.isPrimaryKey)
217
- return key;
218
+ keys.push(key);
218
219
  }
219
- throw new FlintValidationError("Table has no primary key column");
220
+ if (keys.length === 0) {
221
+ throw new FlintValidationError("Table has no primary key column");
222
+ }
223
+ return keys;
220
224
  }
221
225
  function resolveForeignKeyCondition(parent, parentName, child, childName) {
222
226
  for (const [, col] of columnEntries(child)) {
@@ -654,8 +658,8 @@ class JoinBuilderImpl {
654
658
  }
655
659
  #decodeJoinRows(rows) {
656
660
  const parentEntries = columnEntries(this.#parent);
657
- const pkKey = findPKKey(this.#parent);
658
- const pkColName = getCol(this.#parent, pkKey).name;
661
+ const pkKeys = findPKKeys(this.#parent);
662
+ const pkColNames = pkKeys.map((k) => getCol(this.#parent, k).name);
659
663
  const childEntryMaps = [];
660
664
  for (const j of this.#joins) {
661
665
  childEntryMaps.push({
@@ -666,7 +670,7 @@ class JoinBuilderImpl {
666
670
  }
667
671
  const grouped = new Map;
668
672
  for (const row of rows) {
669
- const pk = row[pkColName];
673
+ const pk = pkColNames.length === 1 ? row[pkColNames[0]] : pkColNames.map((name) => String(row[name])).join("||");
670
674
  if (!grouped.has(pk)) {
671
675
  const parentRow = {};
672
676
  for (const [key, col] of parentEntries) {
@@ -784,8 +788,8 @@ class SingleJoinBuilderImpl {
784
788
  }
785
789
  #decodeJoinRow(rows) {
786
790
  const parentEntries = columnEntries(this.#parent);
787
- const pkKey = findPKKey(this.#parent);
788
- const pkColName = getCol(this.#parent, pkKey).name;
791
+ const pkKeys = findPKKeys(this.#parent);
792
+ const pkColNames = pkKeys.map((k) => getCol(this.#parent, k).name);
789
793
  const childEntryMaps = [];
790
794
  for (const j of this.#joins) {
791
795
  childEntryMaps.push({
@@ -796,7 +800,7 @@ class SingleJoinBuilderImpl {
796
800
  }
797
801
  const grouped = new Map;
798
802
  for (const r of rows) {
799
- const pk = r[pkColName];
803
+ const pk = pkColNames.length === 1 ? r[pkColNames[0]] : pkColNames.map((name) => String(r[name])).join("||");
800
804
  if (!grouped.has(pk)) {
801
805
  const parentRow = {};
802
806
  for (const [key, col] of parentEntries) {
@@ -4997,12 +4997,16 @@ function decodeSelectedRow(raw, tbl, keys) {
4997
4997
  }
4998
4998
  return out;
4999
4999
  }
5000
- function findPKKey(tbl) {
5000
+ function findPKKeys(tbl) {
5001
+ const keys = [];
5001
5002
  for (const [key, col] of columnEntries(tbl)) {
5002
5003
  if (col.__internal.isPrimaryKey)
5003
- return key;
5004
+ keys.push(key);
5004
5005
  }
5005
- throw new FlintValidationError("Table has no primary key column");
5006
+ if (keys.length === 0) {
5007
+ throw new FlintValidationError("Table has no primary key column");
5008
+ }
5009
+ return keys;
5006
5010
  }
5007
5011
  function resolveForeignKeyCondition(parent, parentName, child, childName) {
5008
5012
  for (const [, col] of columnEntries(child)) {
@@ -5440,8 +5444,8 @@ class JoinBuilderImpl {
5440
5444
  }
5441
5445
  #decodeJoinRows(rows) {
5442
5446
  const parentEntries = columnEntries(this.#parent);
5443
- const pkKey = findPKKey(this.#parent);
5444
- const pkColName = getCol(this.#parent, pkKey).name;
5447
+ const pkKeys = findPKKeys(this.#parent);
5448
+ const pkColNames = pkKeys.map((k) => getCol(this.#parent, k).name);
5445
5449
  const childEntryMaps = [];
5446
5450
  for (const j of this.#joins) {
5447
5451
  childEntryMaps.push({
@@ -5452,7 +5456,7 @@ class JoinBuilderImpl {
5452
5456
  }
5453
5457
  const grouped = new Map;
5454
5458
  for (const row of rows) {
5455
- const pk = row[pkColName];
5459
+ const pk = pkColNames.length === 1 ? row[pkColNames[0]] : pkColNames.map((name) => String(row[name])).join("||");
5456
5460
  if (!grouped.has(pk)) {
5457
5461
  const parentRow = {};
5458
5462
  for (const [key, col] of parentEntries) {
@@ -5570,8 +5574,8 @@ class SingleJoinBuilderImpl {
5570
5574
  }
5571
5575
  #decodeJoinRow(rows) {
5572
5576
  const parentEntries = columnEntries(this.#parent);
5573
- const pkKey = findPKKey(this.#parent);
5574
- const pkColName = getCol(this.#parent, pkKey).name;
5577
+ const pkKeys = findPKKeys(this.#parent);
5578
+ const pkColNames = pkKeys.map((k) => getCol(this.#parent, k).name);
5575
5579
  const childEntryMaps = [];
5576
5580
  for (const j of this.#joins) {
5577
5581
  childEntryMaps.push({
@@ -5582,7 +5586,7 @@ class SingleJoinBuilderImpl {
5582
5586
  }
5583
5587
  const grouped = new Map;
5584
5588
  for (const r of rows) {
5585
- const pk = r[pkColName];
5589
+ const pk = pkColNames.length === 1 ? r[pkColNames[0]] : pkColNames.map((name) => String(r[name])).join("||");
5586
5590
  if (!grouped.has(pk)) {
5587
5591
  const parentRow = {};
5588
5592
  for (const [key, col] of parentEntries) {
@@ -6131,12 +6131,16 @@ function decodeSelectedRow(raw, tbl, keys) {
6131
6131
  }
6132
6132
  return out;
6133
6133
  }
6134
- function findPKKey(tbl) {
6134
+ function findPKKeys(tbl) {
6135
+ const keys = [];
6135
6136
  for (const [key, col] of columnEntries(tbl)) {
6136
6137
  if (col.__internal.isPrimaryKey)
6137
- return key;
6138
+ keys.push(key);
6138
6139
  }
6139
- throw new FlintValidationError("Table has no primary key column");
6140
+ if (keys.length === 0) {
6141
+ throw new FlintValidationError("Table has no primary key column");
6142
+ }
6143
+ return keys;
6140
6144
  }
6141
6145
  function resolveForeignKeyCondition(parent, parentName, child, childName) {
6142
6146
  for (const [, col] of columnEntries(child)) {
@@ -6574,8 +6578,8 @@ class JoinBuilderImpl {
6574
6578
  }
6575
6579
  #decodeJoinRows(rows) {
6576
6580
  const parentEntries = columnEntries(this.#parent);
6577
- const pkKey = findPKKey(this.#parent);
6578
- const pkColName = getCol(this.#parent, pkKey).name;
6581
+ const pkKeys = findPKKeys(this.#parent);
6582
+ const pkColNames = pkKeys.map((k) => getCol(this.#parent, k).name);
6579
6583
  const childEntryMaps = [];
6580
6584
  for (const j of this.#joins) {
6581
6585
  childEntryMaps.push({
@@ -6586,7 +6590,7 @@ class JoinBuilderImpl {
6586
6590
  }
6587
6591
  const grouped = new Map;
6588
6592
  for (const row of rows) {
6589
- const pk = row[pkColName];
6593
+ const pk = pkColNames.length === 1 ? row[pkColNames[0]] : pkColNames.map((name) => String(row[name])).join("||");
6590
6594
  if (!grouped.has(pk)) {
6591
6595
  const parentRow = {};
6592
6596
  for (const [key, col] of parentEntries) {
@@ -6704,8 +6708,8 @@ class SingleJoinBuilderImpl {
6704
6708
  }
6705
6709
  #decodeJoinRow(rows) {
6706
6710
  const parentEntries = columnEntries(this.#parent);
6707
- const pkKey = findPKKey(this.#parent);
6708
- const pkColName = getCol(this.#parent, pkKey).name;
6711
+ const pkKeys = findPKKeys(this.#parent);
6712
+ const pkColNames = pkKeys.map((k) => getCol(this.#parent, k).name);
6709
6713
  const childEntryMaps = [];
6710
6714
  for (const j of this.#joins) {
6711
6715
  childEntryMaps.push({
@@ -6716,7 +6720,7 @@ class SingleJoinBuilderImpl {
6716
6720
  }
6717
6721
  const grouped = new Map;
6718
6722
  for (const r of rows) {
6719
- const pk = r[pkColName];
6723
+ const pk = pkColNames.length === 1 ? r[pkColNames[0]] : pkColNames.map((name) => String(r[name])).join("||");
6720
6724
  if (!grouped.has(pk)) {
6721
6725
  const parentRow = {};
6722
6726
  for (const [key, col] of parentEntries) {
@@ -291,7 +291,28 @@ function table(name, columns, indexFn) {
291
291
  const raw = indexFn(result);
292
292
  if (raw.length > 0) {
293
293
  const tableObj = result;
294
- tableObj.__indexes = raw.map((item) => item.build());
294
+ const indexes = [];
295
+ let primaryKeyDef;
296
+ for (const item of raw) {
297
+ if (item && typeof item === "object" && "_type" in item) {
298
+ if (item._type === "primaryKey") {
299
+ primaryKeyDef = item.build();
300
+ } else {
301
+ indexes.push(item.build());
302
+ }
303
+ }
304
+ }
305
+ if (primaryKeyDef) {
306
+ for (const [key, col] of Object.entries(columns)) {
307
+ if (col.__internal.isPrimaryKey) {
308
+ throw new Error(`Column "${key}" has primaryKey() but table "${name}" also defines a composite primaryKey(). Use one or the other.`);
309
+ }
310
+ }
311
+ tableObj.__primaryKey = primaryKeyDef;
312
+ }
313
+ if (indexes.length > 0) {
314
+ tableObj.__indexes = indexes;
315
+ }
295
316
  }
296
317
  }
297
318
  return result;
@@ -365,10 +386,28 @@ function snakeCaseTable(tableName, columns, indexFn) {
365
386
  var snakeCase = {
366
387
  table: snakeCaseTable
367
388
  };
389
+ function primaryKey() {
390
+ let columns = [];
391
+ const builder = {
392
+ _type: "primaryKey",
393
+ on(...cols) {
394
+ columns = cols;
395
+ return builder;
396
+ },
397
+ build() {
398
+ if (columns.length === 0) {
399
+ throw new Error("primaryKey() has no columns \u2014 call .on() before returning");
400
+ }
401
+ return { columns: columns.map((c) => c.name) };
402
+ }
403
+ };
404
+ return builder;
405
+ }
368
406
  function index(name) {
369
407
  let columns = [];
370
408
  let isUnique = false;
371
409
  const builder = {
410
+ _type: "index",
372
411
  on(...cols) {
373
412
  columns = cols;
374
413
  return builder;
@@ -395,6 +434,7 @@ export {
395
434
  table,
396
435
  snakeCase,
397
436
  real,
437
+ primaryKey,
398
438
  json,
399
439
  integer,
400
440
  index,
@@ -2432,12 +2432,16 @@ function decodeSelectedRow(raw, tbl, keys) {
2432
2432
  }
2433
2433
  return out;
2434
2434
  }
2435
- function findPKKey(tbl) {
2435
+ function findPKKeys(tbl) {
2436
+ const keys = [];
2436
2437
  for (const [key, col] of columnEntries(tbl)) {
2437
2438
  if (col.__internal.isPrimaryKey)
2438
- return key;
2439
+ keys.push(key);
2439
2440
  }
2440
- throw new FlintValidationError("Table has no primary key column");
2441
+ if (keys.length === 0) {
2442
+ throw new FlintValidationError("Table has no primary key column");
2443
+ }
2444
+ return keys;
2441
2445
  }
2442
2446
  function resolveForeignKeyCondition(parent, parentName, child, childName) {
2443
2447
  for (const [, col] of columnEntries(child)) {
@@ -2875,8 +2879,8 @@ class JoinBuilderImpl {
2875
2879
  }
2876
2880
  #decodeJoinRows(rows) {
2877
2881
  const parentEntries = columnEntries(this.#parent);
2878
- const pkKey = findPKKey(this.#parent);
2879
- const pkColName = getCol(this.#parent, pkKey).name;
2882
+ const pkKeys = findPKKeys(this.#parent);
2883
+ const pkColNames = pkKeys.map((k) => getCol(this.#parent, k).name);
2880
2884
  const childEntryMaps = [];
2881
2885
  for (const j of this.#joins) {
2882
2886
  childEntryMaps.push({
@@ -2887,7 +2891,7 @@ class JoinBuilderImpl {
2887
2891
  }
2888
2892
  const grouped = new Map;
2889
2893
  for (const row of rows) {
2890
- const pk = row[pkColName];
2894
+ const pk = pkColNames.length === 1 ? row[pkColNames[0]] : pkColNames.map((name) => String(row[name])).join("||");
2891
2895
  if (!grouped.has(pk)) {
2892
2896
  const parentRow = {};
2893
2897
  for (const [key, col] of parentEntries) {
@@ -3005,8 +3009,8 @@ class SingleJoinBuilderImpl {
3005
3009
  }
3006
3010
  #decodeJoinRow(rows) {
3007
3011
  const parentEntries = columnEntries(this.#parent);
3008
- const pkKey = findPKKey(this.#parent);
3009
- const pkColName = getCol(this.#parent, pkKey).name;
3012
+ const pkKeys = findPKKeys(this.#parent);
3013
+ const pkColNames = pkKeys.map((k) => getCol(this.#parent, k).name);
3010
3014
  const childEntryMaps = [];
3011
3015
  for (const j of this.#joins) {
3012
3016
  childEntryMaps.push({
@@ -3017,7 +3021,7 @@ class SingleJoinBuilderImpl {
3017
3021
  }
3018
3022
  const grouped = new Map;
3019
3023
  for (const r of rows) {
3020
- const pk = r[pkColName];
3024
+ const pk = pkColNames.length === 1 ? r[pkColNames[0]] : pkColNames.map((name) => String(r[name])).join("||");
3021
3025
  if (!grouped.has(pk)) {
3022
3026
  const parentRow = {};
3023
3027
  for (const [key, col] of parentEntries) {
@@ -1313,12 +1313,16 @@ function decodeSelectedRow(raw, tbl, keys) {
1313
1313
  }
1314
1314
  return out;
1315
1315
  }
1316
- function findPKKey(tbl) {
1316
+ function findPKKeys(tbl) {
1317
+ const keys = [];
1317
1318
  for (const [key, col] of columnEntries(tbl)) {
1318
1319
  if (col.__internal.isPrimaryKey)
1319
- return key;
1320
+ keys.push(key);
1320
1321
  }
1321
- throw new FlintValidationError("Table has no primary key column");
1322
+ if (keys.length === 0) {
1323
+ throw new FlintValidationError("Table has no primary key column");
1324
+ }
1325
+ return keys;
1322
1326
  }
1323
1327
  function resolveForeignKeyCondition(parent, parentName, child, childName) {
1324
1328
  for (const [, col] of columnEntries(child)) {
@@ -1756,8 +1760,8 @@ class JoinBuilderImpl {
1756
1760
  }
1757
1761
  #decodeJoinRows(rows) {
1758
1762
  const parentEntries = columnEntries(this.#parent);
1759
- const pkKey = findPKKey(this.#parent);
1760
- const pkColName = getCol(this.#parent, pkKey).name;
1763
+ const pkKeys = findPKKeys(this.#parent);
1764
+ const pkColNames = pkKeys.map((k) => getCol(this.#parent, k).name);
1761
1765
  const childEntryMaps = [];
1762
1766
  for (const j of this.#joins) {
1763
1767
  childEntryMaps.push({
@@ -1768,7 +1772,7 @@ class JoinBuilderImpl {
1768
1772
  }
1769
1773
  const grouped = new Map;
1770
1774
  for (const row of rows) {
1771
- const pk = row[pkColName];
1775
+ const pk = pkColNames.length === 1 ? row[pkColNames[0]] : pkColNames.map((name) => String(row[name])).join("||");
1772
1776
  if (!grouped.has(pk)) {
1773
1777
  const parentRow = {};
1774
1778
  for (const [key, col] of parentEntries) {
@@ -1886,8 +1890,8 @@ class SingleJoinBuilderImpl {
1886
1890
  }
1887
1891
  #decodeJoinRow(rows) {
1888
1892
  const parentEntries = columnEntries(this.#parent);
1889
- const pkKey = findPKKey(this.#parent);
1890
- const pkColName = getCol(this.#parent, pkKey).name;
1893
+ const pkKeys = findPKKeys(this.#parent);
1894
+ const pkColNames = pkKeys.map((k) => getCol(this.#parent, k).name);
1891
1895
  const childEntryMaps = [];
1892
1896
  for (const j of this.#joins) {
1893
1897
  childEntryMaps.push({
@@ -1898,7 +1902,7 @@ class SingleJoinBuilderImpl {
1898
1902
  }
1899
1903
  const grouped = new Map;
1900
1904
  for (const r of rows) {
1901
- const pk = r[pkColName];
1905
+ const pk = pkColNames.length === 1 ? r[pkColNames[0]] : pkColNames.map((name) => String(r[name])).join("||");
1902
1906
  if (!grouped.has(pk)) {
1903
1907
  const parentRow = {};
1904
1908
  for (const [key, col] of parentEntries) {
package/dist/src/index.js CHANGED
@@ -211,12 +211,16 @@ function decodeSelectedRow(raw, tbl, keys) {
211
211
  }
212
212
  return out;
213
213
  }
214
- function findPKKey(tbl) {
214
+ function findPKKeys(tbl) {
215
+ const keys = [];
215
216
  for (const [key, col] of columnEntries(tbl)) {
216
217
  if (col.__internal.isPrimaryKey)
217
- return key;
218
+ keys.push(key);
218
219
  }
219
- throw new FlintValidationError("Table has no primary key column");
220
+ if (keys.length === 0) {
221
+ throw new FlintValidationError("Table has no primary key column");
222
+ }
223
+ return keys;
220
224
  }
221
225
  function resolveForeignKeyCondition(parent, parentName, child, childName) {
222
226
  for (const [, col] of columnEntries(child)) {
@@ -654,8 +658,8 @@ class JoinBuilderImpl {
654
658
  }
655
659
  #decodeJoinRows(rows) {
656
660
  const parentEntries = columnEntries(this.#parent);
657
- const pkKey = findPKKey(this.#parent);
658
- const pkColName = getCol(this.#parent, pkKey).name;
661
+ const pkKeys = findPKKeys(this.#parent);
662
+ const pkColNames = pkKeys.map((k) => getCol(this.#parent, k).name);
659
663
  const childEntryMaps = [];
660
664
  for (const j of this.#joins) {
661
665
  childEntryMaps.push({
@@ -666,7 +670,7 @@ class JoinBuilderImpl {
666
670
  }
667
671
  const grouped = new Map;
668
672
  for (const row of rows) {
669
- const pk = row[pkColName];
673
+ const pk = pkColNames.length === 1 ? row[pkColNames[0]] : pkColNames.map((name) => String(row[name])).join("||");
670
674
  if (!grouped.has(pk)) {
671
675
  const parentRow = {};
672
676
  for (const [key, col] of parentEntries) {
@@ -784,8 +788,8 @@ class SingleJoinBuilderImpl {
784
788
  }
785
789
  #decodeJoinRow(rows) {
786
790
  const parentEntries = columnEntries(this.#parent);
787
- const pkKey = findPKKey(this.#parent);
788
- const pkColName = getCol(this.#parent, pkKey).name;
791
+ const pkKeys = findPKKeys(this.#parent);
792
+ const pkColNames = pkKeys.map((k) => getCol(this.#parent, k).name);
789
793
  const childEntryMaps = [];
790
794
  for (const j of this.#joins) {
791
795
  childEntryMaps.push({
@@ -796,7 +800,7 @@ class SingleJoinBuilderImpl {
796
800
  }
797
801
  const grouped = new Map;
798
802
  for (const r of rows) {
799
- const pk = r[pkColName];
803
+ const pk = pkColNames.length === 1 ? r[pkColNames[0]] : pkColNames.map((name) => String(r[name])).join("||");
800
804
  if (!grouped.has(pk)) {
801
805
  const parentRow = {};
802
806
  for (const [key, col] of parentEntries) {
@@ -1456,7 +1460,28 @@ function table(name, columns, indexFn) {
1456
1460
  const raw = indexFn(result);
1457
1461
  if (raw.length > 0) {
1458
1462
  const tableObj = result;
1459
- tableObj.__indexes = raw.map((item) => item.build());
1463
+ const indexes = [];
1464
+ let primaryKeyDef;
1465
+ for (const item of raw) {
1466
+ if (item && typeof item === "object" && "_type" in item) {
1467
+ if (item._type === "primaryKey") {
1468
+ primaryKeyDef = item.build();
1469
+ } else {
1470
+ indexes.push(item.build());
1471
+ }
1472
+ }
1473
+ }
1474
+ if (primaryKeyDef) {
1475
+ for (const [key, col] of Object.entries(columns)) {
1476
+ if (col.__internal.isPrimaryKey) {
1477
+ throw new Error(`Column "${key}" has primaryKey() but table "${name}" also defines a composite primaryKey(). Use one or the other.`);
1478
+ }
1479
+ }
1480
+ tableObj.__primaryKey = primaryKeyDef;
1481
+ }
1482
+ if (indexes.length > 0) {
1483
+ tableObj.__indexes = indexes;
1484
+ }
1460
1485
  }
1461
1486
  }
1462
1487
  return result;
@@ -1530,10 +1555,28 @@ function snakeCaseTable(tableName, columns, indexFn) {
1530
1555
  var snakeCase = {
1531
1556
  table: snakeCaseTable
1532
1557
  };
1558
+ function primaryKey() {
1559
+ let columns = [];
1560
+ const builder = {
1561
+ _type: "primaryKey",
1562
+ on(...cols) {
1563
+ columns = cols;
1564
+ return builder;
1565
+ },
1566
+ build() {
1567
+ if (columns.length === 0) {
1568
+ throw new Error("primaryKey() has no columns \u2014 call .on() before returning");
1569
+ }
1570
+ return { columns: columns.map((c) => c.name) };
1571
+ }
1572
+ };
1573
+ return builder;
1574
+ }
1533
1575
  function index(name) {
1534
1576
  let columns = [];
1535
1577
  let isUnique = false;
1536
1578
  const builder = {
1579
+ _type: "index",
1537
1580
  on(...cols) {
1538
1581
  columns = cols;
1539
1582
  return builder;
@@ -1623,7 +1666,7 @@ function introspectColumns(client, tableName) {
1623
1666
  const col = {
1624
1667
  name: row.name,
1625
1668
  sqlType: normalizeType(row.type),
1626
- isPrimaryKey: row.pk === 1,
1669
+ isPrimaryKey: row.pk > 0,
1627
1670
  isNotNull: row.notnull === 1,
1628
1671
  isUnique: uniqueColumns.has(row.name),
1629
1672
  hasDefault,
@@ -1639,7 +1682,8 @@ function introspectColumns(client, tableName) {
1639
1682
  }
1640
1683
  return col;
1641
1684
  });
1642
- return { columns, indexRows };
1685
+ const pkColumns = rows.filter((row) => row.pk > 0).sort((a, b) => a.pk - b.pk).map((row) => row.name);
1686
+ return { columns, indexRows, primaryKeyColumns: pkColumns.length > 1 ? pkColumns : undefined };
1643
1687
  }
1644
1688
  function introspectIndexes(client, indexRows) {
1645
1689
  const indexes = [];
@@ -1660,12 +1704,13 @@ function introspect(client) {
1660
1704
  const tableRows = client.query(`SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' AND name != '__flint_migrations'`).all();
1661
1705
  const tables = {};
1662
1706
  for (const { name } of tableRows) {
1663
- const { columns, indexRows } = introspectColumns(client, name);
1707
+ const { columns, indexRows, primaryKeyColumns } = introspectColumns(client, name);
1664
1708
  const indexes = introspectIndexes(client, indexRows);
1665
1709
  tables[name] = {
1666
1710
  name,
1667
1711
  columns,
1668
- indexes
1712
+ indexes,
1713
+ primaryKeyColumns
1669
1714
  };
1670
1715
  }
1671
1716
  return {
@@ -1680,6 +1725,7 @@ export {
1680
1725
  sql,
1681
1726
  snakeCase,
1682
1727
  real,
1728
+ primaryKey,
1683
1729
  or,
1684
1730
  neq,
1685
1731
  min,
@@ -97,7 +97,17 @@ function serializeTable(table) {
97
97
  });
98
98
  }
99
99
  }
100
- return { name: tableName, columns, indexes };
100
+ let primaryKeyColumns;
101
+ if (tableObj.__primaryKey) {
102
+ const pkDef = tableObj.__primaryKey;
103
+ primaryKeyColumns = pkDef.columns;
104
+ for (const col of columns) {
105
+ if (col.isPrimaryKey) {
106
+ throw new Error(`Column "${col.name}" has primaryKey() but table "${tableName}" also defines a composite primaryKey(). Use one or the other.`);
107
+ }
108
+ }
109
+ }
110
+ return { name: tableName, columns, indexes, primaryKeyColumns };
101
111
  }
102
112
  function serializeSchema(tables) {
103
113
  const tableMap = {};
@@ -262,6 +272,11 @@ function diffTable(tableName, prev, curr) {
262
272
  if (unsafe) {
263
273
  return [rebuildTable(tableName, prev, curr)];
264
274
  }
275
+ const prevPK = prev.primaryKeyColumns ?? [];
276
+ const currPK = curr.primaryKeyColumns ?? [];
277
+ if (JSON.stringify(prevPK) !== JSON.stringify(currPK)) {
278
+ return [rebuildTable(tableName, prev, curr)];
279
+ }
265
280
  const ops = [...columnOps];
266
281
  const prevIndexes = new Map(prev.indexes.map((i) => [i.name, i]));
267
282
  const currIndexes = new Map(curr.indexes.map((i) => [i.name, i]));
@@ -466,9 +481,9 @@ __export(exports_sql, {
466
481
  function sqlType(col) {
467
482
  return col.sqlType.toUpperCase();
468
483
  }
469
- function columnToDDL(col) {
484
+ function columnToDDL(col, isCompositePK = false) {
470
485
  const parts = [col.name, sqlType(col)];
471
- if (col.isPrimaryKey)
486
+ if (col.isPrimaryKey && !isCompositePK)
472
487
  parts.push("PRIMARY KEY");
473
488
  if (col.isAutoIncrement === true)
474
489
  parts.push("AUTOINCREMENT");
@@ -515,7 +530,12 @@ function rebuildTableToSQL(op) {
515
530
  const tempName = `_flint_rebuild_${tableName}`;
516
531
  const stmts = [];
517
532
  stmts.push("PRAGMA defer_foreign_keys = 1");
518
- const cols = newTable.columns.map(columnToDDL).join(`,
533
+ const isComposite = newTable.primaryKeyColumns && newTable.primaryKeyColumns.length > 0;
534
+ const colDefs = newTable.columns.map((c) => columnToDDL(c, isComposite));
535
+ if (isComposite) {
536
+ colDefs.push(`PRIMARY KEY(${newTable.primaryKeyColumns.join(", ")})`);
537
+ }
538
+ const cols = colDefs.join(`,
519
539
  `);
520
540
  stmts.push(`CREATE TABLE ${tempName} (
521
541
  ${cols}
@@ -542,7 +562,12 @@ function rebuildTableToSQL(op) {
542
562
  function operationToSQL(op) {
543
563
  switch (op.type) {
544
564
  case "addTable": {
545
- const cols = op.table.columns.map(columnToDDL).join(`,
565
+ const isComposite = op.table.primaryKeyColumns && op.table.primaryKeyColumns.length > 0;
566
+ const colDefs = op.table.columns.map((c) => columnToDDL(c, isComposite));
567
+ if (isComposite) {
568
+ colDefs.push(`PRIMARY KEY(${op.table.primaryKeyColumns.join(", ")})`);
569
+ }
570
+ const cols = colDefs.join(`,
546
571
  `);
547
572
  const stmts = [`CREATE TABLE ${op.table.name} (
548
573
  ${cols}
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "flint-orm",
3
- "version": "0.7.2",
3
+ "version": "0.8.0",
4
+ "license": "MIT",
4
5
  "keywords": [
5
6
  "database",
6
7
  "libsql",