@bungres/kit 0.7.0 → 1.1.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/dist/cli.js CHANGED
@@ -56,12 +56,12 @@ var require_src = __commonJS((exports, module) => {
56
56
  ret += `${CSI2}${y}B`;
57
57
  return ret;
58
58
  },
59
- up: (count2 = 1) => `${CSI2}${count2}A`,
60
- down: (count2 = 1) => `${CSI2}${count2}B`,
61
- forward: (count2 = 1) => `${CSI2}${count2}C`,
62
- backward: (count2 = 1) => `${CSI2}${count2}D`,
63
- nextLine: (count2 = 1) => `${CSI2}E`.repeat(count2),
64
- prevLine: (count2 = 1) => `${CSI2}F`.repeat(count2),
59
+ up: (count = 1) => `${CSI2}${count}A`,
60
+ down: (count = 1) => `${CSI2}${count}B`,
61
+ forward: (count = 1) => `${CSI2}${count}C`,
62
+ backward: (count = 1) => `${CSI2}${count}D`,
63
+ nextLine: (count = 1) => `${CSI2}E`.repeat(count),
64
+ prevLine: (count = 1) => `${CSI2}F`.repeat(count),
65
65
  left: `${CSI2}G`,
66
66
  hide: `${CSI2}?25l`,
67
67
  show: `${CSI2}?25h`,
@@ -69,21 +69,21 @@ var require_src = __commonJS((exports, module) => {
69
69
  restore: `${ESC2}8`
70
70
  };
71
71
  var scroll = {
72
- up: (count2 = 1) => `${CSI2}S`.repeat(count2),
73
- down: (count2 = 1) => `${CSI2}T`.repeat(count2)
72
+ up: (count = 1) => `${CSI2}S`.repeat(count),
73
+ down: (count = 1) => `${CSI2}T`.repeat(count)
74
74
  };
75
75
  var erase = {
76
76
  screen: `${CSI2}2J`,
77
- up: (count2 = 1) => `${CSI2}1J`.repeat(count2),
78
- down: (count2 = 1) => `${CSI2}J`.repeat(count2),
77
+ up: (count = 1) => `${CSI2}1J`.repeat(count),
78
+ down: (count = 1) => `${CSI2}J`.repeat(count),
79
79
  line: `${CSI2}2K`,
80
80
  lineEnd: `${CSI2}K`,
81
81
  lineStart: `${CSI2}1K`,
82
- lines(count2) {
82
+ lines(count) {
83
83
  let clear = "";
84
- for (let i = 0;i < count2; i++)
85
- clear += this.line + (i < count2 - 1 ? cursor.up() : "");
86
- if (count2)
84
+ for (let i = 0;i < count; i++)
85
+ clear += this.line + (i < count - 1 ? cursor.up() : "");
86
+ if (count)
87
87
  clear += cursor.left;
88
88
  return clear;
89
89
  }
@@ -98,16 +98,16 @@ var require_picocolors = __commonJS((exports, module) => {
98
98
  var env = p2.env || {};
99
99
  var isColorSupported = !(!!env.NO_COLOR || argv.includes("--no-color")) && (!!env.FORCE_COLOR || argv.includes("--color") || p2.platform === "win32" || (p2.stdout || {}).isTTY && env.TERM !== "dumb" || !!env.CI);
100
100
  var formatter = (open, close, replace = open) => (input2) => {
101
- let string = "" + input2, index2 = string.indexOf(close, open.length);
102
- return ~index2 ? open + replaceClose(string, close, replace, index2) + close : open + string + close;
101
+ let string = "" + input2, index = string.indexOf(close, open.length);
102
+ return ~index ? open + replaceClose(string, close, replace, index) + close : open + string + close;
103
103
  };
104
- var replaceClose = (string, close, replace, index2) => {
104
+ var replaceClose = (string, close, replace, index) => {
105
105
  let result2 = "", cursor3 = 0;
106
106
  do {
107
- result2 += string.substring(cursor3, index2) + replace;
108
- cursor3 = index2 + close.length;
109
- index2 = string.indexOf(close, cursor3);
110
- } while (~index2);
107
+ result2 += string.substring(cursor3, index) + replace;
108
+ cursor3 = index + close.length;
109
+ index = string.indexOf(close, cursor3);
110
+ } while (~index);
111
111
  return result2 + string.substring(cursor3);
112
112
  };
113
113
  var createColors = (enabled = isColorSupported) => {
@@ -164,7 +164,7 @@ var require_picocolors = __commonJS((exports, module) => {
164
164
  // src/schema-loader.ts
165
165
  import { resolve, join } from "path";
166
166
 
167
- // ../bungres-orm/src/schema/table.ts
167
+ // ../bungres-orm/dist/index.js
168
168
  var TableConfigSymbol = Symbol.for("BungresTableConfig");
169
169
  function getTableConfig(table) {
170
170
  return table[TableConfigSymbol];
@@ -234,7 +234,6 @@ var table = createTableFactory("snake");
234
234
  var snakeCase = { table: createTableFactory("snake") };
235
235
  var camelCase = { table: createTableFactory("camel") };
236
236
  var noCasing = { table: createTableFactory("none") };
237
- // ../bungres-orm/src/schema/columns.ts
238
237
  function buildColumn(dataType, nameOrOpts, opts) {
239
238
  let name = "";
240
239
  let options = opts;
@@ -263,6 +262,13 @@ function buildColumn(dataType, nameOrOpts, opts) {
263
262
  return Object.assign(config2, {
264
263
  as(alias) {
265
264
  return Object.assign({}, this, { alias });
265
+ },
266
+ array() {
267
+ return Object.assign({}, this, { dataType: `${this.dataType}[]` });
268
+ },
269
+ generatedAlwaysAs(expr) {
270
+ const sqlStr = typeof expr === "string" ? expr : expr.sql;
271
+ return Object.assign({}, this, { generatedAs: sqlStr });
266
272
  }
267
273
  });
268
274
  }
@@ -292,7 +298,6 @@ var textArray = col("text[]");
292
298
  var integerArray = col("integer[]");
293
299
  var varcharArray = col("varchar[]");
294
300
  var uuidArray = col("uuid[]");
295
- // ../bungres-orm/src/core/sql.ts
296
301
  function sql(strings, ...values) {
297
302
  let query = "";
298
303
  const params = [];
@@ -335,7 +340,6 @@ function colName(c) {
335
340
  return c.sql;
336
341
  return c.tableName ? `${c.tableName}."${c.name}"` : `"${c.name}"`;
337
342
  }
338
- // ../bungres-orm/src/core/conditions.ts
339
343
  function isColumnConfig(val) {
340
344
  return val !== null && typeof val === "object" && "name" in val && "dataType" in val;
341
345
  }
@@ -489,14 +493,13 @@ function parseOrderByObject(tableConfig, orderByObj) {
489
493
  }
490
494
  return parts;
491
495
  }
492
-
493
- // ../bungres-orm/src/builders/delete.ts
494
496
  class DeleteBuilder {
495
497
  _table;
496
498
  _executor;
497
499
  _where = [];
498
500
  _returning;
499
501
  _comment;
502
+ _with = [];
500
503
  constructor(table2, executor) {
501
504
  this._table = table2;
502
505
  this._executor = executor;
@@ -515,6 +518,10 @@ class DeleteBuilder {
515
518
  }
516
519
  return this;
517
520
  }
521
+ with(...ctes) {
522
+ this._with.push(...ctes);
523
+ return this;
524
+ }
518
525
  returning(...columns) {
519
526
  this._returning = columns.length > 0 ? columns : ["*"];
520
527
  return this;
@@ -524,8 +531,20 @@ class DeleteBuilder {
524
531
  return this;
525
532
  }
526
533
  toSQL() {
527
- let query = `DELETE FROM ${getTableConfig(this._table).qualifiedName}`;
534
+ const tConfig = getTableConfig(this._table);
528
535
  const params = [];
536
+ let prefix = "";
537
+ if (this._with.length > 0) {
538
+ const cteStrs = [];
539
+ for (const cte of this._with) {
540
+ const chunk = cte.query.toSQL();
541
+ const offset = params.length;
542
+ params.push(...chunk.params);
543
+ cteStrs.push(`"${cte.alias}" AS (${chunk.sql.replace(/\$(\d+)/g, (_, n) => `$${parseInt(n) + offset}`)})`);
544
+ }
545
+ prefix = `WITH ${cteStrs.join(", ")} `;
546
+ }
547
+ let query = `DELETE FROM ${tConfig.qualifiedName}`;
529
548
  if (this._where.length > 0) {
530
549
  const combined = sqlJoin(this._where, " AND ");
531
550
  query += " WHERE " + combined.sql;
@@ -537,17 +556,19 @@ class DeleteBuilder {
537
556
  if (this._comment) {
538
557
  query += ` /* ${this._comment.replace(/\*\//g, "")} */`;
539
558
  }
540
- return { sql: query, params };
559
+ return { sql: prefix + query, params };
541
560
  }
542
561
  }
543
- // ../bungres-orm/src/builders/insert.ts
562
+
544
563
  class InsertBuilder {
545
564
  _table;
546
565
  _executor;
547
566
  _values = [];
548
567
  _onConflict;
568
+ _onConflictUpdateConfig;
549
569
  _returning;
550
570
  _comment;
571
+ _with = [];
551
572
  constructor(table2, executor) {
552
573
  this._table = table2;
553
574
  this._executor = executor;
@@ -574,6 +595,14 @@ class InsertBuilder {
574
595
  this._onConflict = clause;
575
596
  return this;
576
597
  }
598
+ onConflictDoUpdate(config2) {
599
+ this._onConflictUpdateConfig = config2;
600
+ return this;
601
+ }
602
+ with(...ctes) {
603
+ this._with.push(...ctes);
604
+ return this;
605
+ }
577
606
  returning(...columns) {
578
607
  this._returning = columns.length > 0 ? columns : ["*"];
579
608
  return this;
@@ -599,6 +628,17 @@ class InsertBuilder {
599
628
  }
600
629
  const columnsStr = keys.map((k) => `"${tConfig.columns[k]?.name ?? k}"`).join(", ");
601
630
  const params = [];
631
+ let prefix = "";
632
+ if (this._with.length > 0) {
633
+ const cteStrs = [];
634
+ for (const cte of this._with) {
635
+ const chunk = cte.query.toSQL();
636
+ const offset = params.length;
637
+ params.push(...chunk.params);
638
+ cteStrs.push(`"${cte.alias}" AS (${chunk.sql.replace(/\$(\d+)/g, (_, n) => `$${parseInt(n) + offset}`)})`);
639
+ }
640
+ prefix = `WITH ${cteStrs.join(", ")} `;
641
+ }
602
642
  const valuesStrs = this._values.map((v) => {
603
643
  const vals = keys.map((k) => {
604
644
  const val = v[k];
@@ -642,6 +682,52 @@ class InsertBuilder {
642
682
  query += " " + this._onConflict.sql.replace(/\$(\d+)/g, (_, n) => `$${parseInt(n) + offset}`);
643
683
  params.push(...this._onConflict.params);
644
684
  }
685
+ } else if (this._onConflictUpdateConfig) {
686
+ const config2 = this._onConflictUpdateConfig;
687
+ const targets = Array.isArray(config2.target) ? config2.target : [config2.target];
688
+ const targetStrs = [];
689
+ for (const t of targets) {
690
+ if (typeof t === "string") {
691
+ targetStrs.push(`"${tConfig.columns[t]?.name ?? t}"`);
692
+ } else {
693
+ const offset = params.length;
694
+ targetStrs.push(t.sql.replace(/\$(\d+)/g, (_, n) => `$${parseInt(n) + offset}`));
695
+ params.push(...t.params);
696
+ }
697
+ }
698
+ const setEntries = Object.entries(config2.set);
699
+ if (setEntries.length === 0)
700
+ throw new Error("InsertBuilder: onConflictDoUpdate requires 'set' fields");
701
+ const setClauses = setEntries.map(([key, value]) => {
702
+ const dbCol = tConfig.columns[key]?.name ?? key;
703
+ if (value && typeof value === "object" && "sql" in value && "params" in value) {
704
+ const chunk = value;
705
+ const offset = params.length;
706
+ params.push(...chunk.params);
707
+ return `"${dbCol}" = ${chunk.sql.replace(/\$(\d+)/g, (_, n) => `$${parseInt(n) + offset}`)}`;
708
+ }
709
+ if (value && typeof value === "object" && !(value instanceof Date)) {
710
+ const colType = tConfig.columns[key]?.dataType;
711
+ if (colType === "json" || colType === "jsonb") {
712
+ params.push(value);
713
+ } else if (Array.isArray(value)) {
714
+ const pgArray = "{" + value.map((item) => {
715
+ if (item === null || item === undefined)
716
+ return "NULL";
717
+ if (typeof item === "string")
718
+ return '"' + item.replace(/"/g, "\\\"") + '"';
719
+ return typeof item === "object" ? '"' + JSON.stringify(item).replace(/"/g, "\\\"") + '"' : String(item);
720
+ }).join(",") + "}";
721
+ params.push(pgArray);
722
+ } else {
723
+ params.push(JSON.stringify(value));
724
+ }
725
+ return `"${dbCol}" = $${params.length}`;
726
+ }
727
+ params.push(value);
728
+ return `"${dbCol}" = $${params.length}`;
729
+ });
730
+ query += ` ON CONFLICT (${targetStrs.join(", ")}) DO UPDATE SET ${setClauses.join(", ")}`;
645
731
  }
646
732
  if (this._returning) {
647
733
  query += " RETURNING " + (this._returning[0] === "*" ? Object.keys(tConfig.columns).map((c) => `"${tConfig.columns[c].name}" AS "${c}"`).join(", ") : this._returning.map((c) => `"${tConfig.columns[c]?.name ?? c}" AS "${c}"`).join(", "));
@@ -649,10 +735,10 @@ class InsertBuilder {
649
735
  if (this._comment) {
650
736
  query += ` /* ${this._comment.replace(/\*\//g, "")} */`;
651
737
  }
652
- return { sql: query, params };
738
+ return { sql: prefix + query, params };
653
739
  }
654
740
  }
655
- // ../bungres-orm/src/builders/select.ts
741
+
656
742
  class SelectBuilder {
657
743
  _table;
658
744
  _executor;
@@ -665,6 +751,8 @@ class SelectBuilder {
665
751
  _select;
666
752
  _selection;
667
753
  _joins = [];
754
+ _with = [];
755
+ _setOperations = [];
668
756
  _comment;
669
757
  constructor(table2, executor, selection) {
670
758
  this._table = table2;
@@ -675,12 +763,35 @@ class SelectBuilder {
675
763
  return this._executor.execute(this).then(onfulfilled, onrejected);
676
764
  }
677
765
  async single() {
766
+ if (this._limit === undefined) {
767
+ this.limit(1);
768
+ }
678
769
  return this._executor.executeSingle(this);
679
770
  }
680
771
  select(...columns) {
681
772
  this._select = columns;
682
773
  return this;
683
774
  }
775
+ with(...ctes) {
776
+ this._with.push(...ctes);
777
+ return this;
778
+ }
779
+ union(other) {
780
+ this._setOperations.push({ type: "UNION", builder: other });
781
+ return this;
782
+ }
783
+ unionAll(other) {
784
+ this._setOperations.push({ type: "UNION ALL", builder: other });
785
+ return this;
786
+ }
787
+ intersect(other) {
788
+ this._setOperations.push({ type: "INTERSECT", builder: other });
789
+ return this;
790
+ }
791
+ except(other) {
792
+ this._setOperations.push({ type: "EXCEPT", builder: other });
793
+ return this;
794
+ }
684
795
  comment(tag) {
685
796
  this._comment = tag;
686
797
  return this;
@@ -783,7 +894,23 @@ class SelectBuilder {
783
894
  }).join(", ");
784
895
  } else {
785
896
  const qName = getTableConfig(this._table).qualifiedName;
786
- cols = Object.keys(getTableConfig(this._table).columns).map((c) => `${qName}."${getTableConfig(this._table).columns[c].name}" AS "${c}"`).join(", ");
897
+ const colsKeys = Object.keys(getTableConfig(this._table).columns);
898
+ if (colsKeys.length === 0) {
899
+ cols = "*";
900
+ } else {
901
+ cols = colsKeys.map((c) => `${qName}."${getTableConfig(this._table).columns[c].name}" AS "${c}"`).join(", ");
902
+ }
903
+ }
904
+ let prefix = "";
905
+ if (this._with.length > 0) {
906
+ const cteStrs = [];
907
+ for (const cte of this._with) {
908
+ const chunk = cte.query.toSQL();
909
+ const offset = params.length;
910
+ params.push(...chunk.params);
911
+ cteStrs.push(`"${cte.alias}" AS (${chunk.sql.replace(/\$(\d+)/g, (_, n) => `$${parseInt(n) + offset}`)})`);
912
+ }
913
+ prefix = `WITH ${cteStrs.join(", ")} `;
787
914
  }
788
915
  let query = `SELECT ${cols} FROM ${getTableConfig(this._table).qualifiedName}`;
789
916
  if (this._joins.length > 0) {
@@ -843,10 +970,18 @@ class SelectBuilder {
843
970
  params.push(this._offset);
844
971
  query += ` OFFSET $${params.length}`;
845
972
  }
973
+ if (this._setOperations.length > 0) {
974
+ for (const op of this._setOperations) {
975
+ const chunk = op.builder.toSQL();
976
+ const offset = params.length;
977
+ params.push(...chunk.params);
978
+ query += ` ${op.type} ${chunk.sql.replace(/\$(\d+)/g, (_, n) => `$${parseInt(n) + offset}`)}`;
979
+ }
980
+ }
846
981
  if (this._comment) {
847
982
  query += ` /* ${this._comment.replace(/\*\//g, "")} */`;
848
983
  }
849
- return { sql: query, params };
984
+ return { sql: prefix + query, params };
850
985
  }
851
986
  }
852
987
 
@@ -861,7 +996,7 @@ class SelectBuilderIntermediate {
861
996
  return new SelectBuilder(table2, this._executor, this._selection);
862
997
  }
863
998
  }
864
- // ../bungres-orm/src/builders/update.ts
999
+
865
1000
  class UpdateBuilder {
866
1001
  _table;
867
1002
  _executor;
@@ -869,6 +1004,7 @@ class UpdateBuilder {
869
1004
  _where = [];
870
1005
  _returning;
871
1006
  _comment;
1007
+ _with = [];
872
1008
  constructor(table2, executor) {
873
1009
  this._table = table2;
874
1010
  this._executor = executor;
@@ -891,6 +1027,10 @@ class UpdateBuilder {
891
1027
  }
892
1028
  return this;
893
1029
  }
1030
+ with(...ctes) {
1031
+ this._with.push(...ctes);
1032
+ return this;
1033
+ }
894
1034
  returning(...columns) {
895
1035
  this._returning = columns.length > 0 ? columns : ["*"];
896
1036
  return this;
@@ -905,6 +1045,17 @@ class UpdateBuilder {
905
1045
  throw new Error("UpdateBuilder: no fields to set");
906
1046
  }
907
1047
  const params = [];
1048
+ let prefix = "";
1049
+ if (this._with.length > 0) {
1050
+ const cteStrs = [];
1051
+ for (const cte of this._with) {
1052
+ const chunk = cte.query.toSQL();
1053
+ const offset = params.length;
1054
+ params.push(...chunk.params);
1055
+ cteStrs.push(`"${cte.alias}" AS (${chunk.sql.replace(/\$(\d+)/g, (_, n) => `$${parseInt(n) + offset}`)})`);
1056
+ }
1057
+ prefix = `WITH ${cteStrs.join(", ")} `;
1058
+ }
908
1059
  const setClauses = entries.map(([key, value]) => {
909
1060
  const dbCol = getTableConfig(this._table).columns[key]?.name ?? key;
910
1061
  if (value && typeof value === "object" && "sql" in value && "params" in value) {
@@ -947,10 +1098,9 @@ class UpdateBuilder {
947
1098
  if (this._comment) {
948
1099
  query += ` /* ${this._comment.replace(/\*\//g, "")} */`;
949
1100
  }
950
- return { sql: query, params };
1101
+ return { sql: prefix + query, params };
951
1102
  }
952
1103
  }
953
- // ../bungres-orm/src/builders/relational.ts
954
1104
  function getPkColumn(tableConfig) {
955
1105
  if (tableConfig.primaryKeys?.length > 0)
956
1106
  return tableConfig.primaryKeys[0];
@@ -1208,8 +1358,6 @@ class RelationalQueryBuilder {
1208
1358
  return { sql: sql2, params };
1209
1359
  }
1210
1360
  }
1211
-
1212
- // ../bungres-orm/src/core/db.ts
1213
1361
  function parseDBName(url) {
1214
1362
  try {
1215
1363
  return new URL(url).pathname.slice(1);
@@ -1318,6 +1466,7 @@ class BungresDB {
1318
1466
 
1319
1467
  class BungresTransaction {
1320
1468
  _sql;
1469
+ _savepointCounter = 0;
1321
1470
  constructor(sql2) {
1322
1471
  this._sql = sql2;
1323
1472
  }
@@ -1352,6 +1501,19 @@ class BungresTransaction {
1352
1501
  const result2 = await this._sql.unsafe(query, params);
1353
1502
  return Array.from(result2);
1354
1503
  }
1504
+ async transaction(fn) {
1505
+ this._savepointCounter++;
1506
+ const spName = `sp_${this._savepointCounter}`;
1507
+ await this._sql.unsafe(`SAVEPOINT ${spName}`);
1508
+ try {
1509
+ const result2 = await fn(this);
1510
+ await this._sql.unsafe(`RELEASE SAVEPOINT ${spName}`);
1511
+ return result2;
1512
+ } catch (e) {
1513
+ await this._sql.unsafe(`ROLLBACK TO SAVEPOINT ${spName}`);
1514
+ throw e;
1515
+ }
1516
+ }
1355
1517
  }
1356
1518
  function bungres(config2) {
1357
1519
  const db2 = new BungresDB(config2);
@@ -1375,7 +1537,6 @@ function bungres(config2) {
1375
1537
  }
1376
1538
  return db2;
1377
1539
  }
1378
- // ../bungres-orm/src/ddl.ts
1379
1540
  function generateCreateTable(config2, ifNotExists = true) {
1380
1541
  const tableName = config2.schema ? `"${config2.schema}"."${config2.name}"` : `"${config2.name}"`;
1381
1542
  const exists = ifNotExists ? " IF NOT EXISTS" : "";
@@ -1385,8 +1546,8 @@ function generateCreateTable(config2, ifNotExists = true) {
1385
1546
  columnDefs.push(`PRIMARY KEY (${pkCols})`);
1386
1547
  }
1387
1548
  if (config2.checks) {
1388
- for (const check of config2.checks) {
1389
- columnDefs.push(`CHECK (${check})`);
1549
+ for (const check2 of config2.checks) {
1550
+ columnDefs.push(`CHECK (${check2})`);
1390
1551
  }
1391
1552
  }
1392
1553
  let sql2 = `CREATE TABLE${exists} ${tableName} (
@@ -1424,7 +1585,9 @@ function buildColumnDDL(key, col2, tableName) {
1424
1585
  if (col2.unique && !col2.primaryKey) {
1425
1586
  parts.push("UNIQUE");
1426
1587
  }
1427
- if (col2.defaultFn) {
1588
+ if (col2.generatedAs) {
1589
+ parts.push(`GENERATED ALWAYS AS (${col2.generatedAs}) STORED`);
1590
+ } else if (col2.defaultFn) {
1428
1591
  parts.push(`DEFAULT ${col2.defaultFn}`);
1429
1592
  } else if (col2.defaultValue !== undefined) {
1430
1593
  parts.push(`DEFAULT ${formatDefaultValue(col2.defaultValue, col2.dataType)}`);
@@ -1445,6 +1608,13 @@ function buildColumnDDL(key, col2, tableName) {
1445
1608
  return parts.join(" ");
1446
1609
  }
1447
1610
  function buildType(col2) {
1611
+ if (col2.enumConfig) {
1612
+ return `"${col2.enumConfig.enumName}"`;
1613
+ }
1614
+ if (col2.dataType.endsWith("[]")) {
1615
+ const baseType = buildType({ ...col2, dataType: col2.dataType.slice(0, -2) });
1616
+ return `${baseType}[]`;
1617
+ }
1448
1618
  switch (col2.dataType) {
1449
1619
  case "varchar":
1450
1620
  return col2.length ? `VARCHAR(${col2.length})` : "VARCHAR";
@@ -1479,12 +1649,12 @@ function formatDefaultValue(value, dataType) {
1479
1649
  }
1480
1650
  function buildIndex(table2, idx) {
1481
1651
  const tableName = table2.schema ? `"${table2.schema}"."${table2.name}"` : `"${table2.name}"`;
1482
- const unique = idx.unique ? "UNIQUE " : "";
1652
+ const unique2 = idx.unique ? "UNIQUE " : "";
1483
1653
  const using = idx.using ? ` USING ${idx.using.toUpperCase()}` : "";
1484
1654
  const cols = idx.columns.map((c) => `"${c}"`).join(", ");
1485
1655
  const where = idx.where ? ` WHERE ${idx.where}` : "";
1486
1656
  const idxName = idx.name ?? `idx_${table2.name}_${idx.columns.join("_")}`;
1487
- return `CREATE ${unique}INDEX IF NOT EXISTS "${idxName}" ON ${tableName}${using} (${cols})${where};`;
1657
+ return `CREATE ${unique2}INDEX IF NOT EXISTS "${idxName}" ON ${tableName}${using} (${cols})${where};`;
1488
1658
  }
1489
1659
  function generateAddColumn(tableName, schema, key, col2) {
1490
1660
  const tbl = schema ? `"${schema}"."${tableName}"` : `"${tableName}"`;
@@ -1494,6 +1664,38 @@ function generateDropColumn(tableName, schema, columnName) {
1494
1664
  const tbl = schema ? `"${schema}"."${tableName}"` : `"${tableName}"`;
1495
1665
  return `ALTER TABLE ${tbl} DROP COLUMN IF EXISTS "${columnName}";`;
1496
1666
  }
1667
+ function generateCreateEnum(name, values) {
1668
+ const vals = values.map((v) => `'${v.replace(/'/g, "''")}'`).join(", ");
1669
+ return `CREATE TYPE "${name}" AS ENUM (${vals});`;
1670
+ }
1671
+ function generateDropEnum(name, ifExists = true) {
1672
+ return `DROP TYPE${ifExists ? " IF EXISTS" : ""} "${name}";`;
1673
+ }
1674
+ function inlineParams(chunk) {
1675
+ let { sql: sql2, params } = chunk;
1676
+ return sql2.replace(/\$(\d+)/g, (_, n) => {
1677
+ const p = params[parseInt(n) - 1];
1678
+ if (typeof p === "string")
1679
+ return `'${p.replace(/'/g, "''")}'`;
1680
+ if (typeof p === "number")
1681
+ return String(p);
1682
+ if (typeof p === "boolean")
1683
+ return p ? "TRUE" : "FALSE";
1684
+ if (p === null)
1685
+ return "NULL";
1686
+ return `'${String(p).replace(/'/g, "''")}'`;
1687
+ });
1688
+ }
1689
+ function generateCreateView(view) {
1690
+ const kind = view.materialized ? "MATERIALIZED VIEW" : "VIEW";
1691
+ const inlineSql = inlineParams(view.query.toSQL());
1692
+ return `CREATE ${kind} "${view.name}" AS ${inlineSql};`;
1693
+ }
1694
+ function generateDropView(view, ifExists = true) {
1695
+ const kind = view.materialized ? "MATERIALIZED VIEW" : "VIEW";
1696
+ return `DROP ${kind}${ifExists ? " IF EXISTS" : ""} "${view.name}";`;
1697
+ }
1698
+
1497
1699
  // src/schema-loader.ts
1498
1700
  async function loadSchemas(patterns, cwd = process.cwd()) {
1499
1701
  const globs = Array.isArray(patterns) ? patterns : [patterns];
@@ -1506,11 +1708,27 @@ async function loadSchemas(patterns, cwd = process.cwd()) {
1506
1708
  for (const [exportName, value] of Object.entries(mod)) {
1507
1709
  if (isTable(value)) {
1508
1710
  entries.push({
1711
+ type: "table",
1509
1712
  exportName,
1510
1713
  config: value[TableConfigSymbol],
1511
1714
  table: value,
1512
1715
  filePath: absPath
1513
1716
  });
1717
+ } else if (isEnum(value)) {
1718
+ entries.push({
1719
+ type: "enum",
1720
+ exportName,
1721
+ enumName: value.enumName,
1722
+ enumValues: value.enumValues,
1723
+ filePath: absPath
1724
+ });
1725
+ } else if (isView(value)) {
1726
+ entries.push({
1727
+ type: "view",
1728
+ exportName,
1729
+ config: value,
1730
+ filePath: absPath
1731
+ });
1514
1732
  }
1515
1733
  }
1516
1734
  }
@@ -1520,6 +1738,12 @@ async function loadSchemas(patterns, cwd = process.cwd()) {
1520
1738
  function isTable(value) {
1521
1739
  return typeof value === "object" && value !== null && TableConfigSymbol in value;
1522
1740
  }
1741
+ function isEnum(value) {
1742
+ return typeof value === "function" && "enumName" in value && "enumValues" in value && Array.isArray(value.enumValues);
1743
+ }
1744
+ function isView(value) {
1745
+ return typeof value === "object" && value !== null && "name" in value && "query" in value && typeof value.query === "object" && typeof value.query.toSQL === "function";
1746
+ }
1523
1747
 
1524
1748
  // ../../node_modules/.bun/@clack+core@1.4.3/node_modules/@clack/core/dist/index.mjs
1525
1749
  import { styleText } from "util";
@@ -1575,7 +1799,7 @@ var getStringTruncatedWidth = (input2, truncationOptions = {}, widthOptions = {}
1575
1799
  [CJKT_WIDE_RE, WIDE_WIDTH]
1576
1800
  ];
1577
1801
  let indexPrev = 0;
1578
- let index2 = 0;
1802
+ let index = 0;
1579
1803
  let length = input2.length;
1580
1804
  let lengthExtra = 0;
1581
1805
  let truncationEnabled = false;
@@ -1587,8 +1811,8 @@ var getStringTruncatedWidth = (input2, truncationOptions = {}, widthOptions = {}
1587
1811
  let widthExtra = 0;
1588
1812
  outer:
1589
1813
  while (true) {
1590
- if (unmatchedEnd > unmatchedStart || index2 >= length && index2 > indexPrev) {
1591
- const unmatched = input2.slice(unmatchedStart, unmatchedEnd) || input2.slice(indexPrev, index2);
1814
+ if (unmatchedEnd > unmatchedStart || index >= length && index > indexPrev) {
1815
+ const unmatched = input2.slice(unmatchedStart, unmatchedEnd) || input2.slice(indexPrev, index);
1592
1816
  lengthExtra = 0;
1593
1817
  for (const char of unmatched.replaceAll(MODIFIER_RE, "")) {
1594
1818
  const codePoint = char.codePointAt(0) || 0;
@@ -1611,17 +1835,17 @@ var getStringTruncatedWidth = (input2, truncationOptions = {}, widthOptions = {}
1611
1835
  }
1612
1836
  unmatchedStart = unmatchedEnd = 0;
1613
1837
  }
1614
- if (index2 >= length) {
1838
+ if (index >= length) {
1615
1839
  break outer;
1616
1840
  }
1617
1841
  for (let i = 0, l = PARSE_BLOCKS.length;i < l; i++) {
1618
1842
  const [BLOCK_RE, BLOCK_WIDTH] = PARSE_BLOCKS[i];
1619
- BLOCK_RE.lastIndex = index2;
1843
+ BLOCK_RE.lastIndex = index;
1620
1844
  if (BLOCK_RE.test(input2)) {
1621
- lengthExtra = BLOCK_RE === CJKT_WIDE_RE ? getCodePointsLength(input2.slice(index2, BLOCK_RE.lastIndex)) : BLOCK_RE === EMOJI_RE ? 1 : BLOCK_RE.lastIndex - index2;
1845
+ lengthExtra = BLOCK_RE === CJKT_WIDE_RE ? getCodePointsLength(input2.slice(index, BLOCK_RE.lastIndex)) : BLOCK_RE === EMOJI_RE ? 1 : BLOCK_RE.lastIndex - index;
1622
1846
  widthExtra = lengthExtra * BLOCK_WIDTH;
1623
1847
  if (width + widthExtra > truncationLimit) {
1624
- truncationIndex = Math.min(truncationIndex, index2 + Math.floor((truncationLimit - width) / BLOCK_WIDTH));
1848
+ truncationIndex = Math.min(truncationIndex, index + Math.floor((truncationLimit - width) / BLOCK_WIDTH));
1625
1849
  }
1626
1850
  if (width + widthExtra > LIMIT) {
1627
1851
  truncationEnabled = true;
@@ -1629,12 +1853,12 @@ var getStringTruncatedWidth = (input2, truncationOptions = {}, widthOptions = {}
1629
1853
  }
1630
1854
  width += widthExtra;
1631
1855
  unmatchedStart = indexPrev;
1632
- unmatchedEnd = index2;
1633
- index2 = indexPrev = BLOCK_RE.lastIndex;
1856
+ unmatchedEnd = index;
1857
+ index = indexPrev = BLOCK_RE.lastIndex;
1634
1858
  continue outer;
1635
1859
  }
1636
1860
  }
1637
- index2 += 1;
1861
+ index += 1;
1638
1862
  }
1639
1863
  return {
1640
1864
  width: truncationEnabled ? truncationLimit : width,
@@ -1693,7 +1917,7 @@ var getClosingCode = (openingCode) => {
1693
1917
  };
1694
1918
  var wrapAnsiCode = (code2) => `${ESC}${ANSI_CSI}${code2}${ANSI_SGR_TERMINATOR}`;
1695
1919
  var wrapAnsiHyperlink = (url) => `${ESC}${ANSI_ESCAPE_LINK}${url}${ANSI_ESCAPE_BELL}`;
1696
- var wrapWord = (rows, word, columns2) => {
1920
+ var wrapWord = (rows, word, columns) => {
1697
1921
  const characters = word[Symbol.iterator]();
1698
1922
  let isInsideEscape = false;
1699
1923
  let isInsideLinkEscape = false;
@@ -1705,7 +1929,7 @@ var wrapWord = (rows, word, columns2) => {
1705
1929
  while (!currentCharacter.done) {
1706
1930
  const character = currentCharacter.value;
1707
1931
  const characterLength = dist_default2(character);
1708
- if (visible + characterLength <= columns2) {
1932
+ if (visible + characterLength <= columns) {
1709
1933
  rows[rows.length - 1] += character;
1710
1934
  } else {
1711
1935
  rows.push(character);
@@ -1726,7 +1950,7 @@ var wrapWord = (rows, word, columns2) => {
1726
1950
  }
1727
1951
  } else {
1728
1952
  visible += characterLength;
1729
- if (visible === columns2 && !nextCharacter.done) {
1953
+ if (visible === columns && !nextCharacter.done) {
1730
1954
  rows.push("");
1731
1955
  visible = 0;
1732
1956
  }
@@ -1754,7 +1978,7 @@ var stringVisibleTrimSpacesRight = (string) => {
1754
1978
  }
1755
1979
  return words.slice(0, last).join(" ") + words.slice(last).join("");
1756
1980
  };
1757
- var exec = (string, columns2, options = {}) => {
1981
+ var exec = (string, columns, options = {}) => {
1758
1982
  if (options.trim !== false && string.trim() === "") {
1759
1983
  return "";
1760
1984
  }
@@ -1764,8 +1988,8 @@ var exec = (string, columns2, options = {}) => {
1764
1988
  const words = string.split(" ");
1765
1989
  let rows = [""];
1766
1990
  let rowLength = 0;
1767
- for (let index2 = 0;index2 < words.length; index2++) {
1768
- const word = words[index2];
1991
+ for (let index = 0;index < words.length; index++) {
1992
+ const word = words[index];
1769
1993
  if (options.trim !== false) {
1770
1994
  const row = rows.at(-1) ?? "";
1771
1995
  const trimmed = row.trimStart();
@@ -1774,8 +1998,8 @@ var exec = (string, columns2, options = {}) => {
1774
1998
  rowLength = dist_default2(trimmed);
1775
1999
  }
1776
2000
  }
1777
- if (index2 !== 0) {
1778
- if (rowLength >= columns2 && (options.wordWrap === false || options.trim === false)) {
2001
+ if (index !== 0) {
2002
+ if (rowLength >= columns && (options.wordWrap === false || options.trim === false)) {
1779
2003
  rows.push("");
1780
2004
  rowLength = 0;
1781
2005
  }
@@ -1785,28 +2009,28 @@ var exec = (string, columns2, options = {}) => {
1785
2009
  }
1786
2010
  }
1787
2011
  const wordLength = dist_default2(word);
1788
- if (options.hard && wordLength > columns2) {
1789
- const remainingColumns = columns2 - rowLength;
1790
- const breaksStartingThisLine = 1 + Math.floor((wordLength - remainingColumns - 1) / columns2);
1791
- const breaksStartingNextLine = Math.floor((wordLength - 1) / columns2);
2012
+ if (options.hard && wordLength > columns) {
2013
+ const remainingColumns = columns - rowLength;
2014
+ const breaksStartingThisLine = 1 + Math.floor((wordLength - remainingColumns - 1) / columns);
2015
+ const breaksStartingNextLine = Math.floor((wordLength - 1) / columns);
1792
2016
  if (breaksStartingNextLine < breaksStartingThisLine) {
1793
2017
  rows.push("");
1794
2018
  }
1795
- wrapWord(rows, word, columns2);
2019
+ wrapWord(rows, word, columns);
1796
2020
  rowLength = dist_default2(rows.at(-1) ?? "");
1797
2021
  continue;
1798
2022
  }
1799
- if (rowLength + wordLength > columns2 && rowLength && wordLength) {
1800
- if (options.wordWrap === false && rowLength < columns2) {
1801
- wrapWord(rows, word, columns2);
2023
+ if (rowLength + wordLength > columns && rowLength && wordLength) {
2024
+ if (options.wordWrap === false && rowLength < columns) {
2025
+ wrapWord(rows, word, columns);
1802
2026
  rowLength = dist_default2(rows.at(-1) ?? "");
1803
2027
  continue;
1804
2028
  }
1805
2029
  rows.push("");
1806
2030
  rowLength = 0;
1807
2031
  }
1808
- if (rowLength + wordLength > columns2 && options.wordWrap === false) {
1809
- wrapWord(rows, word, columns2);
2032
+ if (rowLength + wordLength > columns && options.wordWrap === false) {
2033
+ wrapWord(rows, word, columns);
1810
2034
  rowLength = dist_default2(rows.at(-1) ?? "");
1811
2035
  continue;
1812
2036
  }
@@ -1863,8 +2087,8 @@ var exec = (string, columns2, options = {}) => {
1863
2087
  return returnValue;
1864
2088
  };
1865
2089
  var CRLF_OR_LF = /\r?\n/;
1866
- function wrapAnsi(string, columns2, options) {
1867
- return String(string).normalize().split(CRLF_OR_LF).map((line2) => exec(line2, columns2, options)).join(`
2090
+ function wrapAnsi(string, columns, options) {
2091
+ return String(string).normalize().split(CRLF_OR_LF).map((line2) => exec(line2, columns, options)).join(`
1868
2092
  `);
1869
2093
  }
1870
2094
 
@@ -2887,9 +3111,12 @@ async function runDrop(config2, opts = {}) {
2887
3111
  const s = spinner();
2888
3112
  s.start("Loading schemas...");
2889
3113
  const schemas2 = await loadSchemas(config2.schema);
2890
- if (schemas2.length === 0) {
3114
+ const tableSchemas = schemas2.filter((s2) => s2.type === "table");
3115
+ const enumSchemas = schemas2.filter((s2) => s2.type === "enum");
3116
+ const viewSchemas = schemas2.filter((s2) => s2.type === "view");
3117
+ if (tableSchemas.length === 0 && enumSchemas.length === 0 && viewSchemas.length === 0) {
2891
3118
  s.stop("No schemas found.");
2892
- log.warn(import_picocolors.default.yellow("No table definitions found in schema files."));
3119
+ log.warn(import_picocolors.default.yellow("No definitions found in schema files."));
2893
3120
  outro("Failed.");
2894
3121
  return;
2895
3122
  }
@@ -2911,25 +3138,31 @@ async function runDrop(config2, opts = {}) {
2911
3138
  WHERE table_schema = $1 AND table_name = $2
2912
3139
  ) AS exists`, [config2.migrationsSchema, "__bungres_push"]);
2913
3140
  const pushTableExists = pushTableCheck[0]?.exists ?? false;
2914
- const tablesToDrop = schemas2.filter((sch) => existingTableNames.has(sch.config.name));
2915
- if (tablesToDrop.length === 0 && !migrationTableExists && !pushTableExists) {
3141
+ const tablesToDrop = tableSchemas.filter((sch) => existingTableNames.has(sch.config.name));
3142
+ if (tablesToDrop.length === 0 && enumSchemas.length === 0 && viewSchemas.length === 0 && !migrationTableExists && !pushTableExists) {
2916
3143
  ms.stop("Nothing to do.");
2917
- log.success(import_picocolors.default.green("No tables to drop (they either don't exist or were already dropped)."));
3144
+ log.success(import_picocolors.default.green("No items to drop (they either don't exist or were already dropped)."));
2918
3145
  outro("Done.");
2919
3146
  return;
2920
3147
  }
2921
3148
  ms.stop("Database check complete.");
2922
- const tableNamesToPrint = tablesToDrop.map((sch) => (sch.config.schema ? sch.config.schema + "." : "") + sch.config.name);
3149
+ const namesToPrint = tablesToDrop.map((sch) => (sch.config.schema ? sch.config.schema + "." : "") + sch.config.name + " (table)");
3150
+ for (const v of viewSchemas) {
3151
+ namesToPrint.push(`${v.config.name} (view)`);
3152
+ }
3153
+ for (const e of enumSchemas) {
3154
+ namesToPrint.push(`${e.enumName} (enum)`);
3155
+ }
2923
3156
  if (migrationTableExists) {
2924
- tableNamesToPrint.push(`${config2.migrationsSchema}.${config2.migrationsTable}`);
3157
+ namesToPrint.push(`${config2.migrationsSchema}.${config2.migrationsTable} (table)`);
2925
3158
  }
2926
3159
  if (pushTableExists) {
2927
- tableNamesToPrint.push(`${config2.migrationsSchema}.__bungres_push`);
3160
+ namesToPrint.push(`${config2.migrationsSchema}.__bungres_push (table)`);
2928
3161
  }
2929
3162
  if (!opts.force) {
2930
3163
  log.warn(import_picocolors.default.bgRed(import_picocolors.default.white(" \u26A0\uFE0F WARNING ")));
2931
- log.message(import_picocolors.default.bold(import_picocolors.default.red("This will drop the following tables and ALL their data:")));
2932
- for (const name of tableNamesToPrint) {
3164
+ log.message(import_picocolors.default.bold(import_picocolors.default.red("This will drop the following items and ALL their data:")));
3165
+ for (const name of namesToPrint) {
2933
3166
  log.info(import_picocolors.default.yellow(` - ${name}`));
2934
3167
  }
2935
3168
  const confirm2 = await confirm({
@@ -2943,11 +3176,22 @@ async function runDrop(config2, opts = {}) {
2943
3176
  }
2944
3177
  const exSpinner = spinner();
2945
3178
  exSpinner.start("Dropping tables...");
3179
+ for (const entry of viewSchemas) {
3180
+ const isMat = entry.config.materialized ? "MATERIALIZED VIEW" : "VIEW";
3181
+ const ddl = `DROP ${isMat} IF EXISTS "${entry.config.name}" CASCADE;`;
3182
+ await sql2.unsafe(ddl);
3183
+ log.step(import_picocolors.default.gray(`Dropped ${entry.config.name}`));
3184
+ }
2946
3185
  for (const entry of tablesToDrop) {
2947
3186
  const ddl = `DROP TABLE IF EXISTS "${entry.config.name}" CASCADE;`;
2948
3187
  await sql2.unsafe(ddl);
2949
3188
  log.step(import_picocolors.default.gray(`Dropped ${entry.config.name}`));
2950
3189
  }
3190
+ for (const entry of enumSchemas) {
3191
+ const ddl = `DROP TYPE IF EXISTS "${entry.enumName}" CASCADE;`;
3192
+ await sql2.unsafe(ddl);
3193
+ log.step(import_picocolors.default.gray(`Dropped ${entry.enumName}`));
3194
+ }
2951
3195
  if (migrationTableExists) {
2952
3196
  const qualifiedMigTable = `"${config2.migrationsSchema}"."${config2.migrationsTable}"`;
2953
3197
  await sql2.unsafe(`DROP TABLE IF EXISTS ${qualifiedMigTable} CASCADE`);
@@ -2957,7 +3201,7 @@ async function runDrop(config2, opts = {}) {
2957
3201
  await sql2.unsafe(`DROP TABLE IF EXISTS "${config2.migrationsSchema}"."__bungres_push" CASCADE`);
2958
3202
  log.step(import_picocolors.default.gray(`Dropped ${config2.migrationsSchema}.__bungres_push`));
2959
3203
  }
2960
- exSpinner.stop("Tables dropped.");
3204
+ exSpinner.stop("Items dropped.");
2961
3205
  outro(import_picocolors.default.cyan("\u2728 Drop complete."));
2962
3206
  } catch (err) {
2963
3207
  log.error(import_picocolors.default.red(`Drop failed: ${err.message}`));
@@ -3064,12 +3308,56 @@ function diffSchemas(prev, next) {
3064
3308
  const statements = [];
3065
3309
  const summary = [];
3066
3310
  const warnings = [];
3067
- const prevTables = new Set(Object.keys(prev));
3068
- const nextTables = new Set(Object.keys(next));
3311
+ const prevEnums = prev.enums || {};
3312
+ const nextEnums = next.enums || {};
3313
+ const prevEnumNames = new Set(Object.keys(prevEnums));
3314
+ const nextEnumNames = new Set(Object.keys(nextEnums));
3315
+ for (const enumName of nextEnumNames) {
3316
+ if (!prevEnumNames.has(enumName)) {
3317
+ const e = nextEnums[enumName];
3318
+ statements.push(generateCreateEnum(e.enumName, e.enumValues));
3319
+ summary.push(`CREATE TYPE ${e.enumName}`);
3320
+ }
3321
+ }
3322
+ for (const enumName of prevEnumNames) {
3323
+ if (!nextEnumNames.has(enumName)) {
3324
+ const e = prevEnums[enumName];
3325
+ statements.push(generateDropEnum(e.enumName, true));
3326
+ summary.push(`DROP TYPE ${e.enumName}`);
3327
+ }
3328
+ }
3329
+ for (const enumName of nextEnumNames) {
3330
+ if (prevEnumNames.has(enumName)) {
3331
+ const p2 = prevEnums[enumName];
3332
+ const n2 = nextEnums[enumName];
3333
+ if (JSON.stringify(p2.enumValues) !== JSON.stringify(n2.enumValues)) {
3334
+ warnings.push(`Enum '${n2.enumName}' values have changed. Bungres Kit currently requires manual migration for ALTER TYPE ... ADD VALUE.`);
3335
+ }
3336
+ }
3337
+ }
3338
+ const prevViews = prev.views || {};
3339
+ const nextViews = next.views || {};
3340
+ const prevViewNames = new Set(Object.keys(prevViews));
3341
+ const nextViewNames = new Set(Object.keys(nextViews));
3342
+ for (const viewName of prevViewNames) {
3343
+ if (!nextViewNames.has(viewName)) {
3344
+ statements.push(generateDropView(prevViews[viewName]));
3345
+ summary.push(`DROP VIEW ${viewName}`);
3346
+ } else {
3347
+ const pSql = generateCreateView(prevViews[viewName]);
3348
+ const nSql = generateCreateView(nextViews[viewName]);
3349
+ if (pSql !== nSql) {
3350
+ statements.push(generateDropView(prevViews[viewName]));
3351
+ summary.push(`DROP VIEW ${viewName} (for recreation)`);
3352
+ }
3353
+ }
3354
+ }
3355
+ const prevTables = new Set(Object.keys(prev.tables || {}));
3356
+ const nextTables = new Set(Object.keys(next.tables || {}));
3069
3357
  const newTableConfigs = [];
3070
3358
  for (const tableName of nextTables) {
3071
3359
  if (!prevTables.has(tableName)) {
3072
- newTableConfigs.push(next[tableName]);
3360
+ newTableConfigs.push(next.tables[tableName]);
3073
3361
  }
3074
3362
  }
3075
3363
  for (const config2 of topoSortConfigs(newTableConfigs)) {
@@ -3078,7 +3366,7 @@ function diffSchemas(prev, next) {
3078
3366
  }
3079
3367
  for (const tableName of prevTables) {
3080
3368
  if (!nextTables.has(tableName)) {
3081
- const config2 = prev[tableName];
3369
+ const config2 = prev.tables[tableName];
3082
3370
  const tbl = config2.schema ? `"${config2.schema}"."${tableName}"` : `"${tableName}"`;
3083
3371
  statements.push(`DROP TABLE IF EXISTS ${tbl};`);
3084
3372
  summary.push(`DROP TABLE ${tableName}`);
@@ -3088,8 +3376,8 @@ function diffSchemas(prev, next) {
3088
3376
  for (const tableName of nextTables) {
3089
3377
  if (!prevTables.has(tableName))
3090
3378
  continue;
3091
- const prevConfig = prev[tableName];
3092
- const nextConfig = next[tableName];
3379
+ const prevConfig = prev.tables[tableName];
3380
+ const nextConfig = next.tables[tableName];
3093
3381
  const prevCols = prevConfig.columns;
3094
3382
  const nextCols = nextConfig.columns;
3095
3383
  const prevColNames = new Set(Object.keys(prevCols));
@@ -3144,11 +3432,11 @@ function diffSchemas(prev, next) {
3144
3432
  const idxName = idx.name ?? `idx_${tableName}_${idx.columns.join("_")}`;
3145
3433
  if (!prevIdxNames.has(idxName)) {
3146
3434
  const tbl = nextConfig.schema ? `"${nextConfig.schema}"."${tableName}"` : `"${tableName}"`;
3147
- const unique2 = idx.unique ? "UNIQUE " : "";
3435
+ const unique = idx.unique ? "UNIQUE " : "";
3148
3436
  const using = idx.using ? ` USING ${idx.using.toUpperCase()}` : "";
3149
3437
  const cols = idx.columns.map((c2) => `"${c2}"`).join(", ");
3150
3438
  const where = idx.where ? ` WHERE ${idx.where}` : "";
3151
- statements.push(`CREATE ${unique2}INDEX IF NOT EXISTS "${idxName}" ON ${tbl}${using} (${cols})${where};`);
3439
+ statements.push(`CREATE ${unique}INDEX IF NOT EXISTS "${idxName}" ON ${tbl}${using} (${cols})${where};`);
3152
3440
  summary.push(`CREATE INDEX ${idxName} ON ${tableName}`);
3153
3441
  }
3154
3442
  }
@@ -3161,6 +3449,19 @@ function diffSchemas(prev, next) {
3161
3449
  }
3162
3450
  }
3163
3451
  }
3452
+ for (const viewName of nextViewNames) {
3453
+ if (!prevViewNames.has(viewName)) {
3454
+ statements.push(generateCreateView(nextViews[viewName]));
3455
+ summary.push(`CREATE VIEW ${viewName}`);
3456
+ } else {
3457
+ const pSql = generateCreateView(prevViews[viewName]);
3458
+ const nSql = generateCreateView(nextViews[viewName]);
3459
+ if (pSql !== nSql) {
3460
+ statements.push(generateCreateView(nextViews[viewName]));
3461
+ summary.push(`CREATE VIEW ${viewName} (recreated)`);
3462
+ }
3463
+ }
3464
+ }
3164
3465
  return { statements, summary, warnings };
3165
3466
  }
3166
3467
  function topoSortConfigs(tables) {
@@ -3188,7 +3489,14 @@ function topoSortConfigs(tables) {
3188
3489
  function diffColumn(prev, next) {
3189
3490
  const changes = [];
3190
3491
  const col2 = `"${next.name}"`;
3492
+ const prevDef = prev.defaultFn ?? (prev.defaultValue !== undefined ? String(prev.defaultValue) : undefined);
3493
+ const nextDef = next.defaultFn ?? (next.defaultValue !== undefined ? String(next.defaultValue) : undefined);
3494
+ let typeChanged = false;
3191
3495
  if (prev.dataType !== next.dataType) {
3496
+ typeChanged = true;
3497
+ if (prevDef !== undefined) {
3498
+ changes.push(`ALTER COLUMN ${col2} DROP DEFAULT`);
3499
+ }
3192
3500
  changes.push(`ALTER COLUMN ${col2} TYPE ${next.dataType.toUpperCase()} USING ${col2}::${next.dataType}`);
3193
3501
  } else {
3194
3502
  const prevLen = prev.length;
@@ -3205,9 +3513,12 @@ function diffColumn(prev, next) {
3205
3513
  if (prev.notNull && !next.notNull) {
3206
3514
  changes.push(`ALTER COLUMN ${col2} DROP NOT NULL`);
3207
3515
  }
3208
- const prevDef = prev.defaultFn ?? (prev.defaultValue !== undefined ? String(prev.defaultValue) : undefined);
3209
- const nextDef = next.defaultFn ?? (next.defaultValue !== undefined ? String(next.defaultValue) : undefined);
3210
- if (prevDef !== nextDef) {
3516
+ if (typeChanged) {
3517
+ if (nextDef !== undefined) {
3518
+ const val = next.defaultFn ?? formatDefault(next.defaultValue, next.dataType);
3519
+ changes.push(`ALTER COLUMN ${col2} SET DEFAULT ${val}`);
3520
+ }
3521
+ } else if (prevDef !== nextDef) {
3211
3522
  if (nextDef !== undefined) {
3212
3523
  const val = next.defaultFn ?? formatDefault(next.defaultValue, next.dataType);
3213
3524
  changes.push(`ALTER COLUMN ${col2} SET DEFAULT ${val}`);
@@ -3250,19 +3561,46 @@ async function runGenerate(config2, name) {
3250
3561
  const metaDir = join3(migrationsDir, "meta");
3251
3562
  await Bun.$`mkdir -p ${migrationsDir}`.quiet();
3252
3563
  await Bun.$`mkdir -p ${metaDir}`.quiet();
3253
- const currentSnapshot = Object.fromEntries(schemas2.map((s2) => [s2.config.name, s2.config]));
3254
- let prevSnapshot = {};
3564
+ const currentSnapshot = { tables: {}, enums: {}, views: {} };
3565
+ for (const s2 of schemas2) {
3566
+ if (s2.type === "table") {
3567
+ currentSnapshot.tables[s2.config.name] = s2.config;
3568
+ } else if (s2.type === "enum") {
3569
+ currentSnapshot.enums[s2.enumName] = { enumName: s2.enumName, enumValues: s2.enumValues };
3570
+ } else if (s2.type === "view") {
3571
+ currentSnapshot.views[s2.config.name] = s2.config;
3572
+ }
3573
+ }
3574
+ let prevSnapshot = { tables: {}, enums: {}, views: {} };
3255
3575
  let isFirstMigration = true;
3256
3576
  try {
3257
3577
  const files = readdirSync(metaDir).filter((f2) => f2.endsWith("_snapshot.json")).sort();
3258
3578
  if (files.length > 0) {
3259
3579
  const latest = files[files.length - 1];
3260
- prevSnapshot = JSON.parse(await Bun.file(join3(metaDir, latest)).text());
3580
+ const parsed = JSON.parse(await Bun.file(join3(metaDir, latest)).text());
3581
+ if (parsed.tables || parsed.enums || parsed.views) {
3582
+ prevSnapshot = {
3583
+ tables: parsed.tables || {},
3584
+ enums: parsed.enums || {},
3585
+ views: parsed.views || {}
3586
+ };
3587
+ } else {
3588
+ prevSnapshot = { tables: parsed, enums: {}, views: {} };
3589
+ }
3261
3590
  isFirstMigration = false;
3262
3591
  } else {
3263
3592
  const legacyFile2 = Bun.file(join3(migrationsDir, ".snapshot.json"));
3264
3593
  if (await legacyFile2.exists()) {
3265
- prevSnapshot = JSON.parse(await legacyFile2.text());
3594
+ const parsed = JSON.parse(await legacyFile2.text());
3595
+ if (parsed.tables || parsed.enums || parsed.views) {
3596
+ prevSnapshot = {
3597
+ tables: parsed.tables || {},
3598
+ enums: parsed.enums || {},
3599
+ views: parsed.views || {}
3600
+ };
3601
+ } else {
3602
+ prevSnapshot = { tables: parsed, enums: {}, views: {} };
3603
+ }
3266
3604
  isFirstMigration = false;
3267
3605
  }
3268
3606
  }
@@ -3282,17 +3620,30 @@ async function runGenerate(config2, name) {
3282
3620
  let summary;
3283
3621
  let warnings = [];
3284
3622
  if (isFirstMigration) {
3285
- const sorted = topoSort(schemas2);
3286
- upStatements = sorted.flatMap((entry) => [
3287
- `-- ${entry.exportName}`,
3288
- generateCreateTable(entry.config, true),
3289
- ``
3290
- ]);
3291
- downStatements = [...sorted].reverse().flatMap((entry) => {
3623
+ const tableSchemas = schemas2.filter((s2) => s2.type === "table");
3624
+ const enumSchemas = schemas2.filter((s2) => s2.type === "enum");
3625
+ const viewSchemas = schemas2.filter((s2) => s2.type === "view");
3626
+ const sorted = topoSort(tableSchemas);
3627
+ upStatements = [];
3628
+ downStatements = [];
3629
+ summary = [];
3630
+ for (const e of enumSchemas) {
3631
+ upStatements.push(`-- ${e.exportName}`, `CREATE TYPE "${e.enumName}" AS ENUM (${e.enumValues.map((v) => `'${v.replace(/'/g, "''")}'`).join(", ")});`, ``);
3632
+ downStatements.unshift(`DROP TYPE IF EXISTS "${e.enumName}";`);
3633
+ summary.push(`CREATE TYPE ${e.enumName}`);
3634
+ }
3635
+ for (const entry of sorted) {
3636
+ upStatements.push(`-- ${entry.exportName}`, generateCreateTable(entry.config, true), ``);
3292
3637
  const tbl = entry.config.schema ? `"${entry.config.schema}"."${entry.config.name}"` : `"${entry.config.name}"`;
3293
- return [`DROP TABLE IF EXISTS ${tbl};`];
3294
- });
3295
- summary = sorted.map((s2) => `CREATE TABLE ${s2.config.name}`);
3638
+ downStatements.unshift(`DROP TABLE IF EXISTS ${tbl};`);
3639
+ summary.push(`CREATE TABLE ${entry.config.name}`);
3640
+ }
3641
+ for (const v of viewSchemas) {
3642
+ upStatements.push(`-- ${v.exportName}`, generateCreateView(v.config), ``);
3643
+ const isMat = v.config.materialized ? "MATERIALIZED VIEW" : "VIEW";
3644
+ downStatements.unshift(`DROP ${isMat} IF EXISTS "${v.config.name}";`);
3645
+ summary.push(`CREATE ${isMat} ${v.config.name}`);
3646
+ }
3296
3647
  } else {
3297
3648
  const upDiff = diffSchemas(prevSnapshot, currentSnapshot);
3298
3649
  if (upDiff.statements.length === 0) {
@@ -3424,12 +3775,12 @@ export default defineConfig({
3424
3775
  uuid,
3425
3776
  varchar,
3426
3777
  timestamptz,
3427
- table,
3778
+ pgTable,
3428
3779
  unique,
3429
3780
  index
3430
3781
  } from "@bungres/orm";
3431
3782
 
3432
- export const users = table("users", {
3783
+ export const users = pgTable("users", {
3433
3784
  id: uuid({ primaryKey: true }),
3434
3785
  name: varchar({ length: 255, notNull: true }),
3435
3786
  email: varchar({ length: 255, notNull: true }),
@@ -3470,7 +3821,7 @@ export const db = bungres({ url, schema });
3470
3821
  // src/commands/pull.ts
3471
3822
  import { resolve as resolve4, join as join5 } from "path";
3472
3823
  async function introspectDb(sql2, dbSchema) {
3473
- const columns2 = await sql2.unsafe(`SELECT
3824
+ const columns = await sql2.unsafe(`SELECT
3474
3825
  c.table_name,
3475
3826
  c.column_name,
3476
3827
  c.data_type,
@@ -3504,7 +3855,7 @@ async function introspectDb(sql2, dbSchema) {
3504
3855
  const indexes = await sql2.unsafe(`SELECT tablename, indexname, indexdef
3505
3856
  FROM pg_indexes
3506
3857
  WHERE schemaname = $1`, [dbSchema]);
3507
- return groupByTable(columns2, constraints, indexes);
3858
+ return groupByTable(columns, constraints, indexes);
3508
3859
  }
3509
3860
  async function runPull(config2) {
3510
3861
  console.log("@bungres/kit pull: introspecting database...");
@@ -3527,9 +3878,9 @@ async function runPull(config2) {
3527
3878
  await sql2.end();
3528
3879
  }
3529
3880
  }
3530
- function groupByTable(columns2, constraints, indexes) {
3881
+ function groupByTable(columns, constraints, indexes) {
3531
3882
  const map = new Map;
3532
- for (const col2 of columns2) {
3883
+ for (const col2 of columns) {
3533
3884
  if (!map.has(col2.table_name)) {
3534
3885
  map.set(col2.table_name, {
3535
3886
  tableName: col2.table_name,
@@ -3565,19 +3916,18 @@ function generateSchemaTS(tableMap, dbSchema) {
3565
3916
  `// Generated at: ${new Date().toISOString()}`,
3566
3917
  ``,
3567
3918
  `import {`,
3568
- ` table,`,
3919
+ ` pgTable,`,
3569
3920
  ` text, varchar, char, integer, bigint, smallint,`,
3570
3921
  ` serial, bigserial, boolean, real, doublePrecision,`,
3571
3922
  ` numeric, decimal, json, jsonb,`,
3572
3923
  ` timestamp, timestamptz, date, time, uuid,`,
3573
3924
  ` bytea, inet,`,
3574
- ` textArray, integerArray, varcharArray, uuidArray,`,
3575
3925
  `} from "@bungres/orm";`,
3576
3926
  ``
3577
3927
  ];
3578
3928
  for (const [, table2] of tableMap) {
3579
3929
  const varName = toCamelCase(table2.tableName);
3580
- lines.push(`export const ${varName} = table("${table2.tableName}", {`);
3930
+ lines.push(`export const ${varName} = pgTable("${table2.tableName}", {`);
3581
3931
  for (const col2 of table2.columns) {
3582
3932
  const colExpr = buildColumnExpression(col2);
3583
3933
  lines.push(` ${toCamelCase(col2.name)}: ${colExpr},`);
@@ -3658,11 +4008,13 @@ function buildColumnExpression(col2) {
3658
4008
  refOpts += `, onUpdate: "${updateRule}"`;
3659
4009
  opts.push(`references: { ${refOpts} }`);
3660
4010
  }
3661
- let builderName = pgTypeToBungresBuilderName(col2);
3662
- if (opts.length > 0) {
3663
- return `${builderName}({ ${opts.join(", ")} })`;
3664
- }
3665
- return `${builderName}()`;
4011
+ let builderResult = pgTypeToBungresBuilderName(col2);
4012
+ let builderName = typeof builderResult === "string" ? builderResult : builderResult.base;
4013
+ let isArray = typeof builderResult !== "string" && builderResult.isArray;
4014
+ let expr = opts.length > 0 ? `${builderName}({ ${opts.join(", ")} })` : `${builderName}()`;
4015
+ if (isArray)
4016
+ expr += `.array()`;
4017
+ return expr;
3666
4018
  }
3667
4019
  function pgTypeToBungresBuilderName(col2) {
3668
4020
  const dt = col2.dataType;
@@ -3708,14 +4060,14 @@ function pgTypeToBungresBuilderName(col2) {
3708
4060
  return "text";
3709
4061
  if (dt === "ARRAY") {
3710
4062
  if (col2.udtName === "_text")
3711
- return "textArray";
4063
+ return { base: "text", isArray: true };
3712
4064
  if (col2.udtName === "_int4" || col2.udtName === "_int8")
3713
- return "integerArray";
4065
+ return { base: "integer", isArray: true };
3714
4066
  if (col2.udtName === "_varchar")
3715
- return "varcharArray";
4067
+ return { base: "varchar", isArray: true };
3716
4068
  if (col2.udtName === "_uuid")
3717
- return "uuidArray";
3718
- return "textArray";
4069
+ return { base: "uuid", isArray: true };
4070
+ return { base: "text", isArray: true };
3719
4071
  }
3720
4072
  return "text";
3721
4073
  }
@@ -3750,11 +4102,29 @@ async function runPush(config2, opts = {}) {
3750
4102
  );
3751
4103
  `);
3752
4104
  const rows = await sql2.unsafe(`SELECT snapshot FROM ${qualifiedPush} ORDER BY id DESC LIMIT 1;`);
3753
- let prevSnapshot = {};
4105
+ let prevSnapshot = { tables: {}, enums: {}, views: {} };
3754
4106
  if (rows.length > 0) {
3755
- prevSnapshot = typeof rows[0].snapshot === "string" ? JSON.parse(rows[0].snapshot) : rows[0].snapshot;
4107
+ const parsed = typeof rows[0].snapshot === "string" ? JSON.parse(rows[0].snapshot) : rows[0].snapshot;
4108
+ if (parsed.tables || parsed.enums || parsed.views) {
4109
+ prevSnapshot = {
4110
+ tables: parsed.tables || {},
4111
+ enums: parsed.enums || {},
4112
+ views: parsed.views || {}
4113
+ };
4114
+ } else {
4115
+ prevSnapshot = { tables: parsed, enums: {}, views: {} };
4116
+ }
4117
+ }
4118
+ const currentSnapshot = { tables: {}, enums: {}, views: {} };
4119
+ for (const s2 of schemas2) {
4120
+ if (s2.type === "table") {
4121
+ currentSnapshot.tables[s2.config.name] = s2.config;
4122
+ } else if (s2.type === "enum") {
4123
+ currentSnapshot.enums[s2.enumName] = { enumName: s2.enumName, enumValues: s2.enumValues };
4124
+ } else if (s2.type === "view") {
4125
+ currentSnapshot.views[s2.config.name] = s2.config;
4126
+ }
3756
4127
  }
3757
- const currentSnapshot = Object.fromEntries(schemas2.map((schemaEntry) => [schemaEntry.config.name, schemaEntry.config]));
3758
4128
  const diff = diffSchemas(prevSnapshot, currentSnapshot);
3759
4129
  if (diff.statements.length === 0) {
3760
4130
  ms.stop("Up to date.");
@@ -3811,7 +4181,7 @@ async function runPush(config2, opts = {}) {
3811
4181
  // src/commands/refresh.ts
3812
4182
  var import_picocolors6 = __toESM(require_picocolors(), 1);
3813
4183
  async function runRefresh(config2) {
3814
- const schemas2 = await loadSchemas(config2.schema);
4184
+ const schemas2 = (await loadSchemas(config2.schema)).filter((s) => s.type === "table");
3815
4185
  if (schemas2.length === 0) {
3816
4186
  console.warn("No table definitions found in schema files.");
3817
4187
  return;
@@ -3945,7 +4315,7 @@ async function runSeed(config2) {
3945
4315
  }
3946
4316
  const s = spinner();
3947
4317
  s.start("Loading schemas for auto-seeding...");
3948
- const schemas2 = await loadSchemas(config2.schema);
4318
+ const schemas2 = (await loadSchemas(config2.schema)).filter((s2) => s2.type === "table");
3949
4319
  if (schemas2.length === 0) {
3950
4320
  s.stop("No schemas.");
3951
4321
  outro("Nothing to seed.");
@@ -4077,7 +4447,8 @@ async function runStatus(config2) {
4077
4447
  var import_picocolors10 = __toESM(require_picocolors(), 1);
4078
4448
 
4079
4449
  // src/commands/template.ts
4080
- var indexHtml = `<!DOCTYPE html>
4450
+ function renderIndexHtml(opts) {
4451
+ return `<!DOCTYPE html>
4081
4452
  <html lang="en">
4082
4453
  <head>
4083
4454
  <meta charset="UTF-8">
@@ -4086,8 +4457,7 @@ var indexHtml = `<!DOCTYPE html>
4086
4457
 
4087
4458
  <link rel="preconnect" href="https://fonts.googleapis.com">
4088
4459
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
4089
- <link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&display=swap" rel="stylesheet">
4090
- <link href="https://fonts.googleapis.com/css2?family=Fira+Code:wght@400;500&display=swap" rel="stylesheet">
4460
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
4091
4461
 
4092
4462
  <script src="https://unpkg.com/htmx.org@2.0.0" defer></script>
4093
4463
  <script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
@@ -4099,38 +4469,17 @@ var indexHtml = `<!DOCTYPE html>
4099
4469
  theme: {
4100
4470
  extend: {
4101
4471
  fontFamily: {
4102
- sans: ['Outfit', 'sans-serif'],
4103
- mono: ['"Fira Code"', 'monospace'],
4472
+ sans: ['Inter', 'sans-serif'],
4473
+ mono: ['"JetBrains Mono"', 'monospace'],
4104
4474
  },
4105
4475
  colors: {
4106
- background: 'var(--background)',
4107
- foreground: 'var(--foreground)',
4108
- card: 'var(--card)',
4109
- 'card-foreground': 'var(--card-foreground)',
4110
- popover: 'var(--popover)',
4111
- 'popover-foreground': 'var(--popover-foreground)',
4112
- primary: 'var(--primary)',
4113
- 'primary-foreground': 'var(--primary-foreground)',
4114
- secondary: 'var(--secondary)',
4115
- 'secondary-foreground': 'var(--secondary-foreground)',
4116
- muted: 'var(--muted)',
4117
- 'muted-foreground': 'var(--muted-foreground)',
4118
- accent: 'var(--accent)',
4119
- 'accent-foreground': 'var(--accent-foreground)',
4120
- destructive: 'var(--destructive)',
4121
- border: 'var(--border)',
4122
- input: 'var(--input)',
4123
- ring: 'var(--ring)',
4124
- sidebar: {
4125
- DEFAULT: 'var(--sidebar)',
4126
- foreground: 'var(--sidebar-foreground)',
4127
- primary: 'var(--sidebar-primary)',
4128
- 'primary-foreground': 'var(--sidebar-primary-foreground)',
4129
- accent: 'var(--sidebar-accent)',
4130
- 'accent-foreground': 'var(--sidebar-accent-foreground)',
4131
- border: 'var(--sidebar-border)',
4132
- ring: 'var(--sidebar-ring)',
4133
- }
4476
+ bg: '#161618',
4477
+ panel: '#1c1c1f',
4478
+ border: '#2c2c2f',
4479
+ text: '#ededed',
4480
+ muted: '#8f8f91',
4481
+ accent: '#00e599',
4482
+ hover: '#28282c'
4134
4483
  }
4135
4484
  }
4136
4485
  }
@@ -4138,265 +4487,319 @@ var indexHtml = `<!DOCTYPE html>
4138
4487
  </script>
4139
4488
 
4140
4489
  <style>
4141
- :root {
4142
- --background: oklch(1 0 0);
4143
- --foreground: oklch(0.148 0.004 228.8);
4144
- --card: oklch(1 0 0);
4145
- --card-foreground: oklch(0.148 0.004 228.8);
4146
- --popover: oklch(1 0 0);
4147
- --popover-foreground: oklch(0.148 0.004 228.8);
4148
- --primary: oklch(0.508 0.118 165.612);
4149
- --primary-foreground: oklch(0.979 0.021 166.113);
4150
- --secondary: oklch(0.967 0.001 286.375);
4151
- --secondary-foreground: oklch(0.21 0.006 285.885);
4152
- --muted: oklch(0.963 0.002 197.1);
4153
- --muted-foreground: oklch(0.56 0.021 213.5);
4154
- --accent: oklch(0.963 0.002 197.1);
4155
- --accent-foreground: oklch(0.218 0.008 223.9);
4156
- --destructive: oklch(0.577 0.245 27.325);
4157
- --border: oklch(0.925 0.005 214.3);
4158
- --input: oklch(0.925 0.005 214.3);
4159
- --ring: oklch(0.723 0.014 214.4);
4160
- --chart-1: oklch(0.845 0.143 164.978);
4161
- --chart-2: oklch(0.696 0.17 162.48);
4162
- --chart-3: oklch(0.596 0.145 163.225);
4163
- --chart-4: oklch(0.508 0.118 165.612);
4164
- --chart-5: oklch(0.432 0.095 166.913);
4165
- --radius: 0.625rem;
4166
- --sidebar: oklch(0.987 0.002 197.1);
4167
- --sidebar-foreground: oklch(0.148 0.004 228.8);
4168
- --sidebar-primary: oklch(0.596 0.145 163.225);
4169
- --sidebar-primary-foreground: oklch(0.979 0.021 166.113);
4170
- --sidebar-accent: oklch(0.963 0.002 197.1);
4171
- --sidebar-accent-foreground: oklch(0.218 0.008 223.9);
4172
- --sidebar-border: oklch(0.925 0.005 214.3);
4173
- --sidebar-ring: oklch(0.723 0.014 214.4);
4174
- }
4175
-
4176
- .dark {
4177
- --background: oklch(0.148 0.004 228.8);
4178
- --foreground: oklch(0.987 0.002 197.1);
4179
- --card: oklch(0.218 0.008 223.9);
4180
- --card-foreground: oklch(0.987 0.002 197.1);
4181
- --popover: oklch(0.218 0.008 223.9);
4182
- --popover-foreground: oklch(0.987 0.002 197.1);
4183
- --primary: oklch(0.432 0.095 166.913);
4184
- --primary-foreground: oklch(0.979 0.021 166.113);
4185
- --secondary: oklch(0.274 0.006 286.033);
4186
- --secondary-foreground: oklch(0.985 0 0);
4187
- --muted: oklch(0.275 0.011 216.9);
4188
- --muted-foreground: oklch(0.723 0.014 214.4);
4189
- --accent: oklch(0.275 0.011 216.9);
4190
- --accent-foreground: oklch(0.987 0.002 197.1);
4191
- --destructive: oklch(0.704 0.191 22.216);
4192
- --border: oklch(1 0 0 / 10%);
4193
- --input: oklch(1 0 0 / 15%);
4194
- --ring: oklch(0.56 0.021 213.5);
4195
- --chart-1: oklch(0.845 0.143 164.978);
4196
- --chart-2: oklch(0.696 0.17 162.48);
4197
- --chart-3: oklch(0.596 0.145 163.225);
4198
- --chart-4: oklch(0.508 0.118 165.612);
4199
- --chart-5: oklch(0.432 0.095 166.913);
4200
- --sidebar: oklch(0.218 0.008 223.9);
4201
- --sidebar-foreground: oklch(0.987 0.002 197.1);
4202
- --sidebar-primary: oklch(0.696 0.17 162.48);
4203
- --sidebar-primary-foreground: oklch(0.262 0.051 172.552);
4204
- --sidebar-accent: oklch(0.275 0.011 216.9);
4205
- --sidebar-accent-foreground: oklch(0.987 0.002 197.1);
4206
- --sidebar-border: oklch(1 0 0 / 10%);
4207
- --sidebar-ring: oklch(0.56 0.021 213.5);
4208
- }
4209
-
4210
- ::-webkit-scrollbar { width: 6px; height: 6px; }
4490
+ body { background-color: #161618; color: #ededed; }
4491
+ ::-webkit-scrollbar { width: 8px; height: 8px; }
4211
4492
  ::-webkit-scrollbar-track { background: transparent; }
4212
- ::-webkit-scrollbar-thumb { background: var(--border); border-radius: 4px; }
4213
- ::-webkit-scrollbar-thumb:hover { background: var(--muted-foreground); }
4493
+ ::-webkit-scrollbar-thumb { background: #3a3a3e; border-radius: 4px; }
4494
+ ::-webkit-scrollbar-thumb:hover { background: #4a4a4e; }
4214
4495
  ::-webkit-scrollbar-corner { background: transparent; }
4215
4496
  [x-cloak] { display: none !important; }
4216
- .htmx-indicator { opacity: 0; transition: opacity 200ms ease-in; }
4217
- .htmx-request .htmx-indicator { opacity: 1; }
4218
- .htmx-request.htmx-indicator { opacity: 1; }
4497
+
4498
+ .tab-active { background-color: #1c1c1f; border-top: 2px solid #00e599; color: #ededed; }
4499
+ .tab-inactive { background-color: #161618; border-top: 2px solid transparent; color: #8f8f91; }
4500
+
4501
+ table { border-spacing: 0; }
4502
+ th { border-bottom: 1px solid #2c2c2f; border-right: 1px solid #2c2c2f; background: #1c1c1f; font-weight: 500; font-size: 13px; color: #8f8f91; }
4503
+ td { border-bottom: 1px solid #2c2c2f; border-right: 1px solid #2c2c2f; font-size: 13px; }
4504
+ tr:hover td { background-color: #28282c; }
4219
4505
  </style>
4220
4506
  </head>
4221
- <body class="dark bg-background text-foreground font-sans h-screen flex overflow-hidden selection:bg-primary/30">
4222
- <div x-data="{ currentTable: null, pageSize: 25 }" class="flex h-screen w-full">
4223
- <aside class="w-64 bg-sidebar border-r border-sidebar-border flex flex-col z-20 shrink-0 text-sidebar-foreground">
4224
- <div class="p-4 border-b border-sidebar-border flex items-center gap-3 bg-sidebar">
4225
- <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-primary">
4226
- <ellipse cx="12" cy="5" rx="9" ry="3"></ellipse>
4227
- <path d="M21 12c0 1.66-4 3-9 3s-9-1.34-9-3"></path>
4228
- <path d="M3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5"></path>
4229
- </svg>
4230
- <h1 class="text-sm font-semibold tracking-tight m-0">Bungres Studio</h1>
4507
+ <body class="font-sans h-screen flex overflow-hidden text-sm selection:bg-accent/30">
4508
+ <div x-data="studioState()" class="flex h-screen w-full relative">
4509
+
4510
+ <!-- JSON Modal -->
4511
+ <div x-show="jsonModalOpen" x-cloak class="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm transition-opacity" style="display: none;">
4512
+ <div @click.away="jsonModalOpen = false" class="bg-panel border border-border rounded-lg shadow-xl w-full max-w-2xl flex flex-col max-h-[80vh]">
4513
+ <div class="flex items-center justify-between p-3 border-b border-border bg-bg/50">
4514
+ <h3 class="font-medium text-text text-xs flex items-center gap-2">
4515
+ <svg class="w-4 h-4 text-accent" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="16 18 22 12 16 6"></polyline><polyline points="8 6 2 12 8 18"></polyline></svg>
4516
+ Data Viewer
4517
+ </h3>
4518
+ <button @click="jsonModalOpen = false" class="text-muted hover:text-text p-1 rounded hover:bg-hover transition-colors"><svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg></button>
4519
+ </div>
4520
+ <div class="p-4 overflow-auto flex-1">
4521
+ <pre class="font-mono text-xs text-text bg-bg p-4 rounded border border-border whitespace-pre-wrap break-all" x-text="jsonModalData"></pre>
4522
+ </div>
4523
+ </div>
4524
+ </div>
4525
+
4526
+ <!-- Sidebar -->
4527
+ <aside class="w-64 bg-bg border-r border-border flex flex-col z-20 shrink-0">
4528
+
4529
+ <!-- Schema Selector -->
4530
+ <div class="p-3 border-b border-border">
4531
+ <select x-model="currentSchema" @change="loadSidebar()" class="w-full bg-panel border border-border text-text text-sm rounded px-2 py-1.5 focus:outline-none focus:border-muted cursor-pointer font-medium">
4532
+ <template x-for="s in schemas" :key="s">
4533
+ <option :value="s" x-text="s"></option>
4534
+ </template>
4535
+ </select>
4536
+ </div>
4537
+
4538
+ <!-- Search Box -->
4539
+ <div class="p-3 border-b border-border bg-bg">
4540
+ <div class="relative">
4541
+ <svg class="w-4 h-4 text-muted absolute left-2.5 top-2" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>
4542
+ <input type="text" x-model="searchQuery" placeholder="Filter tables.." class="w-full bg-panel border border-border rounded pl-8 pr-2 py-1.5 text-sm focus:outline-none focus:border-muted text-text placeholder-muted">
4543
+ </div>
4231
4544
  </div>
4232
- <div class="flex-1 overflow-y-auto p-2" hx-get="/htmx/sidebar" hx-trigger="load">
4233
- <div class="p-4 text-xs text-muted-foreground animate-pulse text-center">Loading schemas...</div>
4545
+
4546
+ <!-- Tables List -->
4547
+ <div class="flex-1 overflow-y-auto pt-2" id="sidebar-content" :hx-get="'/htmx/sidebar?schema=' + currentSchema" hx-trigger="load">
4548
+ <div class="p-4 text-xs text-muted animate-pulse text-center">Loading...</div>
4234
4549
  </div>
4235
4550
  </aside>
4236
- <main class="flex-1 flex flex-col relative w-[calc(100vw-16rem)]">
4237
- <header class="h-14 px-4 border-b border-border flex items-center justify-between bg-card z-10 shrink-0 text-card-foreground">
4238
- <h2 class="text-sm font-semibold tracking-tight flex items-center gap-2">
4239
- <template x-if="currentTable">
4240
- <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="text-primary">
4241
- <rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
4242
- <line x1="3" y1="9" x2="21" y2="9"></line>
4243
- <line x1="9" y1="21" x2="9" y2="9"></line>
4244
- </svg>
4245
- </template>
4246
- <template x-if="!currentTable">
4247
- <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="text-muted-foreground">
4248
- <rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
4249
- <line x1="3" y1="9" x2="21" y2="9"></line>
4250
- <line x1="9" y1="21" x2="9" y2="9"></line>
4251
- </svg>
4252
- </template>
4253
- <span x-text="currentTable ? currentTable : 'Select a table'"></span>
4254
- </h2>
4255
- <button x-bind:disabled="!currentTable" class="bg-card border border-border text-card-foreground px-3 py-1.5 rounded-md text-xs font-medium cursor-pointer transition-all hover:bg-accent hover:text-accent-foreground active:scale-95 disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-1.5 group" x-on:click="$dispatch('refresh-table')">
4256
- <svg class="w-4 h-4 group-hover:rotate-180 transition-transform duration-500" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
4257
- <polyline points="23 4 23 10 17 10"></polyline>
4258
- <polyline points="1 20 1 14 7 14"></polyline>
4259
- <path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"></path>
4260
- </svg>
4261
- Refresh
4551
+
4552
+ <!-- Main Content -->
4553
+ <main class="flex-1 flex flex-col min-w-0 bg-panel">
4554
+ <!-- Tabs Header -->
4555
+ <header class="h-10 border-b border-border flex items-center bg-bg shrink-0 overflow-x-auto overflow-y-hidden">
4556
+ <template x-for="tab in tabs" :key="tab.id">
4557
+ <div
4558
+ @click="activeTab = tab.id"
4559
+ :class="activeTab === tab.id ? 'tab-active' : 'tab-inactive hover:bg-panel hover:text-text'"
4560
+ class="h-full flex items-center gap-2 pl-4 pr-2 cursor-pointer border-r border-border min-w-max transition-colors group relative select-none"
4561
+ >
4562
+ <!-- Icon -->
4563
+ <template x-if="tab.type === 'table'">
4564
+ <svg class="w-4 h-4 text-muted" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect><line x1="3" y1="9" x2="21" y2="9"></line><line x1="9" y1="21" x2="9" y2="9"></line></svg>
4565
+ </template>
4566
+ <template x-if="tab.type === 'query'">
4567
+ <svg class="w-4 h-4 text-accent" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="16 18 22 12 16 6"></polyline><polyline points="8 6 2 12 8 18"></polyline></svg>
4568
+ </template>
4569
+
4570
+ <span x-text="tab.title" class="font-medium text-xs mr-2"></span>
4571
+
4572
+ <!-- Close Button -->
4573
+ <button @click.stop="closeTab(tab.id)" class="opacity-0 group-hover:opacity-100 p-0.5 rounded hover:bg-hover text-muted hover:text-text transition-opacity">
4574
+ <svg class="w-3 h-3" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
4575
+ </button>
4576
+ </div>
4577
+ </template>
4578
+ <button @click="openQueryEditor()" class="h-full px-3 text-muted hover:text-text hover:bg-panel flex items-center justify-center transition-colors">
4579
+ <svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line></svg>
4262
4580
  </button>
4263
4581
  </header>
4264
- <div class="flex-1 overflow-auto relative z-[1] text-left bg-background">
4265
- <div id="data-container" class="h-full w-full flex flex-col" x-on:refresh-table.window="if(currentTable) htmx.ajax('GET', '/htmx/tables/' + currentTable + '?limit=' + pageSize, {target: '#data-container'})">
4266
- <div class="flex flex-col items-center justify-center h-full w-full text-muted-foreground animate-[fadeIn_0.5s_ease-out]">
4267
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="w-16 h-16 mb-4 text-muted">
4268
- <path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z"></path>
4269
- <polyline points="14 2 14 8 20 8"></polyline>
4270
- </svg>
4271
- <h3 class="text-foreground m-0 mb-2 text-lg">No Table Selected</h3>
4272
- <p class="text-sm">Choose a table from the sidebar to view its data</p>
4582
+
4583
+ <!-- Active Tab Content Container -->
4584
+ <div class="flex-1 relative overflow-hidden bg-panel">
4585
+
4586
+ <!-- Empty State -->
4587
+ <div x-show="tabs.length === 0" class="absolute inset-0 flex flex-col items-center justify-center text-muted">
4588
+ <div class="w-12 h-12 rounded-lg bg-hover border border-border flex items-center justify-center mb-4">
4589
+ <svg class="w-6 h-6 text-text" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect><line x1="3" y1="9" x2="21" y2="9"></line><line x1="9" y1="21" x2="9" y2="9"></line></svg>
4273
4590
  </div>
4591
+ <p class="text-sm">Select a table to view</p>
4274
4592
  </div>
4593
+
4594
+ <!-- Render Tabs -->
4595
+ <template x-for="tab in tabs" :key="tab.id">
4596
+ <div x-show="activeTab === tab.id" class="absolute inset-0 flex flex-col h-full w-full">
4597
+
4598
+ <!-- Table View -->
4599
+ <template x-if="tab.type === 'table'">
4600
+ <div class="flex flex-col h-full w-full">
4601
+ <!-- Table Header Toolbars -->
4602
+ <div class="h-12 border-b border-border flex items-center justify-between px-3 shrink-0 bg-panel">
4603
+ <div class="flex items-center gap-2">
4604
+ <span class="text-xs text-muted font-medium ml-1 flex items-center gap-2">
4605
+ <svg class="w-3.5 h-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect><line x1="3" y1="9" x2="21" y2="9"></line><line x1="9" y1="21" x2="9" y2="9"></line></svg>
4606
+ <span x-text="tab.title"></span>
4607
+ </span>
4608
+ </div>
4609
+ <!-- HTMX will swap the pagination controls into this div -->
4610
+ <div :id="'pagination-' + tab.id" class="flex items-center gap-2"></div>
4611
+ </div>
4612
+
4613
+ <!-- Table Data Grid Container -->
4614
+ <div class="flex-1 overflow-auto bg-bg" :id="'data-' + tab.id"
4615
+ x-init="htmx.ajax('GET', '/htmx/tables/' + tab.name + '?schema=' + tab.schema + '&tabId=' + tab.id, {target: '#data-' + tab.id})">
4616
+ <div class="p-8 text-center text-muted">Loading data...</div>
4617
+ </div>
4618
+ </div>
4619
+ </template>
4620
+
4621
+ <!-- Query Editor View -->
4622
+ <template x-if="tab.type === 'query'">
4623
+ <div class="flex flex-col h-full w-full" x-data="{ query: 'SELECT 1;' }">
4624
+ <div class="h-12 border-b border-border flex items-center px-3 shrink-0 gap-2 bg-panel">
4625
+ <button
4626
+ @click="htmx.ajax('POST', '/htmx/query', { target: '#query-result-' + tab.id, values: { query: query } })"
4627
+ class="bg-accent text-bg px-3 py-1.5 rounded text-xs font-semibold hover:opacity-90 flex items-center gap-1.5 transition-opacity"
4628
+ >
4629
+ <svg class="w-3 h-3" viewBox="0 0 24 24" fill="currentColor"><polygon points="5 3 19 12 5 21 5 3"></polygon></svg> Run
4630
+ </button>
4631
+ <span class="text-muted text-[10px] font-mono ml-2">Press Ctrl+Enter to run</span>
4632
+ </div>
4633
+ <div class="flex-none h-48 border-b border-border relative">
4634
+ <textarea
4635
+ x-model="query"
4636
+ @keydown.ctrl.enter.prevent="htmx.ajax('POST', '/htmx/query', { target: '#query-result-' + tab.id, values: { query: query } })"
4637
+ class="absolute inset-0 w-full h-full bg-bg text-text font-mono text-sm p-4 focus:outline-none resize-none"
4638
+ placeholder="SELECT 1;"
4639
+ ></textarea>
4640
+ </div>
4641
+ <div class="flex-1 overflow-auto bg-bg flex flex-col" :id="'query-result-' + tab.id">
4642
+ <div class="h-full flex items-center justify-center text-muted">
4643
+ <div class="flex flex-col items-center">
4644
+ <svg class="w-6 h-6 mb-2 opacity-50" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="5 3 19 12 5 21 5 3"></polygon></svg>
4645
+ <span class="text-xs">Run a query to see results</span>
4646
+ </div>
4647
+ </div>
4648
+ </div>
4649
+ </div>
4650
+ </template>
4651
+
4652
+ </div>
4653
+ </template>
4275
4654
  </div>
4276
4655
  </main>
4277
4656
  </div>
4278
4657
  <script>
4279
- document.addEventListener('alpine:init', () => {})
4658
+ const INITIAL_SCHEMAS = ${JSON.stringify(opts.schemas)};
4659
+ const INITIAL_CURRENT_SCHEMA = ${JSON.stringify(opts.currentSchema)};
4660
+
4661
+ function studioState() {
4662
+ return {
4663
+ schemas: INITIAL_SCHEMAS,
4664
+ currentSchema: INITIAL_CURRENT_SCHEMA,
4665
+ searchQuery: '',
4666
+ tabs: [],
4667
+ activeTab: null,
4668
+ queryEditorCount: 0,
4669
+ jsonModalOpen: false,
4670
+ jsonModalData: '',
4671
+
4672
+ init() {
4673
+ this.$watch('activeTab', (value) => {
4674
+ const tab = this.tabs.find(t => t.id === value);
4675
+ if (tab && tab.schema && tab.schema !== this.currentSchema) {
4676
+ this.currentSchema = tab.schema;
4677
+ this.loadSidebar();
4678
+ }
4679
+ });
4680
+
4681
+ // Listen for table clicks from the sidebar
4682
+ window.addEventListener('open-table', (e) => {
4683
+ const tableName = e.detail.tableName;
4684
+ const schema = this.currentSchema;
4685
+ const tabId = 'table_' + schema + '_' + tableName;
4686
+
4687
+ if (!this.tabs.find(t => t.id === tabId)) {
4688
+ this.tabs.push({ id: tabId, type: 'table', title: tableName, name: tableName, schema: schema });
4689
+ }
4690
+ this.activeTab = tabId;
4691
+ });
4692
+
4693
+ window.addEventListener('open-json-modal', (e) => {
4694
+ try {
4695
+ const str = atob(e.detail);
4696
+ this.jsonModalData = JSON.stringify(JSON.parse(str), null, 2);
4697
+ } catch(err) {
4698
+ this.jsonModalData = 'Invalid JSON data';
4699
+ }
4700
+ this.jsonModalOpen = true;
4701
+ });
4702
+ },
4703
+
4704
+ loadSidebar() {
4705
+ htmx.ajax('GET', '/htmx/sidebar?schema=' + this.currentSchema, {target: '#sidebar-content'});
4706
+ },
4707
+
4708
+ openQueryEditor() {
4709
+ this.queryEditorCount++;
4710
+ const tabId = 'query_' + this.queryEditorCount;
4711
+ this.tabs.push({ id: tabId, type: 'query', title: 'Query Editor ' + (this.queryEditorCount > 1 ? this.queryEditorCount : ''), schema: this.currentSchema });
4712
+ this.activeTab = tabId;
4713
+ },
4714
+
4715
+ closeTab(id) {
4716
+ const idx = this.tabs.findIndex(t => t.id === id);
4717
+ if (idx === -1) return;
4718
+
4719
+ this.tabs.splice(idx, 1);
4720
+ if (this.activeTab === id) {
4721
+ if (this.tabs.length > 0) {
4722
+ this.activeTab = this.tabs[Math.max(0, idx - 1)].id;
4723
+ } else {
4724
+ this.activeTab = null;
4725
+ }
4726
+ }
4727
+ }
4728
+ }
4729
+ }
4280
4730
  </script>
4281
4731
  </body>
4282
4732
  </html>`;
4733
+ }
4283
4734
 
4284
4735
  // src/commands/studio.ts
4285
4736
  async function runStudio(config2) {
4286
- const schemas2 = await loadSchemas(config2.schema);
4737
+ const schemas2 = (await loadSchemas(config2.schema)).filter((s) => s.type === "table");
4287
4738
  if (schemas2.length === 0) {
4288
- console.warn("No table definitions found in schema files.");
4289
- return;
4739
+ console.warn("No table definitions found in schema files. Connecting anyway to browse DB...");
4290
4740
  }
4291
4741
  const schemaObj2 = {};
4292
4742
  for (const s of schemas2) {
4293
4743
  schemaObj2[s.exportName] = s.table;
4294
4744
  }
4295
4745
  const db2 = bungres({ url: config2.dbUrl, schema: schemaObj2 });
4746
+ let allSchemas = ["public"];
4747
+ try {
4748
+ const res = await db2.execute(rawSql(`SELECT schema_name FROM information_schema.schemata WHERE schema_name NOT LIKE 'pg_%' AND schema_name != 'information_schema'`));
4749
+ if (Array.isArray(res) && res.length > 0) {
4750
+ allSchemas = res.map((r2) => r2.schema_name);
4751
+ }
4752
+ } catch (e) {}
4296
4753
  const port = Bun.env.PORT ? parseInt(Bun.env.PORT, 10) : 5555;
4297
4754
  const server = Bun.serve({
4298
4755
  port,
4299
4756
  async fetch(req) {
4300
4757
  const url = new URL(req.url);
4301
- if (req.method === "GET" && url.pathname === "/api/tables") {
4302
- const tables = schemas2.map((s) => {
4303
- const fkColumns = new Set;
4304
- if (s.config.foreignKeys) {
4305
- s.config.foreignKeys.forEach((fk) => {
4306
- fk.columns.forEach((col2) => fkColumns.add(col2));
4307
- });
4308
- }
4309
- const columns2 = Object.entries(s.config.columns).map(([key, col2]) => {
4310
- const fk = col2.references ? {
4311
- table: col2.references.table,
4312
- column: col2.references.column
4313
- } : fkColumns.has(col2.name) ? { isForeignKey: true } : null;
4314
- return {
4315
- name: col2.name,
4316
- type: col2.dataType,
4317
- primaryKey: col2.primaryKey,
4318
- foreignKey: fk
4319
- };
4320
- });
4321
- return {
4322
- name: s.config.name,
4323
- exportName: s.exportName,
4324
- columns: columns2,
4325
- foreignKeys: s.config.foreignKeys || []
4326
- };
4327
- });
4328
- return new Response(JSON.stringify(tables), {
4329
- headers: { "Content-Type": "application/json" }
4330
- });
4331
- }
4332
- if (req.method === "GET" && url.pathname.startsWith("/api/tables/") && url.pathname.endsWith("/data")) {
4333
- const tableName = url.pathname.split("/")[3];
4334
- const schema = schemas2.find((s) => s.config.name === tableName);
4335
- if (!schema) {
4336
- return new Response("Table not found", { status: 404 });
4337
- }
4338
- try {
4339
- const page = parseInt(url.searchParams.get("page") || "1", 10);
4340
- const limit = parseInt(url.searchParams.get("limit") || "50", 10);
4341
- const offset = (page - 1) * limit;
4342
- const countResult = await db2.select({ count: schema.table }).from(schema.table);
4343
- const total = Array.isArray(countResult) ? countResult.length : 0;
4344
- const data = await db2.select().from(schema.table).limit(limit).offset(offset);
4345
- return new Response(JSON.stringify({
4346
- data,
4347
- total,
4348
- page,
4349
- limit,
4350
- totalPages: Math.ceil(total / limit)
4351
- }), {
4352
- headers: { "Content-Type": "application/json" }
4353
- });
4354
- } catch (e) {
4355
- return new Response(JSON.stringify({ error: e.message }), {
4356
- status: 500,
4357
- headers: { "Content-Type": "application/json" }
4358
- });
4359
- }
4360
- }
4361
- if (req.method === "POST" && url.pathname === "/api/editor/open") {
4758
+ if (req.method === "GET" && url.pathname === "/htmx/sidebar") {
4759
+ const currentSchema = url.searchParams.get("schema") || config2.dbSchema || "public";
4760
+ let items = [];
4362
4761
  try {
4363
- const body = await req.json();
4364
- const tableName = body.tableName;
4365
- const schema = schemas2.find((s) => s.config.name === tableName);
4366
- if (schema && schema.filePath) {
4367
- console.log(`Attempting to open ${schema.filePath} in editor...`);
4368
- Bun.openInEditor(schema.filePath, { editor: "vscode" });
4369
- return new Response(JSON.stringify({
4370
- success: true,
4371
- filePath: schema.filePath
4372
- }), {
4373
- headers: { "Content-Type": "application/json" }
4374
- });
4762
+ const query = `
4763
+ SELECT c.relname as name, c.reltuples as count, c.relkind as type
4764
+ FROM pg_class c
4765
+ JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
4766
+ WHERE n.nspname = '${currentSchema}'
4767
+ AND c.relkind IN ('r', 'v', 'm')
4768
+ ORDER BY c.relname
4769
+ `;
4770
+ const res = await db2.execute(rawSql(query));
4771
+ if (Array.isArray(res)) {
4772
+ items = res;
4375
4773
  }
4376
- return new Response("Schema not found", { status: 404 });
4377
4774
  } catch (e) {
4378
- return new Response("Invalid request", { status: 400 });
4775
+ items = schemas2.map((s) => ({ name: s.config.name, count: 0, type: "r" }));
4379
4776
  }
4380
- }
4381
- if (req.method === "GET" && url.pathname === "/htmx/sidebar") {
4382
- let html = '<ul class="flex flex-col gap-0.5 p-2 m-0 list-none">';
4383
- schemas2.forEach((s) => {
4384
- const tableName = s.config.name;
4777
+ const formatCount = (n2) => {
4778
+ if (n2 >= 1e6)
4779
+ return (n2 / 1e6).toFixed(1) + "M";
4780
+ if (n2 >= 1000)
4781
+ return (n2 / 1000).toFixed(1) + "k";
4782
+ return Math.floor(n2).toString();
4783
+ };
4784
+ let html = '<div class="px-3 py-2 text-[10px] font-semibold text-muted tracking-wider flex justify-between items-center"><div class="flex items-center gap-1.5"><svg class="w-3 h-3" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"></polyline></svg> TABLES</div></div>';
4785
+ html += '<ul class="flex flex-col gap-0.5 p-2 m-0 list-none">';
4786
+ items.forEach((item) => {
4787
+ let icon = '<svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect><line x1="3" y1="9" x2="21" y2="9"></line><line x1="9" y1="21" x2="9" y2="9"></line></svg>';
4788
+ if (item.type === "v")
4789
+ icon = '<svg class="w-4 h-4 text-purple-400" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path><circle cx="12" cy="12" r="3"></circle></svg>';
4790
+ if (item.type === "m")
4791
+ icon = '<svg class="w-4 h-4 text-orange-400" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="12 2 2 7 12 12 22 7 12 2"></polygon><polyline points="2 17 12 22 22 17"></polyline><polyline points="2 12 12 17 22 12"></polyline></svg>';
4385
4792
  html += `
4386
- <li>
4793
+ <li x-show="searchQuery === '' || '${item.name}'.toLowerCase().includes(searchQuery.toLowerCase())">
4387
4794
  <button
4388
- hx-get="/htmx/tables/${tableName}?page=1&limit=25"
4389
- hx-target="#data-container"
4390
- @click="currentTable = '${tableName}'; pageSize = 25"
4391
- :class="currentTable === '${tableName}' ? 'bg-primary text-primary-foreground' : 'text-muted-foreground hover:bg-sidebar-accent hover:text-sidebar-accent-foreground'"
4392
- class="w-full text-left px-3 py-2 rounded-md text-sm font-medium flex items-center gap-2 transition-colors focus:outline-none"
4795
+ @click="$dispatch('open-table', { tableName: '${item.name}' })"
4796
+ class="w-full text-left px-2 py-1 rounded text-sm font-medium flex items-center justify-between transition-colors focus:outline-none hover:bg-hover hover:text-text group text-muted"
4393
4797
  >
4394
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
4395
- <rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
4396
- <line x1="3" y1="9" x2="21" y2="9"></line>
4397
- <line x1="9" y1="21" x2="9" y2="9"></line>
4398
- </svg>
4399
- ${tableName}
4798
+ <div class="flex items-center gap-2 truncate">
4799
+ ${icon}
4800
+ <span class="truncate">${item.name}</span>
4801
+ </div>
4802
+ <span class="text-[10px] font-mono text-muted/50 group-hover:text-muted">${formatCount(Math.max(0, item.count))}</span>
4400
4803
  </button>
4401
4804
  </li>
4402
4805
  `;
@@ -4406,140 +4809,215 @@ async function runStudio(config2) {
4406
4809
  }
4407
4810
  if (req.method === "GET" && url.pathname.startsWith("/htmx/tables/")) {
4408
4811
  const tableName = url.pathname.split("/")[3];
4409
- const schema = schemas2.find((s) => s.config.name === tableName);
4410
- if (!schema) {
4411
- return new Response(`<div class="p-4 text-red-500">Table not found</div>`, { status: 404, headers: { "Content-Type": "text/html" } });
4412
- }
4812
+ const reqSchema = url.searchParams.get("schema") || config2.dbSchema || "public";
4813
+ const tabId = url.searchParams.get("tabId") || `table_${tableName}`;
4413
4814
  try {
4414
4815
  const page = parseInt(url.searchParams.get("page") || "1", 10);
4415
4816
  const limit = parseInt(url.searchParams.get("limit") || "25", 10);
4416
4817
  const offset = (page - 1) * limit;
4417
- const countResult = await db2.select({ count: schema.table }).from(schema.table);
4418
- const total = Array.isArray(countResult) ? countResult.length : 0;
4818
+ let countResult;
4819
+ let data = [];
4820
+ const tsSchema = schemas2.find((s) => s.config.name === tableName);
4821
+ let colConfigs = {};
4822
+ let fkColumns = new Set;
4823
+ try {
4824
+ if (tsSchema) {
4825
+ colConfigs = tsSchema.config.columns || {};
4826
+ if (tsSchema.config.foreignKeys) {
4827
+ tsSchema.config.foreignKeys.forEach((fk) => fk.columns.forEach((col2) => fkColumns.add(col2)));
4828
+ }
4829
+ const countRes = await db2.select({ count: tsSchema.table }).from(tsSchema.table);
4830
+ countResult = Array.isArray(countRes) ? [{ count: countRes.length }] : [{ count: 0 }];
4831
+ data = await db2.select().from(tsSchema.table).limit(limit).offset(offset);
4832
+ } else {
4833
+ countResult = await db2.execute(rawSql(`SELECT COUNT(*) as count FROM "${reqSchema}"."${tableName}"`));
4834
+ data = await db2.execute(rawSql(`SELECT * FROM "${reqSchema}"."${tableName}" LIMIT ${limit} OFFSET ${offset}`));
4835
+ const colsRes = await db2.execute(rawSql(`SELECT column_name, data_type FROM information_schema.columns WHERE table_schema = '${reqSchema}' AND table_name = '${tableName}'`));
4836
+ if (Array.isArray(colsRes)) {
4837
+ colsRes.forEach((c2) => {
4838
+ colConfigs[c2.column_name] = { dataType: c2.data_type };
4839
+ });
4840
+ }
4841
+ }
4842
+ } catch (e) {
4843
+ return new Response(`<div class="p-4 text-red-500">Table not found or query error</div>`, { status: 404, headers: { "Content-Type": "text/html" } });
4844
+ }
4845
+ const total = Array.isArray(countResult) && countResult.length > 0 ? parseInt(countResult[0].count, 10) : 0;
4419
4846
  const totalPages = Math.ceil(total / limit) || 1;
4420
- const data = await db2.select().from(schema.table).limit(limit).offset(offset);
4847
+ if (!Array.isArray(data))
4848
+ data = [];
4421
4849
  const formatValue = (val) => {
4422
4850
  if (val === null || val === undefined)
4423
- return '<span class="italic text-muted-foreground">null</span>';
4424
- if (typeof val === "number")
4425
- return `<span class="text-foreground">${val}</span>`;
4426
- if (typeof val === "boolean")
4427
- return `<span class="text-foreground">${val}</span>`;
4428
- if (val instanceof Date)
4429
- return `<span class="text-foreground">${val.toISOString()}</span>`;
4430
- if (typeof val === "object")
4431
- return `<span class="text-foreground font-mono text-xs">${JSON.stringify(val).replace(/&/g, "&amp;").replace(/</g, "&lt;")}</span>`;
4432
- return `<span class="text-foreground">${String(val).replace(/&/g, "&amp;").replace(/</g, "&lt;")}</span>`;
4851
+ return '<span class="italic text-muted/50">NULL</span>';
4852
+ if (typeof val === "object" && !(val instanceof Date)) {
4853
+ const rawJson = JSON.stringify(val);
4854
+ const b64 = Buffer.from(rawJson).toString("base64");
4855
+ const safeJson = rawJson.replace(/&/g, "&amp;").replace(/</g, "&lt;");
4856
+ return `<button onclick="window.dispatchEvent(new CustomEvent('open-json-modal', { detail: '${b64}' }))" class="text-text font-mono text-xs hover:underline text-left truncate w-full max-w-[250px] block cursor-pointer" title="Click to view formatted data">${safeJson}</button>`;
4857
+ }
4858
+ return `<span class="text-text">${String(val).replace(/&/g, "&amp;").replace(/</g, "&lt;")}</span>`;
4433
4859
  };
4434
- if (data.length === 0) {
4435
- return new Response(`
4436
- <div class="flex flex-col items-center justify-center h-full w-full text-muted-foreground animate-[fadeIn_0.5s_ease-out]">
4437
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="w-16 h-16 mb-4 text-muted">
4438
- <circle cx="12" cy="12" r="10"></circle>
4439
- <line x1="12" y1="8" x2="12" y2="12"></line>
4440
- <line x1="12" y1="16" x2="12.01" y2="16"></line>
4441
- </svg>
4442
- <h3 class="text-foreground m-0 mb-2 text-lg">Table is Empty</h3>
4443
- <p class="text-sm">No records found in "${tableName}"</p>
4860
+ const columns = tsSchema ? Object.keys(colConfigs) : Object.keys(data[0] || {});
4861
+ let html = '<div class="h-full w-full overflow-auto bg-bg">';
4862
+ if (data.length === 0 && columns.length === 0) {
4863
+ html += '<div class="p-8 text-center text-muted">Table is Empty</div></div>';
4864
+ } else {
4865
+ html += '<table class="text-left border-collapse whitespace-nowrap min-w-max">';
4866
+ html += "<thead><tr>";
4867
+ html += `<th class="w-10 px-3 py-2 sticky top-0 z-10 text-center bg-panel"><input type="checkbox" @change="document.querySelectorAll('#data-${tabId} .row-checkbox').forEach(cb => cb.checked = $event.target.checked)" class="w-3.5 h-3.5 rounded border-muted bg-transparent cursor-pointer"></th>`;
4868
+ columns.forEach((col2) => {
4869
+ const colConfig = colConfigs[col2];
4870
+ let typeLabel = colConfig ? colConfig.dataType : "unknown";
4871
+ let badges = "";
4872
+ if (colConfig?.primaryKey) {
4873
+ badges += '<span class="ml-1.5 text-[9px] bg-emerald-500/20 text-emerald-400 px-1 py-0.5 rounded uppercase font-bold" title="Primary Key">PK</span>';
4874
+ }
4875
+ if (tsSchema && tsSchema.config.indexes) {
4876
+ const isIndexed = tsSchema.config.indexes.some((idx) => idx.columns.includes(col2));
4877
+ if (isIndexed)
4878
+ badges += '<span class="ml-1.5 text-[9px] bg-blue-500/20 text-blue-400 px-1 py-0.5 rounded uppercase font-bold" title="Indexed">IDX</span>';
4879
+ }
4880
+ if (colConfig?.unique)
4881
+ badges += '<span class="ml-1.5 text-[9px] bg-amber-500/20 text-amber-400 px-1 py-0.5 rounded uppercase font-bold" title="Unique">UQ</span>';
4882
+ if (fkColumns.has(col2))
4883
+ badges += '<span class="ml-1.5 text-[9px] bg-purple-500/20 text-purple-400 px-1 py-0.5 rounded uppercase font-bold" title="Foreign Key">FK</span>';
4884
+ html += `<th class="px-4 py-2 font-medium sticky top-0 z-10 whitespace-nowrap bg-panel text-muted hover:text-text cursor-pointer transition-colors border-b border-r border-border">
4885
+ <div class="flex items-center gap-2">
4886
+ <span class="flex items-center">${col2}${badges}</span>
4887
+ <span class="text-[10px] font-mono opacity-50">${typeLabel}</span>
4888
+ </div>
4889
+ </th>`;
4890
+ });
4891
+ html += "</tr></thead><tbody>";
4892
+ data.forEach((row) => {
4893
+ html += '<tr class="hover:bg-hover transition-colors">';
4894
+ html += '<td class="px-3 py-1.5 text-center bg-bg border-b border-r border-border"><input type="checkbox" class="row-checkbox w-3.5 h-3.5 rounded border-muted bg-transparent cursor-pointer"></td>';
4895
+ columns.forEach((col2) => {
4896
+ html += `<td class="px-4 py-1.5 max-w-[300px] overflow-hidden text-ellipsis font-mono border-b border-r border-border bg-bg">${formatValue(row[col2])}</td>`;
4897
+ });
4898
+ html += "</tr>";
4899
+ });
4900
+ html += "</tbody></table></div>";
4901
+ }
4902
+ const startRecord = total === 0 ? 0 : (page - 1) * limit + 1;
4903
+ const endRecord = Math.min(page * limit, total);
4904
+ let paginationHtml = `
4905
+ <div id="pagination-${tabId}" hx-swap-oob="true" class="flex items-center gap-4 text-xs text-muted font-mono">
4906
+ <span>${startRecord}-${endRecord} of ${total}</span>
4907
+
4908
+ <div class="flex items-center gap-3">
4909
+ <div class="relative flex items-center border border-border rounded bg-panel overflow-hidden group">
4910
+ <select
4911
+ class="bg-transparent text-text pl-2 pr-6 py-0.5 focus:outline-none cursor-pointer appearance-none text-center relative z-10 w-full"
4912
+ @change="htmx.ajax('GET', '/htmx/tables/${tableName}?schema=${reqSchema}&tabId=${tabId}&page=1&limit=' + $event.target.value, {target: '#data-${tabId}'})"
4913
+ >
4914
+ <option class="bg-bg text-text" style="background-color: #161618; color: #ededed;" value="10" ${limit === 10 ? "selected" : ""}>10</option>
4915
+ <option class="bg-bg text-text" style="background-color: #161618; color: #ededed;" value="25" ${limit === 25 ? "selected" : ""}>25</option>
4916
+ <option class="bg-bg text-text" style="background-color: #161618; color: #ededed;" value="50" ${limit === 50 ? "selected" : ""}>50</option>
4917
+ <option class="bg-bg text-text" style="background-color: #161618; color: #ededed;" value="100" ${limit === 100 ? "selected" : ""}>100</option>
4918
+ </select>
4919
+ <div class="absolute right-1 text-muted pointer-events-none z-0 group-hover:text-text transition-colors">
4920
+ <svg class="w-3 h-3" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 9 12 15 18 9"></polyline></svg>
4921
+ </div>
4922
+ </div>
4923
+
4924
+ <div class="flex items-center border border-border rounded bg-panel overflow-hidden">
4925
+ <div class="px-2 py-0.5 text-text text-center min-w-[30px]">${page}</div>
4926
+ <div class="px-1 py-0.5 text-muted border-l border-border text-[10px] flex items-center justify-center pointer-events-none bg-panel">
4927
+ of ${totalPages}
4928
+ </div>
4929
+ <button
4930
+ ${page <= 1 ? "disabled" : ""}
4931
+ hx-get="/htmx/tables/${tableName}?schema=${reqSchema}&tabId=${tabId}&page=${page - 1}&limit=${limit}"
4932
+ hx-target="#data-${tabId}"
4933
+ class="px-2 py-1 hover:bg-hover hover:text-text disabled:opacity-50 disabled:cursor-not-allowed border-l border-r border-border transition-colors flex items-center justify-center"
4934
+ ><svg class="w-3.5 h-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="15 18 9 12 15 6"></polyline></svg></button>
4935
+ <button
4936
+ ${page >= totalPages ? "disabled" : ""}
4937
+ hx-get="/htmx/tables/${tableName}?schema=${reqSchema}&tabId=${tabId}&page=${page + 1}&limit=${limit}"
4938
+ hx-target="#data-${tabId}"
4939
+ class="px-2 py-1 hover:bg-hover hover:text-text disabled:opacity-50 disabled:cursor-not-allowed transition-colors flex items-center justify-center"
4940
+ ><svg class="w-3.5 h-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="9 18 15 12 9 6"></polyline></svg></button>
4941
+ </div>
4942
+ </div>
4943
+ </div>
4944
+ `;
4945
+ return new Response(html + paginationHtml, { headers: { "Content-Type": "text/html" } });
4946
+ } catch (e) {
4947
+ return new Response(`
4948
+ <div class="p-6">
4949
+ <div class="bg-red-500/10 border border-red-500/20 text-red-400 p-4 rounded-lg">
4950
+ <h3 class="font-semibold mb-1">Error Loading Data</h3>
4951
+ <p class="text-sm opacity-80">${e.message}</p>
4444
4952
  </div>
4445
- `, { headers: { "Content-Type": "text/html" } });
4953
+ </div>
4954
+ `, { headers: { "Content-Type": "text/html" } });
4955
+ }
4956
+ }
4957
+ if (req.method === "POST" && url.pathname === "/htmx/query") {
4958
+ try {
4959
+ const body = await req.formData();
4960
+ const query = body.get("query")?.toString() || "";
4961
+ if (!query.trim()) {
4962
+ return new Response('<div class="p-6 text-muted">No query provided.</div>', { headers: { "Content-Type": "text/html" } });
4446
4963
  }
4447
- const columns2 = Object.keys(data[0] || {});
4448
- let html = '<div class="flex flex-col h-full w-full bg-background">';
4449
- html += '<div class="flex-1 overflow-auto">';
4450
- html += '<table class="text-left border-collapse text-sm whitespace-nowrap">';
4451
- html += "<thead><tr>";
4452
- columns2.forEach((col2) => {
4453
- const colConfig = schema.config.columns ? schema.config.columns[col2] : null;
4454
- let typeLabel = colConfig ? colConfig.dataType : "unknown";
4455
- let indexLabel = "";
4456
- if (colConfig?.primaryKey) {
4457
- indexLabel += '<span class="ml-1 text-[10px] bg-emerald-500/20 text-emerald-500 px-1 rounded uppercase" title="Primary Key">PK</span>';
4458
- }
4459
- if (colConfig?.unique) {
4460
- indexLabel += '<span class="ml-1 text-[10px] bg-amber-500/20 text-amber-500 px-1 rounded uppercase" title="Unique">UQ</span>';
4461
- }
4462
- if (colConfig?.references) {
4463
- indexLabel += '<span class="ml-1 text-[10px] bg-yellow-500/20 text-yellow-500 px-1 rounded uppercase" title="Foreign Key">FK</span>';
4464
- }
4465
- if (schema.config.indexes) {
4466
- const isIndexed = schema.config.indexes.some((idx) => idx.columns.includes(col2));
4467
- if (isIndexed) {
4468
- indexLabel += '<span class="ml-1 text-[10px] bg-blue-500/20 text-blue-500 px-1 rounded uppercase" title="Indexed">IDX</span>';
4469
- }
4964
+ const startMs = Date.now();
4965
+ const res = await db2.execute(rawSql(query));
4966
+ const duration2 = Date.now() - startMs;
4967
+ const data = Array.isArray(res) ? res : [res];
4968
+ if (data.length === 0 || data.length === 1 && typeof data[0] === "object" && Object.keys(data[0]).length === 0) {
4969
+ return new Response(`<div class="p-4 text-accent text-xs font-mono border-b border-border">Query executed successfully in ${duration2}ms. No data returned.</div>`, { headers: { "Content-Type": "text/html" } });
4970
+ }
4971
+ const columns = Object.keys(data[0] || {});
4972
+ const formatValue = (val) => {
4973
+ if (val === null || val === undefined)
4974
+ return '<span class="italic text-muted/50">NULL</span>';
4975
+ if (typeof val === "object" && !(val instanceof Date)) {
4976
+ const rawJson = JSON.stringify(val);
4977
+ const b64 = Buffer.from(rawJson).toString("base64");
4978
+ const safeJson = rawJson.replace(/&/g, "&amp;").replace(/</g, "&lt;");
4979
+ return `<button onclick="window.dispatchEvent(new CustomEvent('open-json-modal', { detail: '${b64}' }))" class="text-text font-mono text-xs hover:underline text-left truncate w-full max-w-[250px] block cursor-pointer" title="Click to view formatted data">${safeJson}</button>`;
4470
4980
  }
4471
- html += `<th class="bg-card border-b border-r border-border px-4 py-2.5 font-medium text-muted-foreground sticky top-0 z-10">
4472
- <div class="flex flex-col gap-0.5">
4473
- <span class="text-foreground">${col2}</span>
4474
- <div class="flex items-center flex-wrap gap-1 mt-0.5">
4475
- <span class="text-[10px] font-mono text-muted-foreground/70 mr-1">${typeLabel}</span>
4476
- ${indexLabel}
4477
- </div>
4981
+ return `<span class="text-text">${String(val).replace(/&/g, "&amp;").replace(/</g, "&lt;")}</span>`;
4982
+ };
4983
+ let html = '<div class="h-full w-full overflow-auto bg-bg">';
4984
+ html += '<table class="text-left border-collapse whitespace-nowrap min-w-max">';
4985
+ html += "<thead><tr>";
4986
+ columns.forEach((col2) => {
4987
+ const row0 = data.length > 0 ? data[0] : null;
4988
+ const typeLabel = row0 && row0[col2] != null ? typeof row0[col2] : "unknown";
4989
+ html += `<th class="px-4 py-2 font-medium sticky top-0 z-10 whitespace-nowrap bg-panel text-muted hover:text-text cursor-pointer transition-colors border-b border-r border-border">
4990
+ <div class="flex items-center gap-2">
4991
+ <span class="text-text">${col2}</span>
4992
+ <span class="text-[10px] font-mono opacity-50">${typeLabel}</span>
4478
4993
  </div>
4479
4994
  </th>`;
4480
4995
  });
4481
4996
  html += "</tr></thead><tbody>";
4482
4997
  data.forEach((row) => {
4483
- html += '<tr class="hover:bg-muted/50 transition-colors">';
4484
- columns2.forEach((col2) => {
4485
- html += `<td class="border-b border-r border-border px-4 py-2 max-w-[300px] overflow-hidden text-ellipsis">${formatValue(row[col2])}</td>`;
4998
+ html += '<tr class="hover:bg-hover transition-colors">';
4999
+ columns.forEach((col2) => {
5000
+ html += `<td class="px-4 py-1.5 max-w-[300px] overflow-hidden text-ellipsis font-mono border-b border-r border-border bg-bg">${formatValue(row[col2])}</td>`;
4486
5001
  });
4487
5002
  html += "</tr>";
4488
5003
  });
4489
5004
  html += "</tbody></table></div>";
4490
- const startRecord = (page - 1) * limit + 1;
4491
- const endRecord = Math.min(page * limit, total);
4492
- html += `
4493
- <div class="flex items-center justify-between px-4 py-3 bg-card border-t border-border text-xs text-muted-foreground shrink-0 sticky bottom-0 z-10 w-full">
4494
- <div class="flex items-center gap-4">
4495
- <span>${total} records</span>
4496
- <span>${startRecord}-${endRecord}</span>
4497
- </div>
4498
- <div class="flex items-center gap-2">
4499
- <select
4500
- name="limit"
4501
- class="bg-background border border-border text-foreground px-2 py-1.5 rounded-md focus:outline-none focus:ring-1 focus:ring-ring cursor-pointer"
4502
- @change="pageSize = $event.target.value; htmx.ajax('GET', '/htmx/tables/${tableName}?page=1&limit=' + pageSize, {target: '#data-container'})"
4503
- >
4504
- <option value="10" ${limit === 10 ? "selected" : ""}>10</option>
4505
- <option value="25" ${limit === 25 ? "selected" : ""}>25</option>
4506
- <option value="50" ${limit === 50 ? "selected" : ""}>50</option>
4507
- <option value="100" ${limit === 100 ? "selected" : ""}>100</option>
4508
- </select>
4509
-
4510
- <button
4511
- ${page <= 1 ? "disabled" : ""}
4512
- hx-get="/htmx/tables/${tableName}?page=${page - 1}&limit=${limit}"
4513
- hx-target="#data-container"
4514
- class="bg-background border border-border text-foreground px-3 py-1.5 rounded-md hover:bg-accent hover:text-accent-foreground disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
4515
- >&larr;</button>
4516
-
4517
- <span class="min-w-[40px] text-center">${page} / ${totalPages}</span>
4518
-
4519
- <button
4520
- ${page >= totalPages ? "disabled" : ""}
4521
- hx-get="/htmx/tables/${tableName}?page=${page + 1}&limit=${limit}"
4522
- hx-target="#data-container"
4523
- class="bg-background border border-border text-foreground px-3 py-1.5 rounded-md hover:bg-accent hover:text-accent-foreground disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
4524
- >&rarr;</button>
4525
- </div>
4526
- </div>
4527
- `;
4528
- html += "</div>";
5005
+ html += `<div class="px-4 py-2 text-[10px] text-muted border-t border-border font-mono shrink-0 bg-panel sticky bottom-0">${data.length} row(s) returned in ${duration2}ms</div>`;
4529
5006
  return new Response(html, { headers: { "Content-Type": "text/html" } });
4530
5007
  } catch (e) {
4531
5008
  return new Response(`
4532
5009
  <div class="p-6">
4533
5010
  <div class="bg-red-500/10 border border-red-500/20 text-red-400 p-4 rounded-lg">
4534
- <h3 class="font-semibold mb-1">Error Loading Data</h3>
4535
- <p class="text-sm opacity-80">${e.message}</p>
5011
+ <h3 class="font-semibold mb-1">Query Error</h3>
5012
+ <p class="text-sm opacity-80 font-mono break-words whitespace-pre-wrap">${e.message}</p>
4536
5013
  </div>
4537
5014
  </div>
4538
5015
  `, { headers: { "Content-Type": "text/html" } });
4539
5016
  }
4540
5017
  }
4541
5018
  if (req.method === "GET" && url.pathname === "/") {
4542
- return new Response(indexHtml, {
5019
+ const currentSchema = url.searchParams.get("schema") || config2.dbSchema || "public";
5020
+ return new Response(renderIndexHtml({ schemas: allSchemas, currentSchema }), {
4543
5021
  headers: { "Content-Type": "text/html" }
4544
5022
  });
4545
5023
  }
@@ -4554,7 +5032,7 @@ async function runStudio(config2) {
4554
5032
  // src/commands/tusky.ts
4555
5033
  var import_picocolors11 = __toESM(require_picocolors(), 1);
4556
5034
  async function runTusky(config) {
4557
- const schemas = await loadSchemas(config.schema);
5035
+ const schemas = (await loadSchemas(config.schema)).filter((s) => s.type === "table");
4558
5036
  const schemaObj = {};
4559
5037
  for (const s of schemas) {
4560
5038
  schemaObj[s.exportName] = s.table;
@@ -4684,7 +5162,7 @@ async function ensureDatabase2(dbUrl) {
4684
5162
  // package.json
4685
5163
  var package_default = {
4686
5164
  name: "@bungres/kit",
4687
- version: "0.7.0",
5165
+ version: "1.1.0",
4688
5166
  description: "CLI toolkit for @bungres/orm \u2014 migrate, push, generate, pull",
4689
5167
  license: "MIT",
4690
5168
  engines: {
@@ -4699,9 +5177,10 @@ var package_default = {
4699
5177
  types: "./dist/index.d.ts",
4700
5178
  exports: {
4701
5179
  ".": {
4702
- bun: "./src/index.ts",
5180
+ types: "./dist/index.d.ts",
4703
5181
  import: "./dist/index.js",
4704
- types: "./dist/index.d.ts"
5182
+ require: "./dist/index.js",
5183
+ default: "./dist/index.js"
4705
5184
  }
4706
5185
  },
4707
5186
  files: [
@@ -4734,7 +5213,7 @@ var package_default = {
4734
5213
  test: "bun test"
4735
5214
  },
4736
5215
  dependencies: {
4737
- "@bungres/orm": "^0.6.0",
5216
+ "@bungres/orm": "^1.0.0",
4738
5217
  "@clack/prompts": "^1.7.0",
4739
5218
  picocolors: "^1.1.1"
4740
5219
  },