@bungres/kit 1.0.0 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -168,14 +168,25 @@ var CONFIG_FILES = [
168
168
  "bungres.config.mts",
169
169
  "bungres.config.mjs"
170
170
  ];
171
- async function loadConfig(cwd = process.cwd()) {
171
+ async function loadConfig(cwd = process.cwd(), explicitConfigPath) {
172
172
  let userConfig = {};
173
- for (const file of CONFIG_FILES) {
174
- const configPath = join(cwd, file);
175
- if (await Bun.file(configPath).exists()) {
176
- const mod = await import(resolve(configPath));
173
+ if (explicitConfigPath) {
174
+ const fullPath = resolve(cwd, explicitConfigPath);
175
+ if (await Bun.file(fullPath).exists()) {
176
+ const mod = await import(fullPath);
177
177
  userConfig = mod.default ?? mod;
178
- break;
178
+ } else {
179
+ console.error(`bungres: Config file not found at ${fullPath}`);
180
+ process.exit(1);
181
+ }
182
+ } else {
183
+ for (const file of CONFIG_FILES) {
184
+ const configPath = join(cwd, file);
185
+ if (await Bun.file(configPath).exists()) {
186
+ const mod = await import(resolve(configPath));
187
+ userConfig = mod.default ?? mod;
188
+ break;
189
+ }
179
190
  }
180
191
  }
181
192
  const dbUrl = userConfig.dbCredentials?.url ?? Bun.env.DATABASE_URL ?? Bun.env.POSTGRES_URL ?? "";
@@ -268,10 +279,10 @@ function createTableFactory(casing) {
268
279
  return Object.assign(tableObj, columnConfigs);
269
280
  };
270
281
  }
271
- var table = createTableFactory("snake");
272
- var snakeCase = { table: createTableFactory("snake") };
273
- var camelCase = { table: createTableFactory("camel") };
274
- var noCasing = { table: createTableFactory("none") };
282
+ var pgTable = createTableFactory("snake");
283
+ var snakeCase = { pgTable: createTableFactory("snake") };
284
+ var camelCase = { pgTable: createTableFactory("camel") };
285
+ var noCasing = { pgTable: createTableFactory("none") };
275
286
  function buildColumn(dataType, nameOrOpts, opts) {
276
287
  let name = "";
277
288
  let options = opts;
@@ -298,8 +309,8 @@ function buildColumn(dataType, nameOrOpts, opts) {
298
309
  ...options?.check !== undefined ? { check: options.check } : {}
299
310
  };
300
311
  return Object.assign(config, {
301
- as(alias) {
302
- return Object.assign({}, this, { alias });
312
+ as(alias2) {
313
+ return Object.assign({}, this, { alias: alias2 });
303
314
  },
304
315
  array() {
305
316
  return Object.assign({}, this, { dataType: `${this.dataType}[]` });
@@ -428,18 +439,26 @@ var ilike = (column, pattern) => sql`${rawSql(colName(column))} ILIKE ${pattern}
428
439
  var isNull = (column) => rawSql(`${colName(column)} IS NULL`);
429
440
  var isNotNull = (column) => rawSql(`${colName(column)} IS NOT NULL`);
430
441
  var inArray = (column, values) => {
431
- if (values.length === 0)
442
+ if (typeof values === "object" && values !== null && "toSQL" in values) {
443
+ return sql`${rawSql(colName(column))} IN (${values.toSQL()})`;
444
+ }
445
+ const arr = values;
446
+ if (arr.length === 0)
432
447
  return rawSql("FALSE");
433
- const params = values;
434
- const placeholders = params.map((_, i) => `$${i + 1}`).join(", ");
435
- return { sql: `${colName(column)} = ANY(ARRAY[${placeholders}])`, params };
448
+ const params = arr;
449
+ const placeholders = arr.map((_, i) => `$${i + 1}`).join(", ");
450
+ return { sql: `${colName(column)} IN (${placeholders})`, params };
436
451
  };
437
452
  var notInArray = (column, values) => {
438
- if (values.length === 0)
453
+ if (typeof values === "object" && values !== null && "toSQL" in values) {
454
+ return sql`${rawSql(colName(column))} NOT IN (${values.toSQL()})`;
455
+ }
456
+ const arr = values;
457
+ if (arr.length === 0)
439
458
  return rawSql("TRUE");
440
- const params = values;
441
- const placeholders = params.map((_, i) => `$${i + 1}`).join(", ");
442
- return { sql: `${colName(column)} != ALL(ARRAY[${placeholders}])`, params };
459
+ const params = arr;
460
+ const placeholders = arr.map((_, i) => `$${i + 1}`).join(", ");
461
+ return { sql: `${colName(column)} NOT IN (${placeholders})`, params };
443
462
  };
444
463
  var between = (column, min, max) => sql`${rawSql(colName(column))} BETWEEN ${min} AND ${max}`;
445
464
  var and = (...conditions) => sqlJoin(conditions, " AND ");
@@ -538,8 +557,8 @@ class DeleteBuilder {
538
557
  _returning;
539
558
  _comment;
540
559
  _with = [];
541
- constructor(table2, executor) {
542
- this._table = table2;
560
+ constructor(table, executor) {
561
+ this._table = table;
543
562
  this._executor = executor;
544
563
  }
545
564
  then(onfulfilled, onrejected) {
@@ -607,8 +626,8 @@ class InsertBuilder {
607
626
  _returning;
608
627
  _comment;
609
628
  _with = [];
610
- constructor(table2, executor) {
611
- this._table = table2;
629
+ constructor(table, executor) {
630
+ this._table = table;
612
631
  this._executor = executor;
613
632
  }
614
633
  then(onfulfilled, onrejected) {
@@ -789,22 +808,29 @@ class SelectBuilder {
789
808
  _select;
790
809
  _selection;
791
810
  _joins = [];
811
+ _isNestedOutput = false;
792
812
  _with = [];
793
813
  _setOperations = [];
794
814
  _comment;
795
- constructor(table2, executor, selection) {
796
- this._table = table2;
815
+ constructor(table, executor, selection) {
816
+ this._table = table;
797
817
  this._executor = executor;
798
818
  this._selection = selection;
799
819
  }
800
820
  then(onfulfilled, onrejected) {
801
- return this._executor.execute(this).then(onfulfilled, onrejected);
821
+ return this._executor.execute(this).then((rows) => {
822
+ if (this._isNestedOutput) {
823
+ rows = rows.map((r) => typeof r._nested_data === "string" ? JSON.parse(r._nested_data) : r._nested_data);
824
+ }
825
+ return onfulfilled ? onfulfilled(rows) : rows;
826
+ }, onrejected);
802
827
  }
803
828
  async single() {
804
829
  if (this._limit === undefined) {
805
830
  this.limit(1);
806
831
  }
807
- return this._executor.executeSingle(this);
832
+ const rows = await this.then();
833
+ return rows[0] ?? null;
808
834
  }
809
835
  select(...columns) {
810
836
  this._select = columns;
@@ -866,40 +892,82 @@ class SelectBuilder {
866
892
  this._offset = n;
867
893
  return this;
868
894
  }
895
+ as(aliasName) {
896
+ const columns = {};
897
+ let fields = [];
898
+ if (this._selection) {
899
+ fields = Object.keys(this._selection);
900
+ } else if (this._select && this._select.length > 0) {
901
+ fields = this._select;
902
+ } else if (!(("alias" in this._table) && ("query" in this._table))) {
903
+ fields = Object.keys(getTableConfig(this._table).columns);
904
+ }
905
+ for (const f of fields) {
906
+ const key = typeof f === "string" ? f : f.name || f.alias;
907
+ const dataType = typeof f !== "string" && f.dataType ? f.dataType : "any";
908
+ if (key)
909
+ columns[key] = { name: key, tableName: aliasName, dataType };
910
+ }
911
+ const sqObj = { ...columns };
912
+ const TableConfigSymbol2 = Symbol.for("BungresTableConfig");
913
+ sqObj[TableConfigSymbol2] = {
914
+ name: aliasName,
915
+ qualifiedName: `"${aliasName}"`,
916
+ columns,
917
+ isSubquery: true,
918
+ builder: this
919
+ };
920
+ return sqObj;
921
+ }
869
922
  join(rawClause) {
870
- this._joins.push(rawClause);
923
+ this._joins.push({ chunk: rawSql(rawClause) });
871
924
  return this;
872
925
  }
873
- innerJoin(table2, condition) {
874
- this._joins.push(sql`INNER JOIN ${rawSql(getTableConfig(table2).qualifiedName)} ON ${condition}`);
926
+ _buildJoin(type, table, condition) {
927
+ const TableConfigSymbol2 = Symbol.for("BungresTableConfig");
928
+ const cfg = table[TableConfigSymbol2];
929
+ if (cfg && cfg.isSubquery) {
930
+ const sqChunk = cfg.builder.toSQL();
931
+ return {
932
+ table,
933
+ chunk: sql`${rawSql(type)} JOIN (${sqChunk}) AS "${rawSql(cfg.name)}" ON ${condition}`
934
+ };
935
+ }
936
+ return {
937
+ table,
938
+ chunk: sql`${rawSql(type)} JOIN ${rawSql(cfg.qualifiedName)} ON ${condition}`
939
+ };
940
+ }
941
+ innerJoin(table, condition) {
942
+ this._joins.push(this._buildJoin("INNER", table, condition));
875
943
  return this;
876
944
  }
877
- leftJoin(table2, condition) {
878
- this._joins.push(sql`LEFT JOIN ${rawSql(getTableConfig(table2).qualifiedName)} ON ${condition}`);
945
+ leftJoin(table, condition) {
946
+ this._joins.push(this._buildJoin("LEFT", table, condition));
879
947
  return this;
880
948
  }
881
- rightJoin(table2, condition) {
882
- this._joins.push(sql`RIGHT JOIN ${rawSql(getTableConfig(table2).qualifiedName)} ON ${condition}`);
949
+ rightJoin(table, condition) {
950
+ this._joins.push(this._buildJoin("RIGHT", table, condition));
883
951
  return this;
884
952
  }
885
- fullJoin(table2, condition) {
886
- this._joins.push(sql`FULL JOIN ${rawSql(getTableConfig(table2).qualifiedName)} ON ${condition}`);
953
+ fullJoin(table, condition) {
954
+ this._joins.push(this._buildJoin("FULL", table, condition));
887
955
  return this;
888
956
  }
889
957
  _buildSelection(selection, params) {
890
958
  const parts = [];
891
- for (const [alias, col2] of Object.entries(selection)) {
959
+ for (const [alias2, col2] of Object.entries(selection)) {
892
960
  if (typeof col2 === "object" && col2 !== null) {
893
961
  if ("sql" in col2 && "params" in col2) {
894
962
  const chunk = col2;
895
963
  const offset = params.length;
896
964
  params.push(...chunk.params);
897
- parts.push(`'${alias}', ${chunk.sql.replace(/\$(\d+)/g, (_, n) => `$${parseInt(n) + offset}`)}`);
965
+ parts.push(`'${alias2}', ${chunk.sql.replace(/\$(\d+)/g, (_, n) => `$${parseInt(n) + offset}`)}`);
898
966
  } else if ("name" in col2 && "dataType" in col2) {
899
967
  const c = col2;
900
- parts.push(`'${alias}', ${c.tableName ? c.tableName + "." : ""}"${c.name}"`);
968
+ parts.push(`'${alias2}', ${c.tableName ? c.tableName + "." : ""}"${c.name}"`);
901
969
  } else {
902
- parts.push(`'${alias}', json_build_object(${this._buildSelection(col2, params)})`);
970
+ parts.push(`'${alias2}', json_build_object(${this._buildSelection(col2, params)})`);
903
971
  }
904
972
  }
905
973
  }
@@ -908,35 +976,59 @@ class SelectBuilder {
908
976
  toSQL() {
909
977
  let cols = "";
910
978
  const params = [];
979
+ const isCte = "alias" in this._table && "query" in this._table;
980
+ const qName = isCte ? `"${this._table.alias}"` : getTableConfig(this._table).qualifiedName;
911
981
  if (this._selection) {
912
- cols = Object.entries(this._selection).map(([alias, col2]) => {
982
+ cols = Object.entries(this._selection).map(([alias2, col2]) => {
913
983
  if (typeof col2 === "object" && col2 !== null && "sql" in col2 && "params" in col2) {
914
984
  const chunk = col2;
915
985
  const offset = params.length;
916
986
  params.push(...chunk.params);
917
- return `${chunk.sql.replace(/\$(\d+)/g, (_, n) => `$${parseInt(n) + offset}`)} AS "${alias}"`;
987
+ return `${chunk.sql.replace(/\$(\d+)/g, (_, n) => `$${parseInt(n) + offset}`)} AS "${alias2}"`;
918
988
  } else if (typeof col2 === "object" && col2 !== null && "name" in col2 && "dataType" in col2) {
919
989
  const c = col2;
920
- return `${c.tableName ? c.tableName + "." : ""}"${c.name}" AS "${alias}"`;
990
+ return `${c.tableName ? c.tableName + "." : ""}"${c.name}" AS "${alias2}"`;
921
991
  } else {
922
- return `json_build_object(${this._buildSelection(col2, params)}) AS "${alias}"`;
992
+ return `json_build_object(${this._buildSelection(col2, params)}) AS "${alias2}"`;
923
993
  }
924
994
  }).join(", ");
925
995
  } else if (this._select && this._select.length > 0) {
926
- const qName = getTableConfig(this._table).qualifiedName;
927
996
  cols = this._select.map((c) => {
928
997
  if (typeof c === "string") {
929
- return `${qName}."${getTableConfig(this._table).columns[c]?.name ?? c}" AS "${c}"`;
998
+ const colName2 = isCte ? c : getTableConfig(this._table).columns[c]?.name ?? c;
999
+ return `${qName}."${colName2}" AS "${c}"`;
930
1000
  }
931
1001
  return `${c.tableName ? c.tableName + "." : ""}"${c.name}" AS "${c.alias || c.name}"`;
932
1002
  }).join(", ");
933
1003
  } else {
934
- const qName = getTableConfig(this._table).qualifiedName;
935
- const colsKeys = Object.keys(getTableConfig(this._table).columns);
936
- if (colsKeys.length === 0) {
1004
+ if (isCte) {
937
1005
  cols = "*";
938
1006
  } else {
939
- cols = colsKeys.map((c) => `${qName}."${getTableConfig(this._table).columns[c].name}" AS "${c}"`).join(", ");
1007
+ const hasJoinedTables = this._joins.some((j) => j.table);
1008
+ if (hasJoinedTables) {
1009
+ this._isNestedOutput = true;
1010
+ const rootCfg = getTableConfig(this._table);
1011
+ const tablesToSelect = [{ alias: rootCfg.name, config: rootCfg }];
1012
+ for (const join2 of this._joins) {
1013
+ if (join2.table) {
1014
+ const cfg = getTableConfig(join2.table);
1015
+ tablesToSelect.push({ alias: cfg.name, config: cfg });
1016
+ }
1017
+ }
1018
+ cols = `json_build_object(${tablesToSelect.map((t) => {
1019
+ const innerFields = Object.keys(t.config.columns).map((c) => {
1020
+ return `'${c}', "${t.config.name}"."${t.config.columns[c].name}"`;
1021
+ }).join(", ");
1022
+ return `'${t.alias}', json_build_object(${innerFields})`;
1023
+ }).join(", ")}) AS _nested_data`;
1024
+ } else {
1025
+ const colsKeys = Object.keys(getTableConfig(this._table).columns);
1026
+ if (colsKeys.length === 0) {
1027
+ cols = "*";
1028
+ } else {
1029
+ cols = colsKeys.map((c) => `${qName}."${getTableConfig(this._table).columns[c].name}" AS "${c}"`).join(", ");
1030
+ }
1031
+ }
940
1032
  }
941
1033
  }
942
1034
  let prefix = "";
@@ -950,9 +1042,9 @@ class SelectBuilder {
950
1042
  }
951
1043
  prefix = `WITH ${cteStrs.join(", ")} `;
952
1044
  }
953
- let query = `SELECT ${cols} FROM ${getTableConfig(this._table).qualifiedName}`;
1045
+ let query = `SELECT ${cols} FROM ${qName}`;
954
1046
  if (this._joins.length > 0) {
955
- const joinChunks = this._joins.map((j) => typeof j === "string" ? rawSql(j) : j);
1047
+ const joinChunks = this._joins.map((j) => j.chunk);
956
1048
  const combinedJoins = sqlJoin(joinChunks, " ");
957
1049
  const offset = params.length;
958
1050
  query += " " + combinedJoins.sql.replace(/\$(\d+)/g, (_, n) => `$${parseInt(n) + offset}`);
@@ -965,11 +1057,11 @@ class SelectBuilder {
965
1057
  params.push(...combined.params);
966
1058
  }
967
1059
  if (this._groupBy.length > 0) {
968
- const qName = getTableConfig(this._table).qualifiedName;
1060
+ const qName2 = getTableConfig(this._table).qualifiedName;
969
1061
  query += " GROUP BY " + this._groupBy.map((c) => {
970
1062
  if (typeof c === "string") {
971
- const dbCol = getTableConfig(this._table).columns[c]?.name ?? c;
972
- return `${qName}."${dbCol}"`;
1063
+ const dbCol = isCte ? c : getTableConfig(this._table).columns[c]?.name ?? c;
1064
+ return `${qName2}."${dbCol}"`;
973
1065
  } else if ("sql" in c && "params" in c) {
974
1066
  const offset = params.length;
975
1067
  params.push(...c.params);
@@ -986,11 +1078,11 @@ class SelectBuilder {
986
1078
  params.push(...combined.params);
987
1079
  }
988
1080
  if (this._orderBy.length > 0) {
989
- const qName = getTableConfig(this._table).qualifiedName;
1081
+ const qName2 = getTableConfig(this._table).qualifiedName;
990
1082
  query += " ORDER BY " + this._orderBy.map((o) => {
991
1083
  if (typeof o.column === "string") {
992
- const dbCol = getTableConfig(this._table).columns[o.column]?.name ?? o.column;
993
- return `${qName}."${dbCol}" ${o.dir.toUpperCase()}`;
1084
+ const dbCol = isCte ? o.column : getTableConfig(this._table).columns[o.column]?.name ?? o.column;
1085
+ return `${qName2}."${dbCol}" ${o.dir.toUpperCase()}`;
994
1086
  } else if ("sql" in o.column && "params" in o.column) {
995
1087
  const offset = params.length;
996
1088
  params.push(...o.column.params);
@@ -1030,8 +1122,8 @@ class SelectBuilderIntermediate {
1030
1122
  this._executor = _executor;
1031
1123
  this._selection = _selection;
1032
1124
  }
1033
- from(table2) {
1034
- return new SelectBuilder(table2, this._executor, this._selection);
1125
+ from(table) {
1126
+ return new SelectBuilder(table, this._executor, this._selection);
1035
1127
  }
1036
1128
  }
1037
1129
 
@@ -1043,8 +1135,8 @@ class UpdateBuilder {
1043
1135
  _returning;
1044
1136
  _comment;
1045
1137
  _with = [];
1046
- constructor(table2, executor) {
1047
- this._table = table2;
1138
+ constructor(table, executor) {
1139
+ this._table = table;
1048
1140
  this._executor = executor;
1049
1141
  }
1050
1142
  then(onfulfilled, onrejected) {
@@ -1226,8 +1318,8 @@ class RelationalQueryBuilder {
1226
1318
  let cached = schemaCache.get(tableName);
1227
1319
  if (cached)
1228
1320
  return cached;
1229
- const table2 = this._schema[tableName];
1230
- const tConfig = table2[TableConfigSymbol];
1321
+ const table = this._schema[tableName];
1322
+ const tConfig = table[TableConfigSymbol];
1231
1323
  const ones = {};
1232
1324
  const manys = {};
1233
1325
  const manyToManys = {};
@@ -1240,6 +1332,8 @@ class RelationalQueryBuilder {
1240
1332
  }
1241
1333
  for (const [otherName, otherTable] of Object.entries(this._schema)) {
1242
1334
  const otherConfig = otherTable[TableConfigSymbol];
1335
+ if (!otherConfig)
1336
+ continue;
1243
1337
  for (const [colName2, col2] of Object.entries(otherConfig.columns)) {
1244
1338
  if (col2.references && col2.references.table === tableName) {
1245
1339
  const ref = col2.references;
@@ -1250,6 +1344,8 @@ class RelationalQueryBuilder {
1250
1344
  }
1251
1345
  for (const [junctionName, junctionTable] of Object.entries(this._schema)) {
1252
1346
  const junctionConfig = junctionTable[TableConfigSymbol];
1347
+ if (!junctionConfig)
1348
+ continue;
1253
1349
  const refs = Object.entries(junctionConfig.columns).filter(([_, c]) => c.references);
1254
1350
  const toThis = refs.find(([_, c]) => c.references.table === tableName);
1255
1351
  if (toThis) {
@@ -1272,7 +1368,7 @@ class RelationalQueryBuilder {
1272
1368
  schemaCache.set(tableName, result);
1273
1369
  return result;
1274
1370
  }
1275
- _buildSelectJson(tableName, args, alias, params, parentAlias, joinCondition, extraJoin) {
1371
+ _buildSelectJson(tableName, args, alias2, params, parentAlias, joinCondition, extraJoin) {
1276
1372
  const tableConfig = this._schema[tableName][TableConfigSymbol];
1277
1373
  const relations = this._getRuntimeRelations(tableName);
1278
1374
  const withArgs = args.with || {};
@@ -1290,26 +1386,26 @@ class RelationalQueryBuilder {
1290
1386
  continue;
1291
1387
  }
1292
1388
  }
1293
- jsonFields.push(`'${colKey}', "${alias}"."${colConfig.name}"`);
1389
+ jsonFields.push(`'${colKey}', "${alias2}"."${colConfig.name}"`);
1294
1390
  }
1295
1391
  for (const [relKey, relArgs] of Object.entries(withArgs)) {
1296
1392
  const isTrue = relArgs === true;
1297
1393
  const rArgs = isTrue ? {} : relArgs;
1298
1394
  if (relations.ones[relKey]) {
1299
1395
  const rel = relations.ones[relKey];
1300
- const subAlias = `${alias}_${relKey}`;
1396
+ const subAlias = `${alias2}_${relKey}`;
1301
1397
  const targetConfig = this._schema[rel.targetTable][TableConfigSymbol];
1302
1398
  const targetPk = getPkColumn(targetConfig);
1303
- const joinCond = `"${subAlias}"."${targetPk}" = "${alias}"."${rel.sourceColumn}"`;
1304
- const subQuery = this._buildSelectJson(rel.targetTable, rArgs, subAlias, params, alias, joinCond);
1399
+ const joinCond = `"${subAlias}"."${targetPk}" = "${alias2}"."${rel.sourceColumn}"`;
1400
+ const subQuery = this._buildSelectJson(rel.targetTable, rArgs, subAlias, params, alias2, joinCond);
1305
1401
  const subSql = `SELECT ${subQuery.sql} ${subQuery.from}`;
1306
1402
  jsonFields.push(`'${relKey}', (${subSql})`);
1307
1403
  } else if (relations.manys[relKey]) {
1308
1404
  const rel = relations.manys[relKey];
1309
- const subAlias = `${alias}_${relKey}`;
1405
+ const subAlias = `${alias2}_${relKey}`;
1310
1406
  const thisPk = getPkColumn(tableConfig);
1311
- const joinCond = `"${subAlias}"."${rel.targetColumn}" = "${alias}"."${thisPk}"`;
1312
- const subQuery = this._buildSelectJson(rel.targetTable, rArgs, subAlias, params, alias, joinCond);
1407
+ const joinCond = `"${subAlias}"."${rel.targetColumn}" = "${alias2}"."${thisPk}"`;
1408
+ const subQuery = this._buildSelectJson(rel.targetTable, rArgs, subAlias, params, alias2, joinCond);
1313
1409
  const aggAlias = `${subAlias}_agg`;
1314
1410
  const aggSql = `
1315
1411
  SELECT COALESCE(json_agg(_sub.obj), '[]'::json)
@@ -1321,16 +1417,16 @@ class RelationalQueryBuilder {
1321
1417
  jsonFields.push(`'${relKey}', (${aggSql})`);
1322
1418
  } else if (relations.manyToManys[relKey]) {
1323
1419
  const rel = relations.manyToManys[relKey];
1324
- const subAlias = `${alias}_${relKey}`;
1325
- const junctionAlias = `${alias}_j_${relKey}`;
1420
+ const subAlias = `${alias2}_${relKey}`;
1421
+ const junctionAlias = `${alias2}_j_${relKey}`;
1326
1422
  const targetTableConfig = this._schema[rel.targetTable][TableConfigSymbol];
1327
1423
  const junctionTableConfig = this._schema[rel.junctionTable][TableConfigSymbol];
1328
1424
  const fromExtra = `
1329
1425
  INNER JOIN ${junctionTableConfig.qualifiedName} AS "${junctionAlias}"
1330
1426
  ON "${junctionAlias}"."${rel.joinTargetColumn}" = "${subAlias}"."${getPkColumn(targetTableConfig)}"
1331
1427
  `;
1332
- const whereExtra = `"${junctionAlias}"."${rel.joinSourceColumn}" = "${alias}"."${getPkColumn(tableConfig)}"`;
1333
- const subQuery = this._buildSelectJson(rel.targetTable, rArgs, subAlias, params, alias, whereExtra, fromExtra);
1428
+ const whereExtra = `"${junctionAlias}"."${rel.joinSourceColumn}" = "${alias2}"."${getPkColumn(tableConfig)}"`;
1429
+ const subQuery = this._buildSelectJson(rel.targetTable, rArgs, subAlias, params, alias2, whereExtra, fromExtra);
1334
1430
  const aggSql = `
1335
1431
  SELECT COALESCE(json_agg(_sub.obj), '[]'::json)
1336
1432
  FROM (
@@ -1342,7 +1438,7 @@ class RelationalQueryBuilder {
1342
1438
  }
1343
1439
  }
1344
1440
  let selectSql = `json_build_object(${jsonFields.join(", ")})`;
1345
- let fromSql = `FROM ${tableConfig.qualifiedName} AS "${alias}"`;
1441
+ let fromSql = `FROM ${tableConfig.qualifiedName} AS "${alias2}"`;
1346
1442
  if (extraJoin) {
1347
1443
  fromSql += ` ${extraJoin}`;
1348
1444
  }
@@ -1390,7 +1486,7 @@ class RelationalQueryBuilder {
1390
1486
  }
1391
1487
  buildSQL(args) {
1392
1488
  const params = [];
1393
- const rootAlias = "root";
1489
+ const rootAlias = this._tableName;
1394
1490
  const subQuery = this._buildSelectJson(this._tableName, args, rootAlias, params);
1395
1491
  const sql2 = `SELECT ${subQuery.sql} AS _data ${subQuery.from}`;
1396
1492
  return { sql: sql2, params };
@@ -1466,14 +1562,14 @@ class BungresDB {
1466
1562
  }
1467
1563
  return new SelectBuilderIntermediate(this);
1468
1564
  }
1469
- insert(table2) {
1470
- return new InsertBuilder(table2, this);
1565
+ insert(table) {
1566
+ return new InsertBuilder(table, this);
1471
1567
  }
1472
- update(table2) {
1473
- return new UpdateBuilder(table2, this);
1568
+ update(table) {
1569
+ return new UpdateBuilder(table, this);
1474
1570
  }
1475
- delete(table2) {
1476
- return new DeleteBuilder(table2, this);
1571
+ delete(table) {
1572
+ return new DeleteBuilder(table, this);
1477
1573
  }
1478
1574
  async execute(builder) {
1479
1575
  await this.ready();
@@ -1517,14 +1613,14 @@ class BungresTransaction {
1517
1613
  }
1518
1614
  return new SelectBuilderIntermediate(this);
1519
1615
  }
1520
- insert(table2) {
1521
- return new InsertBuilder(table2, this);
1616
+ insert(table) {
1617
+ return new InsertBuilder(table, this);
1522
1618
  }
1523
- update(table2) {
1524
- return new UpdateBuilder(table2, this);
1619
+ update(table) {
1620
+ return new UpdateBuilder(table, this);
1525
1621
  }
1526
- delete(table2) {
1527
- return new DeleteBuilder(table2, this);
1622
+ delete(table) {
1623
+ return new DeleteBuilder(table, this);
1528
1624
  }
1529
1625
  async execute(builder) {
1530
1626
  const chunk = "toSQL" in builder ? builder.toSQL() : builder;
@@ -1685,13 +1781,13 @@ function formatDefaultValue(value, dataType) {
1685
1781
  return String(value);
1686
1782
  return `'${JSON.stringify(value).replace(/'/g, "''")}'`;
1687
1783
  }
1688
- function buildIndex(table2, idx) {
1689
- const tableName = table2.schema ? `"${table2.schema}"."${table2.name}"` : `"${table2.name}"`;
1784
+ function buildIndex(table, idx) {
1785
+ const tableName = table.schema ? `"${table.schema}"."${table.name}"` : `"${table.name}"`;
1690
1786
  const unique2 = idx.unique ? "UNIQUE " : "";
1691
1787
  const using = idx.using ? ` USING ${idx.using.toUpperCase()}` : "";
1692
1788
  const cols = idx.columns.map((c) => `"${c}"`).join(", ");
1693
1789
  const where = idx.where ? ` WHERE ${idx.where}` : "";
1694
- const idxName = idx.name ?? `idx_${table2.name}_${idx.columns.join("_")}`;
1790
+ const idxName = idx.name ?? `idx_${table.name}_${idx.columns.join("_")}`;
1695
1791
  return `CREATE ${unique2}INDEX IF NOT EXISTS "${idxName}" ON ${tableName}${using} (${cols})${where};`;
1696
1792
  }
1697
1793
  function generateAddColumn(tableName, schema, key, col2) {
@@ -1702,6 +1798,44 @@ function generateDropColumn(tableName, schema, columnName) {
1702
1798
  const tbl = schema ? `"${schema}"."${tableName}"` : `"${tableName}"`;
1703
1799
  return `ALTER TABLE ${tbl} DROP COLUMN IF EXISTS "${columnName}";`;
1704
1800
  }
1801
+ function generateCreateEnum(name, values) {
1802
+ const vals = values.map((v) => `'${v.replace(/'/g, "''")}'`).join(", ");
1803
+ return `CREATE TYPE "${name}" AS ENUM (${vals});`;
1804
+ }
1805
+ function generateDropEnum(name, ifExists = true) {
1806
+ return `DROP TYPE${ifExists ? " IF EXISTS" : ""} "${name}";`;
1807
+ }
1808
+ function inlineParams(chunk) {
1809
+ let { sql: sql2, params } = chunk;
1810
+ return sql2.replace(/\$(\d+)/g, (_, n) => {
1811
+ const p = params[parseInt(n) - 1];
1812
+ if (typeof p === "string")
1813
+ return `'${p.replace(/'/g, "''")}'`;
1814
+ if (typeof p === "number")
1815
+ return String(p);
1816
+ if (typeof p === "boolean")
1817
+ return p ? "TRUE" : "FALSE";
1818
+ if (p === null)
1819
+ return "NULL";
1820
+ return `'${String(p).replace(/'/g, "''")}'`;
1821
+ });
1822
+ }
1823
+ function generateCreateView(view) {
1824
+ const kind = view.materialized ? "MATERIALIZED VIEW" : "VIEW";
1825
+ let inlineSql = "";
1826
+ if (view.sql) {
1827
+ inlineSql = view.sql;
1828
+ } else if (view.query && typeof view.query.toSQL === "function") {
1829
+ inlineSql = inlineParams(view.query.toSQL());
1830
+ } else {
1831
+ inlineSql = "SELECT * FROM (VALUES ('LEGACY_SNAPSHOT_MISSING_SQL')) AS t(c)";
1832
+ }
1833
+ return `CREATE ${kind} "${view.name}" AS ${inlineSql};`;
1834
+ }
1835
+ function generateDropView(view, ifExists = true) {
1836
+ const kind = view.materialized ? "MATERIALIZED VIEW" : "VIEW";
1837
+ return `DROP ${kind}${ifExists ? " IF EXISTS" : ""} "${view.name}";`;
1838
+ }
1705
1839
 
1706
1840
  // src/schema-loader.ts
1707
1841
  async function loadSchemas(patterns, cwd = process.cwd()) {
@@ -1715,11 +1849,27 @@ async function loadSchemas(patterns, cwd = process.cwd()) {
1715
1849
  for (const [exportName, value] of Object.entries(mod)) {
1716
1850
  if (isTable(value)) {
1717
1851
  entries.push({
1852
+ type: "table",
1718
1853
  exportName,
1719
1854
  config: value[TableConfigSymbol],
1720
1855
  table: value,
1721
1856
  filePath: absPath
1722
1857
  });
1858
+ } else if (isEnum(value)) {
1859
+ entries.push({
1860
+ type: "enum",
1861
+ exportName,
1862
+ enumName: value.enumName,
1863
+ enumValues: value.enumValues,
1864
+ filePath: absPath
1865
+ });
1866
+ } else if (isView(value)) {
1867
+ entries.push({
1868
+ type: "view",
1869
+ exportName,
1870
+ config: value,
1871
+ filePath: absPath
1872
+ });
1723
1873
  }
1724
1874
  }
1725
1875
  }
@@ -1729,17 +1879,67 @@ async function loadSchemas(patterns, cwd = process.cwd()) {
1729
1879
  function isTable(value) {
1730
1880
  return typeof value === "object" && value !== null && TableConfigSymbol in value;
1731
1881
  }
1882
+ function isEnum(value) {
1883
+ return typeof value === "function" && "enumName" in value && "enumValues" in value && Array.isArray(value.enumValues);
1884
+ }
1885
+ function isView(value) {
1886
+ return typeof value === "object" && value !== null && "name" in value && "query" in value && typeof value.query === "object" && typeof value.query.toSQL === "function";
1887
+ }
1732
1888
  // src/differ.ts
1733
1889
  function diffSchemas(prev, next) {
1734
1890
  const statements = [];
1735
1891
  const summary = [];
1736
1892
  const warnings = [];
1737
- const prevTables = new Set(Object.keys(prev));
1738
- const nextTables = new Set(Object.keys(next));
1893
+ const prevEnums = prev.enums || {};
1894
+ const nextEnums = next.enums || {};
1895
+ const prevEnumNames = new Set(Object.keys(prevEnums));
1896
+ const nextEnumNames = new Set(Object.keys(nextEnums));
1897
+ for (const enumName of nextEnumNames) {
1898
+ if (!prevEnumNames.has(enumName)) {
1899
+ const e = nextEnums[enumName];
1900
+ statements.push(generateCreateEnum(e.enumName, e.enumValues));
1901
+ summary.push(`CREATE TYPE ${e.enumName}`);
1902
+ }
1903
+ }
1904
+ for (const enumName of prevEnumNames) {
1905
+ if (!nextEnumNames.has(enumName)) {
1906
+ const e = prevEnums[enumName];
1907
+ statements.push(generateDropEnum(e.enumName, true));
1908
+ summary.push(`DROP TYPE ${e.enumName}`);
1909
+ }
1910
+ }
1911
+ for (const enumName of nextEnumNames) {
1912
+ if (prevEnumNames.has(enumName)) {
1913
+ const p = prevEnums[enumName];
1914
+ const n = nextEnums[enumName];
1915
+ if (JSON.stringify(p.enumValues) !== JSON.stringify(n.enumValues)) {
1916
+ warnings.push(`Enum '${n.enumName}' values have changed. Bungres Kit currently requires manual migration for ALTER TYPE ... ADD VALUE.`);
1917
+ }
1918
+ }
1919
+ }
1920
+ const prevViews = prev.views || {};
1921
+ const nextViews = next.views || {};
1922
+ const prevViewNames = new Set(Object.keys(prevViews));
1923
+ const nextViewNames = new Set(Object.keys(nextViews));
1924
+ for (const viewName of prevViewNames) {
1925
+ if (!nextViewNames.has(viewName)) {
1926
+ statements.push(generateDropView(prevViews[viewName]));
1927
+ summary.push(`DROP VIEW ${viewName}`);
1928
+ } else {
1929
+ const pSql = generateCreateView(prevViews[viewName]);
1930
+ const nSql = generateCreateView(nextViews[viewName]);
1931
+ if (pSql !== nSql) {
1932
+ statements.push(generateDropView(prevViews[viewName]));
1933
+ summary.push(`DROP VIEW ${viewName} (for recreation)`);
1934
+ }
1935
+ }
1936
+ }
1937
+ const prevTables = new Set(Object.keys(prev.tables || {}));
1938
+ const nextTables = new Set(Object.keys(next.tables || {}));
1739
1939
  const newTableConfigs = [];
1740
1940
  for (const tableName of nextTables) {
1741
1941
  if (!prevTables.has(tableName)) {
1742
- newTableConfigs.push(next[tableName]);
1942
+ newTableConfigs.push(next.tables[tableName]);
1743
1943
  }
1744
1944
  }
1745
1945
  for (const config of topoSortConfigs(newTableConfigs)) {
@@ -1748,7 +1948,7 @@ function diffSchemas(prev, next) {
1748
1948
  }
1749
1949
  for (const tableName of prevTables) {
1750
1950
  if (!nextTables.has(tableName)) {
1751
- const config = prev[tableName];
1951
+ const config = prev.tables[tableName];
1752
1952
  const tbl = config.schema ? `"${config.schema}"."${tableName}"` : `"${tableName}"`;
1753
1953
  statements.push(`DROP TABLE IF EXISTS ${tbl};`);
1754
1954
  summary.push(`DROP TABLE ${tableName}`);
@@ -1758,8 +1958,8 @@ function diffSchemas(prev, next) {
1758
1958
  for (const tableName of nextTables) {
1759
1959
  if (!prevTables.has(tableName))
1760
1960
  continue;
1761
- const prevConfig = prev[tableName];
1762
- const nextConfig = next[tableName];
1961
+ const prevConfig = prev.tables[tableName];
1962
+ const nextConfig = next.tables[tableName];
1763
1963
  const prevCols = prevConfig.columns;
1764
1964
  const nextCols = nextConfig.columns;
1765
1965
  const prevColNames = new Set(Object.keys(prevCols));
@@ -1831,6 +2031,19 @@ function diffSchemas(prev, next) {
1831
2031
  }
1832
2032
  }
1833
2033
  }
2034
+ for (const viewName of nextViewNames) {
2035
+ if (!prevViewNames.has(viewName)) {
2036
+ statements.push(generateCreateView(nextViews[viewName]));
2037
+ summary.push(`CREATE VIEW ${viewName}`);
2038
+ } else {
2039
+ const pSql = generateCreateView(prevViews[viewName]);
2040
+ const nSql = generateCreateView(nextViews[viewName]);
2041
+ if (pSql !== nSql) {
2042
+ statements.push(generateCreateView(nextViews[viewName]));
2043
+ summary.push(`CREATE VIEW ${viewName} (recreated)`);
2044
+ }
2045
+ }
2046
+ }
1834
2047
  return { statements, summary, warnings };
1835
2048
  }
1836
2049
  function topoSortConfigs(tables) {
@@ -1851,14 +2064,21 @@ function topoSortConfigs(tables) {
1851
2064
  visit(dep);
1852
2065
  result.push(config);
1853
2066
  }
1854
- for (const table2 of tables)
1855
- visit(table2.name);
2067
+ for (const table of tables)
2068
+ visit(table.name);
1856
2069
  return result;
1857
2070
  }
1858
2071
  function diffColumn(prev, next) {
1859
2072
  const changes = [];
1860
2073
  const col2 = `"${next.name}"`;
2074
+ const prevDef = prev.defaultFn ?? (prev.defaultValue !== undefined ? String(prev.defaultValue) : undefined);
2075
+ const nextDef = next.defaultFn ?? (next.defaultValue !== undefined ? String(next.defaultValue) : undefined);
2076
+ let typeChanged = false;
1861
2077
  if (prev.dataType !== next.dataType) {
2078
+ typeChanged = true;
2079
+ if (prevDef !== undefined) {
2080
+ changes.push(`ALTER COLUMN ${col2} DROP DEFAULT`);
2081
+ }
1862
2082
  changes.push(`ALTER COLUMN ${col2} TYPE ${next.dataType.toUpperCase()} USING ${col2}::${next.dataType}`);
1863
2083
  } else {
1864
2084
  const prevLen = prev.length;
@@ -1875,9 +2095,12 @@ function diffColumn(prev, next) {
1875
2095
  if (prev.notNull && !next.notNull) {
1876
2096
  changes.push(`ALTER COLUMN ${col2} DROP NOT NULL`);
1877
2097
  }
1878
- const prevDef = prev.defaultFn ?? (prev.defaultValue !== undefined ? String(prev.defaultValue) : undefined);
1879
- const nextDef = next.defaultFn ?? (next.defaultValue !== undefined ? String(next.defaultValue) : undefined);
1880
- if (prevDef !== nextDef) {
2098
+ if (typeChanged) {
2099
+ if (nextDef !== undefined) {
2100
+ const val = next.defaultFn ?? formatDefault(next.defaultValue, next.dataType);
2101
+ changes.push(`ALTER COLUMN ${col2} SET DEFAULT ${val}`);
2102
+ }
2103
+ } else if (prevDef !== nextDef) {
1881
2104
  if (nextDef !== undefined) {
1882
2105
  const val = next.defaultFn ?? formatDefault(next.defaultValue, next.dataType);
1883
2106
  changes.push(`ALTER COLUMN ${col2} SET DEFAULT ${val}`);
@@ -3000,6 +3223,30 @@ ${i}` : `${s}${styleText("inverse", r2)}${i}`;
3000
3223
  });
3001
3224
  }
3002
3225
  }
3226
+ class n extends V {
3227
+ get userInputWithCursor() {
3228
+ if (this.state === "submit")
3229
+ return this.userInput;
3230
+ const t2 = this.userInput;
3231
+ if (this.cursor >= t2.length)
3232
+ return `${this.userInput}\u2588`;
3233
+ const r2 = t2.slice(0, this.cursor), s = t2.slice(this.cursor, this.cursor + 1), e = t2.slice(this.cursor + 1);
3234
+ return `${r2}${styleText("inverse", s)}${e}`;
3235
+ }
3236
+ get cursor() {
3237
+ return this._cursor;
3238
+ }
3239
+ constructor(t2) {
3240
+ super({
3241
+ ...t2,
3242
+ initialUserInput: t2.initialUserInput ?? t2.initialValue
3243
+ }), this.on("userInput", (r2) => {
3244
+ this._setValue(r2);
3245
+ }), this.on("finalize", () => {
3246
+ this.value || (this.value = t2.defaultValue), this.value === undefined && (this.value = "");
3247
+ });
3248
+ }
3249
+ }
3003
3250
 
3004
3251
  // ../../node_modules/.bun/@clack+prompts@1.7.0/node_modules/@clack/prompts/dist/index.mjs
3005
3252
  import { styleText as styleText2, stripVTControlCharacters } from "util";
@@ -3260,24 +3507,62 @@ var SELECT_INSTRUCTIONS = [
3260
3507
  `${styleText2("dim", "Enter:")} confirm`
3261
3508
  ];
3262
3509
  var i = `${styleText2("gray", S_BAR)} `;
3510
+ var text = (e) => new n({
3511
+ validate: e.validate,
3512
+ placeholder: e.placeholder,
3513
+ defaultValue: e.defaultValue,
3514
+ initialValue: e.initialValue,
3515
+ output: e.output,
3516
+ signal: e.signal,
3517
+ input: e.input,
3518
+ render() {
3519
+ const i2 = e?.withGuide ?? settings.withGuide, s = `${`${i2 ? `${styleText2("gray", S_BAR)}
3520
+ ` : ""}${symbol(this.state)} `}${e.message}
3521
+ `, c2 = e.placeholder && e.placeholder.length > 0 ? styleText2("inverse", e.placeholder[0]) + styleText2("dim", e.placeholder.slice(1)) : styleText2(["inverse", "hidden"], "_"), o2 = this.userInput ? this.userInputWithCursor : c2, l2 = this.value ?? "";
3522
+ switch (this.state) {
3523
+ case "error": {
3524
+ const n2 = this.error ? ` ${styleText2("yellow", this.error)}` : "", r2 = i2 ? `${styleText2("yellow", S_BAR)} ` : "", d = i2 ? styleText2("yellow", S_BAR_END) : "";
3525
+ return `${s.trim()}
3526
+ ${r2}${o2}
3527
+ ${d}${n2}
3528
+ `;
3529
+ }
3530
+ case "submit": {
3531
+ const n2 = l2 ? ` ${styleText2("dim", l2)}` : "", r2 = i2 ? styleText2("gray", S_BAR) : "";
3532
+ return `${s}${r2}${n2}`;
3533
+ }
3534
+ case "cancel": {
3535
+ const n2 = l2 ? ` ${styleText2(["strikethrough", "dim"], l2)}` : "", r2 = i2 ? styleText2("gray", S_BAR) : "";
3536
+ return `${s}${r2}${n2}${l2.trim() ? `
3537
+ ${r2}` : ""}`;
3538
+ }
3539
+ default: {
3540
+ const n2 = i2 ? `${styleText2("cyan", S_BAR)} ` : "", r2 = i2 ? styleText2("cyan", S_BAR_END) : "";
3541
+ return `${s}${n2}${o2}
3542
+ ${r2}
3543
+ `;
3544
+ }
3545
+ }
3546
+ }
3547
+ }).prompt();
3263
3548
 
3264
3549
  // src/commands/push.ts
3265
3550
  var import_picocolors = __toESM(require_picocolors(), 1);
3266
3551
  async function runPush(config, opts = {}) {
3267
3552
  intro(import_picocolors.default.bgCyan(import_picocolors.default.black(" @bungres/kit push ")));
3268
- const s = spinner();
3269
- s.start("Loading schemas...");
3553
+ let activeSpinner = spinner();
3554
+ activeSpinner.start("Loading schemas...");
3270
3555
  const schemas = await loadSchemas(config.schema);
3271
3556
  if (schemas.length === 0) {
3272
- s.stop("No schemas found.");
3557
+ activeSpinner.stop("No schemas found.");
3273
3558
  log.warn(import_picocolors.default.yellow("No table definitions found. Check your schema glob pattern."));
3274
3559
  outro("Failed.");
3275
3560
  return;
3276
3561
  }
3277
- s.stop(`Loaded ${schemas.length} schemas.`);
3562
+ activeSpinner.stop(`Loaded ${schemas.length} schemas.`);
3278
3563
  const sql2 = new Bun.SQL(config.dbUrl);
3279
- const ms = spinner();
3280
- ms.start("Computing diff from database...");
3564
+ activeSpinner = spinner();
3565
+ activeSpinner.start("Computing diff from database...");
3281
3566
  try {
3282
3567
  await sql2.unsafe(`CREATE SCHEMA IF NOT EXISTS "${config.migrationsSchema}";`);
3283
3568
  const qualifiedPush = `"${config.migrationsSchema}"."__bungres_push"`;
@@ -3288,19 +3573,41 @@ async function runPush(config, opts = {}) {
3288
3573
  );
3289
3574
  `);
3290
3575
  const rows = await sql2.unsafe(`SELECT snapshot FROM ${qualifiedPush} ORDER BY id DESC LIMIT 1;`);
3291
- let prevSnapshot = {};
3576
+ let prevSnapshot = { tables: {}, enums: {}, views: {} };
3292
3577
  if (rows.length > 0) {
3293
- prevSnapshot = typeof rows[0].snapshot === "string" ? JSON.parse(rows[0].snapshot) : rows[0].snapshot;
3578
+ const parsed = typeof rows[0].snapshot === "string" ? JSON.parse(rows[0].snapshot) : rows[0].snapshot;
3579
+ if (parsed.tables || parsed.enums || parsed.views) {
3580
+ prevSnapshot = {
3581
+ tables: parsed.tables || {},
3582
+ enums: parsed.enums || {},
3583
+ views: parsed.views || {}
3584
+ };
3585
+ } else {
3586
+ prevSnapshot = { tables: parsed, enums: {}, views: {} };
3587
+ }
3588
+ }
3589
+ const currentSnapshot = { tables: {}, enums: {}, views: {} };
3590
+ for (const s of schemas) {
3591
+ if (s.type === "table") {
3592
+ currentSnapshot.tables[s.config.name] = s.config;
3593
+ } else if (s.type === "enum") {
3594
+ currentSnapshot.enums[s.enumName] = { enumName: s.enumName, enumValues: s.enumValues };
3595
+ } else if (s.type === "view") {
3596
+ currentSnapshot.views[s.config.name] = {
3597
+ name: s.config.name,
3598
+ materialized: s.config.materialized,
3599
+ sql: typeof s.config.query?.toSQL === "function" ? inlineParams(s.config.query.toSQL()) : s.config.sql
3600
+ };
3601
+ }
3294
3602
  }
3295
- const currentSnapshot = Object.fromEntries(schemas.map((schemaEntry) => [schemaEntry.config.name, schemaEntry.config]));
3296
3603
  const diff = diffSchemas(prevSnapshot, currentSnapshot);
3297
3604
  if (diff.statements.length === 0) {
3298
- ms.stop("Up to date.");
3605
+ activeSpinner.stop("Up to date.");
3299
3606
  log.success(import_picocolors.default.green("No schema changes detected. Database is up to date."));
3300
3607
  outro("Done.");
3301
3608
  return;
3302
3609
  }
3303
- ms.stop(`Computed ${diff.statements.length} statements.`);
3610
+ activeSpinner.stop(`Computed ${diff.statements.length} statements.`);
3304
3611
  log.message(import_picocolors.default.bold("Changes to apply:"));
3305
3612
  for (const change of diff.summary) {
3306
3613
  if (change.startsWith("CREATE") || change.startsWith("ALTER TABLE") && change.includes("ADD")) {
@@ -3327,8 +3634,8 @@ async function runPush(config, opts = {}) {
3327
3634
  return;
3328
3635
  }
3329
3636
  }
3330
- const exSpinner = spinner();
3331
- exSpinner.start("Pushing changes...");
3637
+ activeSpinner = spinner();
3638
+ activeSpinner.start("Pushing changes...");
3332
3639
  for (const stmt of diff.statements) {
3333
3640
  if (config.verbose) {
3334
3641
  log.info(import_picocolors.default.gray(`-- ${stmt}`));
@@ -3336,9 +3643,10 @@ async function runPush(config, opts = {}) {
3336
3643
  await sql2.unsafe(stmt);
3337
3644
  }
3338
3645
  await sql2.unsafe(`INSERT INTO ${qualifiedPush} (snapshot) VALUES ($1);`, [JSON.stringify(currentSnapshot)]);
3339
- exSpinner.stop(import_picocolors.default.green("Changes applied successfully."));
3646
+ activeSpinner.stop(import_picocolors.default.green("Changes applied successfully."));
3340
3647
  outro(import_picocolors.default.cyan("\u2728 Push complete."));
3341
3648
  } catch (err) {
3649
+ activeSpinner.stop("Failed.");
3342
3650
  log.error(import_picocolors.default.red(`Push failed: ${err.message}`));
3343
3651
  outro("Failed.");
3344
3652
  } finally {
@@ -3365,19 +3673,50 @@ async function runGenerate(config, name) {
3365
3673
  const metaDir = join3(migrationsDir, "meta");
3366
3674
  await Bun.$`mkdir -p ${migrationsDir}`.quiet();
3367
3675
  await Bun.$`mkdir -p ${metaDir}`.quiet();
3368
- const currentSnapshot = Object.fromEntries(schemas.map((s2) => [s2.config.name, s2.config]));
3369
- let prevSnapshot = {};
3676
+ const currentSnapshot = { tables: {}, enums: {}, views: {} };
3677
+ for (const s2 of schemas) {
3678
+ if (s2.type === "table") {
3679
+ currentSnapshot.tables[s2.config.name] = s2.config;
3680
+ } else if (s2.type === "enum") {
3681
+ currentSnapshot.enums[s2.enumName] = { enumName: s2.enumName, enumValues: s2.enumValues };
3682
+ } else if (s2.type === "view") {
3683
+ currentSnapshot.views[s2.config.name] = {
3684
+ name: s2.config.name,
3685
+ materialized: s2.config.materialized,
3686
+ sql: typeof s2.config.query?.toSQL === "function" ? inlineParams(s2.config.query.toSQL()) : s2.config.sql
3687
+ };
3688
+ }
3689
+ }
3690
+ let prevSnapshot = { tables: {}, enums: {}, views: {} };
3370
3691
  let isFirstMigration = true;
3371
3692
  try {
3372
3693
  const files = readdirSync(metaDir).filter((f2) => f2.endsWith("_snapshot.json")).sort();
3373
3694
  if (files.length > 0) {
3374
3695
  const latest = files[files.length - 1];
3375
- prevSnapshot = JSON.parse(await Bun.file(join3(metaDir, latest)).text());
3696
+ const parsed = JSON.parse(await Bun.file(join3(metaDir, latest)).text());
3697
+ if (parsed.tables || parsed.enums || parsed.views) {
3698
+ prevSnapshot = {
3699
+ tables: parsed.tables || {},
3700
+ enums: parsed.enums || {},
3701
+ views: parsed.views || {}
3702
+ };
3703
+ } else {
3704
+ prevSnapshot = { tables: parsed, enums: {}, views: {} };
3705
+ }
3376
3706
  isFirstMigration = false;
3377
3707
  } else {
3378
3708
  const legacyFile2 = Bun.file(join3(migrationsDir, ".snapshot.json"));
3379
3709
  if (await legacyFile2.exists()) {
3380
- prevSnapshot = JSON.parse(await legacyFile2.text());
3710
+ const parsed = JSON.parse(await legacyFile2.text());
3711
+ if (parsed.tables || parsed.enums || parsed.views) {
3712
+ prevSnapshot = {
3713
+ tables: parsed.tables || {},
3714
+ enums: parsed.enums || {},
3715
+ views: parsed.views || {}
3716
+ };
3717
+ } else {
3718
+ prevSnapshot = { tables: parsed, enums: {}, views: {} };
3719
+ }
3381
3720
  isFirstMigration = false;
3382
3721
  }
3383
3722
  }
@@ -3397,17 +3736,30 @@ async function runGenerate(config, name) {
3397
3736
  let summary;
3398
3737
  let warnings = [];
3399
3738
  if (isFirstMigration) {
3400
- const sorted = topoSort(schemas);
3401
- upStatements = sorted.flatMap((entry) => [
3402
- `-- ${entry.exportName}`,
3403
- generateCreateTable(entry.config, true),
3404
- ``
3405
- ]);
3406
- downStatements = [...sorted].reverse().flatMap((entry) => {
3739
+ const tableSchemas = schemas.filter((s2) => s2.type === "table");
3740
+ const enumSchemas = schemas.filter((s2) => s2.type === "enum");
3741
+ const viewSchemas = schemas.filter((s2) => s2.type === "view");
3742
+ const sorted = topoSort(tableSchemas);
3743
+ upStatements = [];
3744
+ downStatements = [];
3745
+ summary = [];
3746
+ for (const e of enumSchemas) {
3747
+ upStatements.push(`-- ${e.exportName}`, `CREATE TYPE "${e.enumName}" AS ENUM (${e.enumValues.map((v) => `'${v.replace(/'/g, "''")}'`).join(", ")});`, ``);
3748
+ downStatements.unshift(`DROP TYPE IF EXISTS "${e.enumName}";`);
3749
+ summary.push(`CREATE TYPE ${e.enumName}`);
3750
+ }
3751
+ for (const entry of sorted) {
3752
+ upStatements.push(`-- ${entry.exportName}`, generateCreateTable(entry.config, true), ``);
3407
3753
  const tbl = entry.config.schema ? `"${entry.config.schema}"."${entry.config.name}"` : `"${entry.config.name}"`;
3408
- return [`DROP TABLE IF EXISTS ${tbl};`];
3409
- });
3410
- summary = sorted.map((s2) => `CREATE TABLE ${s2.config.name}`);
3754
+ downStatements.unshift(`DROP TABLE IF EXISTS ${tbl};`);
3755
+ summary.push(`CREATE TABLE ${entry.config.name}`);
3756
+ }
3757
+ for (const v of viewSchemas) {
3758
+ upStatements.push(`-- ${v.exportName}`, generateCreateView(v.config), ``);
3759
+ const isMat = v.config.materialized ? "MATERIALIZED VIEW" : "VIEW";
3760
+ downStatements.unshift(`DROP ${isMat} IF EXISTS "${v.config.name}";`);
3761
+ summary.push(`CREATE ${isMat} ${v.config.name}`);
3762
+ }
3411
3763
  } else {
3412
3764
  const upDiff = diffSchemas(prevSnapshot, currentSnapshot);
3413
3765
  if (upDiff.statements.length === 0) {
@@ -3497,9 +3849,9 @@ async function runMigrate(config) {
3497
3849
  intro(import_picocolors3.default.bgCyan(import_picocolors3.default.black(" @bungres/kit migrate ")));
3498
3850
  const migrationsDir = resolve4(config.out);
3499
3851
  const sql2 = new Bun.SQL(config.dbUrl);
3500
- const table2 = config.migrationsTable;
3852
+ const table = config.migrationsTable;
3501
3853
  const schema = config.migrationsSchema;
3502
- const qualifiedTable = `"${schema}"."${table2}"`;
3854
+ const qualifiedTable = `"${schema}"."${table}"`;
3503
3855
  const createSchema = `CREATE SCHEMA IF NOT EXISTS "${schema}";`;
3504
3856
  const createMigrationsTable = `
3505
3857
  CREATE TABLE IF NOT EXISTS ${qualifiedTable} (
@@ -3507,7 +3859,7 @@ CREATE TABLE IF NOT EXISTS ${qualifiedTable} (
3507
3859
  name TEXT NOT NULL UNIQUE,
3508
3860
  applied_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
3509
3861
  );`.trim();
3510
- const s = spinner();
3862
+ let s = spinner();
3511
3863
  s.start("Checking pending migrations...");
3512
3864
  try {
3513
3865
  await sql2.unsafe(createSchema);
@@ -3536,8 +3888,8 @@ CREATE TABLE IF NOT EXISTS ${qualifiedTable} (
3536
3888
  }
3537
3889
  s.stop(`Found ${pending.length} pending migration(s).`);
3538
3890
  for (const file of pending) {
3539
- const ms = spinner();
3540
- ms.start(`Applying ${file}...`);
3891
+ s = spinner();
3892
+ s.start(`Applying ${file}...`);
3541
3893
  const content = await Bun.file(join4(migrationsDir, file)).text();
3542
3894
  let upContent = content;
3543
3895
  const upMatch = content.match(/-- ==== UP ====([\s\S]*?)(?:-- ==== DOWN ====|$)/i);
@@ -3556,10 +3908,11 @@ ${upContent}
3556
3908
  }
3557
3909
  await txSql.unsafe(`INSERT INTO ${qualifiedTable} (name) VALUES ($1)`, [file]);
3558
3910
  });
3559
- ms.stop(import_picocolors3.default.green(`\u2713 Applied ${file}`));
3911
+ s.stop(import_picocolors3.default.green(`\u2713 Applied ${file}`));
3560
3912
  }
3561
3913
  outro(import_picocolors3.default.cyan("\u2728 All migrations applied successfully."));
3562
3914
  } catch (err) {
3915
+ s.stop("Failed.");
3563
3916
  log.error(import_picocolors3.default.red(`Migration failed: ${err.message}`));
3564
3917
  outro("Failed.");
3565
3918
  } finally {
@@ -3664,20 +4017,19 @@ function generateSchemaTS(tableMap, dbSchema) {
3664
4017
  `// Generated at: ${new Date().toISOString()}`,
3665
4018
  ``,
3666
4019
  `import {`,
3667
- ` table,`,
4020
+ ` pgTable,`,
3668
4021
  ` text, varchar, char, integer, bigint, smallint,`,
3669
4022
  ` serial, bigserial, boolean, real, doublePrecision,`,
3670
4023
  ` numeric, decimal, json, jsonb,`,
3671
4024
  ` timestamp, timestamptz, date, time, uuid,`,
3672
4025
  ` bytea, inet,`,
3673
- ` textArray, integerArray, varcharArray, uuidArray,`,
3674
4026
  `} from "@bungres/orm";`,
3675
4027
  ``
3676
4028
  ];
3677
- for (const [, table2] of tableMap) {
3678
- const varName = toCamelCase(table2.tableName);
3679
- lines.push(`export const ${varName} = table("${table2.tableName}", {`);
3680
- for (const col2 of table2.columns) {
4029
+ for (const [, table] of tableMap) {
4030
+ const varName = toCamelCase(table.tableName);
4031
+ lines.push(`export const ${varName} = pgTable("${table.tableName}", {`);
4032
+ for (const col2 of table.columns) {
3681
4033
  const colExpr = buildColumnExpression(col2);
3682
4034
  lines.push(` ${toCamelCase(col2.name)}: ${colExpr},`);
3683
4035
  }
@@ -3685,9 +4037,9 @@ function generateSchemaTS(tableMap, dbSchema) {
3685
4037
  if (dbSchema !== "public") {
3686
4038
  options.push(`schema: "${dbSchema}"`);
3687
4039
  }
3688
- if (table2.indexes.length > 0) {
4040
+ if (table.indexes.length > 0) {
3689
4041
  const idxLines = [];
3690
- for (const idx of table2.indexes) {
4042
+ for (const idx of table.indexes) {
3691
4043
  const m2 = idx.indexdef.match(/CREATE (UNIQUE )?INDEX (.+) ON (.+) USING (\w+) \((.+)\)(?: WHERE (.+))?/i);
3692
4044
  if (m2) {
3693
4045
  const isUnique = !!m2[1];
@@ -3757,11 +4109,13 @@ function buildColumnExpression(col2) {
3757
4109
  refOpts += `, onUpdate: "${updateRule}"`;
3758
4110
  opts.push(`references: { ${refOpts} }`);
3759
4111
  }
3760
- let builderName = pgTypeToBungresBuilderName(col2);
3761
- if (opts.length > 0) {
3762
- return `${builderName}({ ${opts.join(", ")} })`;
3763
- }
3764
- return `${builderName}()`;
4112
+ let builderResult = pgTypeToBungresBuilderName(col2);
4113
+ let builderName = typeof builderResult === "string" ? builderResult : builderResult.base;
4114
+ let isArray = typeof builderResult !== "string" && builderResult.isArray;
4115
+ let expr = opts.length > 0 ? `${builderName}({ ${opts.join(", ")} })` : `${builderName}()`;
4116
+ if (isArray)
4117
+ expr += `.array()`;
4118
+ return expr;
3765
4119
  }
3766
4120
  function pgTypeToBungresBuilderName(col2) {
3767
4121
  const dt = col2.dataType;
@@ -3807,14 +4161,14 @@ function pgTypeToBungresBuilderName(col2) {
3807
4161
  return "text";
3808
4162
  if (dt === "ARRAY") {
3809
4163
  if (col2.udtName === "_text")
3810
- return "textArray";
4164
+ return { base: "text", isArray: true };
3811
4165
  if (col2.udtName === "_int4" || col2.udtName === "_int8")
3812
- return "integerArray";
4166
+ return { base: "integer", isArray: true };
3813
4167
  if (col2.udtName === "_varchar")
3814
- return "varcharArray";
4168
+ return { base: "varchar", isArray: true };
3815
4169
  if (col2.udtName === "_uuid")
3816
- return "uuidArray";
3817
- return "textArray";
4170
+ return { base: "uuid", isArray: true };
4171
+ return { base: "text", isArray: true };
3818
4172
  }
3819
4173
  return "text";
3820
4174
  }
@@ -3828,16 +4182,16 @@ async function runStatus(config) {
3828
4182
  intro(import_picocolors4.default.bgCyan(import_picocolors4.default.black(" @bungres/kit status ")));
3829
4183
  const migrationsDir = resolve6(config.out);
3830
4184
  const sql2 = new Bun.SQL(config.dbUrl);
3831
- const table2 = config.migrationsTable;
4185
+ const table = config.migrationsTable;
3832
4186
  const schema = config.migrationsSchema;
3833
- const qualifiedTable = `"${schema}"."${table2}"`;
4187
+ const qualifiedTable = `"${schema}"."${table}"`;
3834
4188
  const s = spinner();
3835
4189
  s.start("Checking migration status...");
3836
4190
  try {
3837
4191
  const tableCheck = await sql2.unsafe(`SELECT EXISTS (
3838
4192
  SELECT 1 FROM information_schema.tables
3839
4193
  WHERE table_schema = $1 AND table_name = $2
3840
- ) AS exists`, [schema, table2]);
4194
+ ) AS exists`, [schema, table]);
3841
4195
  const trackingExists = tableCheck[0]?.exists ?? false;
3842
4196
  const glob = new Bun.Glob("*.sql");
3843
4197
  const files = [];
@@ -3872,6 +4226,7 @@ async function runStatus(config) {
3872
4226
  log.info(`${import_picocolors4.default.green(appliedSet.size.toString())} applied, ${import_picocolors4.default.yellow(pendingCount.toString())} pending.`);
3873
4227
  outro("Done.");
3874
4228
  } catch (err) {
4229
+ s.stop("Failed.");
3875
4230
  log.error(import_picocolors4.default.red(`Status check failed: ${err.message}`));
3876
4231
  outro("Failed.");
3877
4232
  } finally {
@@ -3882,23 +4237,14 @@ async function runStatus(config) {
3882
4237
  var import_picocolors5 = __toESM(require_picocolors(), 1);
3883
4238
  async function runDrop(config, opts = {}) {
3884
4239
  intro(import_picocolors5.default.bgCyan(import_picocolors5.default.black(" @bungres/kit drop ")));
3885
- const s = spinner();
3886
- s.start("Loading schemas...");
3887
- const schemas = await loadSchemas(config.schema);
3888
- if (schemas.length === 0) {
3889
- s.stop("No schemas found.");
3890
- log.warn(import_picocolors5.default.yellow("No table definitions found in schema files."));
3891
- outro("Failed.");
3892
- return;
3893
- }
3894
- s.stop(`Loaded ${schemas.length} schemas.`);
3895
4240
  const ms = spinner();
3896
- ms.start("Checking database tables...");
4241
+ ms.start("Scanning database for objects to drop...");
3897
4242
  const sql2 = new Bun.SQL(config.dbUrl);
3898
4243
  try {
3899
- const userSchema = config.dbSchema;
3900
- const existingUserTablesResult = await sql2.unsafe(`SELECT table_name FROM information_schema.tables WHERE table_schema = $1`, [userSchema]);
3901
- const existingTableNames = new Set(existingUserTablesResult.map((r2) => r2.table_name));
4244
+ const userSchema = config.dbSchema || "public";
4245
+ const existingViews = await sql2.unsafe(`SELECT table_name FROM information_schema.views WHERE table_schema = $1`, [userSchema]);
4246
+ const existingTables = await sql2.unsafe(`SELECT table_name FROM information_schema.tables WHERE table_schema = $1 AND table_type = 'BASE TABLE'`, [userSchema]);
4247
+ const existingTypes = await sql2.unsafe(`SELECT t.typname FROM pg_type t JOIN pg_namespace n ON n.oid = t.typnamespace WHERE n.nspname = $1 AND t.typtype = 'e'`, [userSchema]);
3902
4248
  const migTableCheck = await sql2.unsafe(`SELECT EXISTS (
3903
4249
  SELECT 1 FROM information_schema.tables
3904
4250
  WHERE table_schema = $1 AND table_name = $2
@@ -3909,29 +4255,20 @@ async function runDrop(config, opts = {}) {
3909
4255
  WHERE table_schema = $1 AND table_name = $2
3910
4256
  ) AS exists`, [config.migrationsSchema, "__bungres_push"]);
3911
4257
  const pushTableExists = pushTableCheck[0]?.exists ?? false;
3912
- const tablesToDrop = schemas.filter((sch) => existingTableNames.has(sch.config.name));
3913
- if (tablesToDrop.length === 0 && !migrationTableExists && !pushTableExists) {
4258
+ const totalObjects = existingViews.length + existingTables.length + existingTypes.length + (migrationTableExists ? 1 : 0) + (pushTableExists ? 1 : 0);
4259
+ if (totalObjects === 0) {
3914
4260
  ms.stop("Nothing to do.");
3915
- log.success(import_picocolors5.default.green("No tables to drop (they either don't exist or were already dropped)."));
4261
+ log.success(import_picocolors5.default.green("No items to drop (schema is empty)."));
3916
4262
  outro("Done.");
3917
4263
  return;
3918
4264
  }
3919
- ms.stop("Database check complete.");
3920
- const tableNamesToPrint = tablesToDrop.map((sch) => (sch.config.schema ? sch.config.schema + "." : "") + sch.config.name);
3921
- if (migrationTableExists) {
3922
- tableNamesToPrint.push(`${config.migrationsSchema}.${config.migrationsTable}`);
3923
- }
3924
- if (pushTableExists) {
3925
- tableNamesToPrint.push(`${config.migrationsSchema}.__bungres_push`);
3926
- }
4265
+ ms.stop("Database scan complete.");
3927
4266
  if (!opts.force) {
3928
4267
  log.warn(import_picocolors5.default.bgRed(import_picocolors5.default.white(" \u26A0\uFE0F WARNING ")));
3929
- log.message(import_picocolors5.default.bold(import_picocolors5.default.red("This will drop the following tables and ALL their data:")));
3930
- for (const name of tableNamesToPrint) {
3931
- log.info(import_picocolors5.default.yellow(` - ${name}`));
3932
- }
4268
+ log.message(import_picocolors5.default.bold(import_picocolors5.default.red("This will GLOBALLY drop all tables, views, and types in the database schema!")));
4269
+ log.info(import_picocolors5.default.yellow(`Found ${existingViews.length} views, ${existingTables.length} tables, ${existingTypes.length} enums.`));
3933
4270
  const confirm2 = await confirm({
3934
- message: "Are you sure you want to proceed?",
4271
+ message: "Are you absolutely sure you want to drop ALL data?",
3935
4272
  initialValue: false
3936
4273
  });
3937
4274
  if (isCancel(confirm2) || !confirm2) {
@@ -3940,24 +4277,36 @@ async function runDrop(config, opts = {}) {
3940
4277
  }
3941
4278
  }
3942
4279
  const exSpinner = spinner();
3943
- exSpinner.start("Dropping tables...");
3944
- for (const entry of tablesToDrop) {
3945
- const ddl = `DROP TABLE IF EXISTS "${entry.config.name}" CASCADE;`;
3946
- await sql2.unsafe(ddl);
3947
- log.step(import_picocolors5.default.gray(`Dropped ${entry.config.name}`));
4280
+ exSpinner.start("Dropping objects...");
4281
+ for (const v of existingViews) {
4282
+ await sql2.unsafe(`DROP VIEW IF EXISTS "${userSchema}"."${v.table_name}" CASCADE;`);
4283
+ if (opts.force !== true)
4284
+ log.step(import_picocolors5.default.gray(`Dropped view ${v.table_name}`));
4285
+ }
4286
+ for (const t2 of existingTables) {
4287
+ await sql2.unsafe(`DROP TABLE IF EXISTS "${userSchema}"."${t2.table_name}" CASCADE;`);
4288
+ if (opts.force !== true)
4289
+ log.step(import_picocolors5.default.gray(`Dropped table ${t2.table_name}`));
4290
+ }
4291
+ for (const type of existingTypes) {
4292
+ await sql2.unsafe(`DROP TYPE IF EXISTS "${userSchema}"."${type.typname}" CASCADE;`);
4293
+ if (opts.force !== true)
4294
+ log.step(import_picocolors5.default.gray(`Dropped enum ${type.typname}`));
3948
4295
  }
3949
4296
  if (migrationTableExists) {
3950
- const qualifiedMigTable = `"${config.migrationsSchema}"."${config.migrationsTable}"`;
3951
- await sql2.unsafe(`DROP TABLE IF EXISTS ${qualifiedMigTable} CASCADE`);
3952
- log.step(import_picocolors5.default.gray(`Dropped ${config.migrationsSchema}.${config.migrationsTable}`));
4297
+ await sql2.unsafe(`DROP TABLE IF EXISTS "${config.migrationsSchema}"."${config.migrationsTable}" CASCADE`);
4298
+ if (opts.force !== true)
4299
+ log.step(import_picocolors5.default.gray(`Dropped ${config.migrationsSchema}.${config.migrationsTable}`));
3953
4300
  }
3954
4301
  if (pushTableExists) {
3955
4302
  await sql2.unsafe(`DROP TABLE IF EXISTS "${config.migrationsSchema}"."__bungres_push" CASCADE`);
3956
- log.step(import_picocolors5.default.gray(`Dropped ${config.migrationsSchema}.__bungres_push`));
4303
+ if (opts.force !== true)
4304
+ log.step(import_picocolors5.default.gray(`Dropped ${config.migrationsSchema}.__bungres_push`));
3957
4305
  }
3958
- exSpinner.stop("Tables dropped.");
4306
+ exSpinner.stop("Items dropped.");
3959
4307
  outro(import_picocolors5.default.cyan("\u2728 Drop complete."));
3960
4308
  } catch (err) {
4309
+ ms.stop("Scan failed.");
3961
4310
  log.error(import_picocolors5.default.red(`Drop failed: ${err.message}`));
3962
4311
  outro("Failed.");
3963
4312
  } finally {