drixio 1.1.10 → 1.2.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
@@ -283,6 +283,23 @@ async function saveDatabaseUrl(url) {
283
283
  );
284
284
  }
285
285
  }
286
+ function assembleConnectionUrl(type, host, port, user, password, database) {
287
+ const targetHost = host || "localhost";
288
+ const targetPort = port || (type === "postgres" ? "5432" : "3306");
289
+ const targetUser = user || (type === "postgres" ? "postgres" : "root");
290
+ const auth = password ? `${encodeURIComponent(targetUser)}:${encodeURIComponent(password)}` : encodeURIComponent(targetUser);
291
+ const defaultDb = type === "postgres" ? "postgres" : "";
292
+ const dbNameStr = database || defaultDb;
293
+ return `${type}://${auth}@${targetHost}:${targetPort}/${dbNameStr}`;
294
+ }
295
+ function resolveLocalDbPath(targetUrl, cwd) {
296
+ const cleanPath = targetUrl.replace(/^file:/, "").trim();
297
+ return path.isAbsolute(cleanPath) ? cleanPath : path.resolve(cwd, cleanPath);
298
+ }
299
+ function isConnectionString(url) {
300
+ const trimmed = url.trim();
301
+ return trimmed.startsWith("postgres://") || trimmed.startsWith("postgresql://") || trimmed.startsWith("mysql://");
302
+ }
286
303
  var DB_ENV_KEYS;
287
304
  var init_loader = __esm({
288
305
  "src/logic/loader.ts"() {
@@ -307,11 +324,319 @@ var init_loader = __esm({
307
324
  }
308
325
  });
309
326
 
327
+ // src/logic/dialect.ts
328
+ function getDialect(type) {
329
+ switch (type) {
330
+ case "sqlite":
331
+ return new SqliteDialect();
332
+ case "postgres":
333
+ return new PostgresDialect();
334
+ case "mysql":
335
+ return new MysqlDialect();
336
+ default:
337
+ return new SqliteDialect();
338
+ }
339
+ }
340
+ function buildSearchWhereClause(adapter, dbType, columns, searchInput) {
341
+ const searchVal = (searchInput || "").trim();
342
+ if (!searchVal) return "";
343
+ const isSqlCondition = /[=<>]|LIKE|IN|AND|OR/i.test(searchVal);
344
+ if (isSqlCondition) {
345
+ return searchVal;
346
+ }
347
+ const dialect = getDialect(dbType);
348
+ const strCols = columns.filter((c) => {
349
+ const t = c.type.toLowerCase();
350
+ return t.includes("char") || t.includes("text") || t.includes("string") || t.includes("uuid");
351
+ });
352
+ if (strCols.length > 0) {
353
+ const likeOp = dbType === "postgres" ? "ILIKE" : "LIKE";
354
+ const escaped = dialect.escapeString(searchVal);
355
+ const conditions = strCols.map(
356
+ (c) => `${adapter.quoteIdentifier(c.name)} ${likeOp} '%${escaped}%'`
357
+ );
358
+ return conditions.join(" OR ");
359
+ }
360
+ if (columns.length > 0) {
361
+ const col = columns[0];
362
+ const escaped = dialect.escapeString(searchVal);
363
+ return `${adapter.quoteIdentifier(col.name)} = '${escaped}'`;
364
+ }
365
+ return "";
366
+ }
367
+ function formatSqlDefaultValue(rawDefault) {
368
+ if (rawDefault === void 0 || rawDefault === null) return null;
369
+ let trimmed = String(rawDefault).trim();
370
+ if (!trimmed || trimmed.toLowerCase() === "null" || trimmed === "-") {
371
+ return null;
372
+ }
373
+ while (trimmed.startsWith("''") && trimmed.endsWith("''") && trimmed.length > 4) {
374
+ trimmed = trimmed.slice(1, -1);
375
+ }
376
+ if (/^-?\d+(\.\d+)?$/.test(trimmed)) {
377
+ return trimmed;
378
+ }
379
+ const lower = trimmed.toLowerCase();
380
+ if (lower === "true" || lower === "false") {
381
+ return lower.toUpperCase();
382
+ }
383
+ const upper = trimmed.toUpperCase();
384
+ if (upper === "CURRENT_TIMESTAMP" || upper === "CURRENT_DATE" || upper === "CURRENT_TIME" || upper === "NOW()" || upper === "TIMESTAMP" || upper.startsWith("CURRENT_TIMESTAMP") || upper.startsWith("NOW()") || upper.startsWith("GEN_RANDOM_UUID(") || upper.startsWith("UUID_GENERATE_") || upper.startsWith("UUID(") || trimmed.startsWith("(") && trimmed.endsWith(")")) {
385
+ return upper === "TIMESTAMP" ? "CURRENT_TIMESTAMP" : trimmed;
386
+ }
387
+ const pgCastMatch = trimmed.match(/^('[\s\S]*')(?:::[\w\s()]+)$/);
388
+ if (pgCastMatch) {
389
+ return pgCastMatch[1];
390
+ }
391
+ if (trimmed.startsWith("'") && trimmed.endsWith("'") && trimmed.length >= 2) {
392
+ return trimmed;
393
+ }
394
+ if (trimmed.startsWith('"') && trimmed.endsWith('"') && trimmed.length >= 2) {
395
+ const inner = trimmed.slice(1, -1).replace(/'/g, "''");
396
+ return `'${inner}'`;
397
+ }
398
+ const escaped = trimmed.replace(/'/g, "''");
399
+ return `'${escaped}'`;
400
+ }
401
+ function generateExplainQuery(rawSql, dbType) {
402
+ let cleanSql = rawSql.replace(/--.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "").trim().replace(/;+$/, "").trim();
403
+ if (!cleanSql) {
404
+ return { sql: "", error: "SQL query is empty after stripping comments" };
405
+ }
406
+ if (/^(CREATE|DROP|ALTER|TRUNCATE)\s+/i.test(cleanSql)) {
407
+ return {
408
+ sql: "",
409
+ error: "DDL statements (CREATE, DROP, ALTER, TRUNCATE) do not have query execution plans to explain."
410
+ };
411
+ }
412
+ const hasUserExplain = /^EXPLAIN\b/i.test(cleanSql);
413
+ let targetSql = cleanSql;
414
+ if (hasUserExplain) {
415
+ targetSql = cleanSql.replace(/^EXPLAIN\s+(QUERY\s+PLAN\s+)?/i, "").replace(
416
+ /^\((ANALYZE|COSTS|VERBOSE|BUFFERS|FORMAT\s+\w+|,|\s)+\)\s*/i,
417
+ ""
418
+ ).trim();
419
+ }
420
+ let explainSql = "";
421
+ const isDmlWrite = /^(INSERT|UPDATE|DELETE)\s+/i.test(targetSql);
422
+ if (dbType === "sqlite") {
423
+ explainSql = `EXPLAIN QUERY PLAN ${targetSql}`;
424
+ } else if (dbType === "postgres") {
425
+ if (isDmlWrite) {
426
+ explainSql = `EXPLAIN (COSTS, VERBOSE) ${targetSql}`;
427
+ } else {
428
+ explainSql = `EXPLAIN (ANALYZE, COSTS, VERBOSE, BUFFERS) ${targetSql}`;
429
+ }
430
+ } else if (dbType === "mysql") {
431
+ explainSql = `EXPLAIN ${targetSql}`;
432
+ } else {
433
+ explainSql = `EXPLAIN ${targetSql}`;
434
+ }
435
+ return { sql: explainSql };
436
+ }
437
+ var SqliteDialect, PostgresDialect, MysqlDialect;
438
+ var init_dialect = __esm({
439
+ "src/logic/dialect.ts"() {
440
+ "use strict";
441
+ SqliteDialect = class {
442
+ quoteIdentifier(name) {
443
+ return `"${name}"`;
444
+ return `"${name.replace(/"/g, '""')}"`;
445
+ }
446
+ escapeString(val) {
447
+ return val.replace(/'/g, "''");
448
+ }
449
+ buildCreateTable(tableName, columns) {
450
+ const lines = columns.map((col) => {
451
+ const isPk = !!(col.isPk || col.primaryKey);
452
+ const tLower = col.type.toLowerCase();
453
+ let typeStr = col.type;
454
+ if (tLower === "integer" || tLower === "int") typeStr = "INTEGER";
455
+ else if (tLower === "text" || tLower === "string") typeStr = "TEXT";
456
+ else if (tLower === "boolean" || tLower === "bool")
457
+ typeStr = "BOOLEAN";
458
+ else if (tLower === "decimal" || tLower === "numeric" || tLower === "float" || tLower === "double")
459
+ typeStr = "REAL";
460
+ else if (tLower === "datetime" || tLower === "timestamp")
461
+ typeStr = "DATETIME";
462
+ else if (col.enumValues && col.enumValues.length > 0) {
463
+ typeStr = `TEXT CHECK(${this.quoteIdentifier(col.name)} IN (${col.enumValues.map((v) => `'${this.escapeString(v)}'`).join(", ")}))`;
464
+ }
465
+ if (isPk && (tLower.includes("int") || typeStr === "INTEGER")) {
466
+ typeStr = "INTEGER PRIMARY KEY AUTOINCREMENT";
467
+ } else if (isPk) {
468
+ typeStr += " PRIMARY KEY";
469
+ } else {
470
+ if (!col.nullable) typeStr += " NOT NULL";
471
+ if (col.isUnique) typeStr += " UNIQUE";
472
+ }
473
+ if (col.defaultValue && col.defaultValue !== "AutoInc" && !col.defaultValue.startsWith("FK ->")) {
474
+ const formatted = formatSqlDefaultValue(col.defaultValue);
475
+ if (formatted !== null) {
476
+ typeStr += ` DEFAULT ${formatted}`;
477
+ }
478
+ }
479
+ return ` ${this.quoteIdentifier(col.name)} ${typeStr}`;
480
+ });
481
+ const fks = columns.filter(
482
+ (col) => col.fkTarget && col.fkTarget.table && col.fkTarget.column
483
+ ).map((col) => {
484
+ let fkStr = ` FOREIGN KEY (${this.quoteIdentifier(col.name)}) REFERENCES ${this.quoteIdentifier(col.fkTarget.table)}(${this.quoteIdentifier(col.fkTarget.column)})`;
485
+ if (col.fkTarget.onDelete && col.fkTarget.onDelete !== "NO ACTION") {
486
+ fkStr += ` ON DELETE ${col.fkTarget.onDelete}`;
487
+ }
488
+ if (col.fkTarget.onUpdate && col.fkTarget.onUpdate !== "NO ACTION") {
489
+ fkStr += ` ON UPDATE ${col.fkTarget.onUpdate}`;
490
+ }
491
+ return fkStr;
492
+ });
493
+ if (fks.length > 0) {
494
+ lines.push(...fks);
495
+ }
496
+ return `CREATE TABLE ${this.quoteIdentifier(tableName)} (
497
+ ${lines.join(",\n")}
498
+ );`;
499
+ }
500
+ };
501
+ PostgresDialect = class {
502
+ quoteIdentifier(name) {
503
+ return `"${name}"`;
504
+ return `"${name.replace(/"/g, '""')}"`;
505
+ }
506
+ escapeString(val) {
507
+ return val.replace(/'/g, "''");
508
+ }
509
+ buildCreateTable(tableName, columns) {
510
+ const lines = columns.map((col) => {
511
+ const isPk = !!(col.isPk || col.primaryKey);
512
+ const tLower = col.type.toLowerCase();
513
+ let typeStr = col.type;
514
+ if (isPk && (tLower === "integer" || tLower === "int")) {
515
+ typeStr = "SERIAL PRIMARY KEY";
516
+ } else {
517
+ if (tLower === "integer" || tLower === "int") typeStr = "INTEGER";
518
+ else if (tLower === "text" || tLower === "string") typeStr = "TEXT";
519
+ else if (tLower === "boolean" || tLower === "bool")
520
+ typeStr = "BOOLEAN";
521
+ else if (tLower === "decimal" || tLower === "numeric")
522
+ typeStr = "NUMERIC";
523
+ else if (tLower === "datetime" || tLower === "timestamp")
524
+ typeStr = "TIMESTAMP";
525
+ else if (col.enumValues && col.enumValues.length > 0) {
526
+ if (col.type && ![
527
+ "enum",
528
+ "varchar",
529
+ "varchar(255)",
530
+ "text",
531
+ "string"
532
+ ].includes(tLower)) {
533
+ typeStr = this.quoteIdentifier(col.type);
534
+ } else {
535
+ typeStr = `VARCHAR(255) CHECK(${this.quoteIdentifier(col.name)} IN (${col.enumValues.map((v) => `'${this.escapeString(v)}'`).join(", ")}))`;
536
+ }
537
+ }
538
+ if (isPk) typeStr += " PRIMARY KEY";
539
+ if (!col.nullable && !isPk) typeStr += " NOT NULL";
540
+ if (col.isUnique && !isPk) typeStr += " UNIQUE";
541
+ }
542
+ if (col.defaultValue && col.defaultValue !== "AutoInc" && !col.defaultValue.startsWith("FK ->")) {
543
+ const formatted = formatSqlDefaultValue(col.defaultValue);
544
+ if (formatted !== null) {
545
+ typeStr += ` DEFAULT ${formatted}`;
546
+ }
547
+ }
548
+ return ` ${this.quoteIdentifier(col.name)} ${typeStr}`;
549
+ });
550
+ const fks = columns.filter(
551
+ (col) => col.fkTarget && col.fkTarget.table && col.fkTarget.column
552
+ ).map((col) => {
553
+ let fkStr = ` FOREIGN KEY (${this.quoteIdentifier(col.name)}) REFERENCES ${this.quoteIdentifier(col.fkTarget.table)}(${this.quoteIdentifier(col.fkTarget.column)})`;
554
+ if (col.fkTarget.onDelete && col.fkTarget.onDelete !== "NO ACTION") {
555
+ fkStr += ` ON DELETE ${col.fkTarget.onDelete}`;
556
+ }
557
+ if (col.fkTarget.onUpdate && col.fkTarget.onUpdate !== "NO ACTION") {
558
+ fkStr += ` ON UPDATE ${col.fkTarget.onUpdate}`;
559
+ }
560
+ return fkStr;
561
+ });
562
+ if (fks.length > 0) {
563
+ lines.push(...fks);
564
+ }
565
+ return `CREATE TABLE ${this.quoteIdentifier(tableName)} (
566
+ ${lines.join(",\n")}
567
+ );`;
568
+ }
569
+ };
570
+ MysqlDialect = class {
571
+ quoteIdentifier(name) {
572
+ return `\`${name}\``;
573
+ return `\`${name.replace(/`/g, "``")}\``;
574
+ }
575
+ escapeString(val) {
576
+ return val.replace(/'/g, "''");
577
+ return val.replace(/\\/g, "\\\\").replace(/'/g, "''");
578
+ }
579
+ buildCreateTable(tableName, columns) {
580
+ const lines = columns.map((col) => {
581
+ const isPk = !!(col.isPk || col.primaryKey);
582
+ const tLower = col.type.toLowerCase();
583
+ let typeStr = col.type;
584
+ if (tLower === "integer" || tLower === "int") typeStr = "INT";
585
+ else if (tLower === "text" || tLower === "string")
586
+ typeStr = "VARCHAR(255)";
587
+ else if (tLower === "boolean" || tLower === "bool")
588
+ typeStr = "BOOLEAN";
589
+ else if (tLower === "decimal" || tLower === "numeric")
590
+ typeStr = "DOUBLE";
591
+ else if (tLower === "datetime" || tLower === "timestamp")
592
+ typeStr = "DATETIME";
593
+ else if (col.enumValues && col.enumValues.length > 0) {
594
+ typeStr = `ENUM(${col.enumValues.map((v) => `'${this.escapeString(v)}'`).join(", ")})`;
595
+ }
596
+ if (isPk && (tLower.includes("int") || typeStr === "INT")) {
597
+ typeStr += " AUTO_INCREMENT PRIMARY KEY";
598
+ } else if (isPk) {
599
+ typeStr += " PRIMARY KEY";
600
+ }
601
+ if (!col.nullable && !isPk) typeStr += " NOT NULL";
602
+ if (col.isUnique && !isPk) typeStr += " UNIQUE";
603
+ if (col.defaultValue && col.defaultValue !== "AutoInc" && !col.defaultValue.startsWith("FK ->")) {
604
+ const formatted = formatSqlDefaultValue(col.defaultValue);
605
+ if (formatted !== null) {
606
+ typeStr += ` DEFAULT ${formatted}`;
607
+ }
608
+ }
609
+ return ` ${this.quoteIdentifier(col.name)} ${typeStr}`;
610
+ });
611
+ const fks = columns.filter(
612
+ (col) => col.fkTarget && col.fkTarget.table && col.fkTarget.column
613
+ ).map((col) => {
614
+ let fkStr = ` FOREIGN KEY (${this.quoteIdentifier(col.name)}) REFERENCES ${this.quoteIdentifier(col.fkTarget.table)}(${this.quoteIdentifier(col.fkTarget.column)})`;
615
+ if (col.fkTarget.onDelete && col.fkTarget.onDelete !== "NO ACTION") {
616
+ fkStr += ` ON DELETE ${col.fkTarget.onDelete}`;
617
+ }
618
+ if (col.fkTarget.onUpdate && col.fkTarget.onUpdate !== "NO ACTION") {
619
+ fkStr += ` ON UPDATE ${col.fkTarget.onUpdate}`;
620
+ }
621
+ return fkStr;
622
+ });
623
+ if (fks.length > 0) {
624
+ lines.push(...fks);
625
+ }
626
+ return `CREATE TABLE ${this.quoteIdentifier(tableName)} (
627
+ ${lines.join(",\n")}
628
+ );`;
629
+ }
630
+ };
631
+ }
632
+ });
633
+
310
634
  // src/logic/adapters/sqlite.ts
311
635
  var SqliteAdapter;
312
636
  var init_sqlite = __esm({
313
637
  "src/logic/adapters/sqlite.ts"() {
314
638
  "use strict";
639
+ init_dialect();
315
640
  SqliteAdapter = class {
316
641
  dbPath;
317
642
  db = null;
@@ -589,15 +914,21 @@ var init_sqlite = __esm({
589
914
  const isCompositePk = pkColumns.length > 1;
590
915
  const colDefs = [];
591
916
  for (const col of newColumns) {
592
- let def = `${this.quoteIdentifier(col.name)} ${col.type || "TEXT"}`;
917
+ let colType = col.type || "TEXT";
918
+ if (col.enumValues && col.enumValues.length > 0) {
919
+ const vals = col.enumValues.map((v) => `'${v.replace(/'/g, "''")}'`).join(", ");
920
+ colType = `TEXT CHECK(${this.quoteIdentifier(col.name)} IN (${vals}))`;
921
+ }
922
+ let def = `${this.quoteIdentifier(col.name)} ${colType}`;
593
923
  if (col.isPk && !isCompositePk) {
594
924
  def += " PRIMARY KEY";
595
925
  }
596
926
  if (!col.nullable && (!col.isPk || isCompositePk)) {
597
927
  def += " NOT NULL";
598
928
  }
599
- if (col.defaultValue !== void 0 && col.defaultValue !== null && col.defaultValue !== "") {
600
- def += ` DEFAULT '${String(col.defaultValue).replace(/'/g, "''")}'`;
929
+ const formattedDef = formatSqlDefaultValue(col.defaultValue);
930
+ if (formattedDef !== null) {
931
+ def += ` DEFAULT ${formattedDef}`;
601
932
  }
602
933
  if (col.fkTarget && col.fkTarget.table && col.fkTarget.column) {
603
934
  def += ` REFERENCES ${this.quoteIdentifier(col.fkTarget.table)}(${this.quoteIdentifier(col.fkTarget.column)})`;
@@ -704,17 +1035,45 @@ var PostgresAdapter;
704
1035
  var init_postgres = __esm({
705
1036
  "src/logic/adapters/postgres.ts"() {
706
1037
  "use strict";
707
- PostgresAdapter = class {
1038
+ PostgresAdapter = class _PostgresAdapter {
708
1039
  connectionString;
709
1040
  pool = null;
1041
+ currentSchema;
710
1042
  constructor(connection) {
711
1043
  this.connectionString = connection;
1044
+ this.currentSchema = _PostgresAdapter.extractSchema(connection);
1045
+ }
1046
+ /**
1047
+ * Extract the schema parameter from a PostgreSQL connection URL.
1048
+ * Supports both `?schema=xxx` (Prisma-style) and `?options=-c search_path%3Dxxx`.
1049
+ * Defaults to 'public' if not found.
1050
+ */
1051
+ static extractSchema(url) {
1052
+ try {
1053
+ const qIdx = url.indexOf("?");
1054
+ if (qIdx === -1) return "public";
1055
+ const params = new URLSearchParams(url.slice(qIdx + 1));
1056
+ const schema = params.get("schema");
1057
+ if (schema) return schema;
1058
+ const opts = params.get("options");
1059
+ if (opts) {
1060
+ const m = opts.match(/search_path[=](\w+)/);
1061
+ if (m) return m[1];
1062
+ }
1063
+ } catch {
1064
+ }
1065
+ return "public";
712
1066
  }
713
1067
  getPool() {
714
1068
  if (!this.pool) {
715
1069
  this.pool = new pg.Pool(buildPgPoolConfig(this.connectionString));
716
1070
  this.pool.on("error", () => {
717
1071
  });
1072
+ this.pool.on("connect", (client) => {
1073
+ const quoted = this.currentSchema.replace(/"/g, '""');
1074
+ client.query(`SET search_path TO "${quoted}", public`).catch(() => {
1075
+ });
1076
+ });
718
1077
  }
719
1078
  return this.pool;
720
1079
  }
@@ -775,15 +1134,15 @@ var init_postgres = __esm({
775
1134
  const query = `
776
1135
  SELECT tablename
777
1136
  FROM pg_catalog.pg_tables
778
- WHERE schemaname = 'public'
1137
+ WHERE schemaname = $1
779
1138
  ORDER BY tablename;
780
1139
  `;
781
- const res = await this.getPool().query(query);
1140
+ const res = await this.getPool().query(query, [this.currentSchema]);
782
1141
  return res.rows.map((row) => row.tablename);
783
1142
  }
784
1143
  async getSchema(tableName) {
785
1144
  const query = `
786
- SELECT c.column_name, c.data_type, c.is_nullable, c.column_default,
1145
+ SELECT c.column_name, c.data_type, c.udt_name, c.is_nullable, c.column_default,
787
1146
  (SELECT count(*)
788
1147
  FROM information_schema.key_column_usage kcu
789
1148
  JOIN information_schema.table_constraints tc
@@ -841,11 +1200,44 @@ var init_postgres = __esm({
841
1200
  AND kcu.column_name = c.column_name
842
1201
  LIMIT 1) as fk_on_update
843
1202
  FROM information_schema.columns c
844
- WHERE c.table_name = $1 AND c.table_schema = 'public'
1203
+ WHERE c.table_name = $1 AND c.table_schema = $2
845
1204
  ORDER BY c.ordinal_position;
846
1205
  `;
847
- const res = await this.getPool().query(query, [tableName]);
1206
+ const res = await this.getPool().query(query, [
1207
+ tableName,
1208
+ this.currentSchema
1209
+ ]);
848
1210
  const enumMap = /* @__PURE__ */ new Map();
1211
+ try {
1212
+ const nativeEnumQuery = `
1213
+ SELECT
1214
+ a.attname AS column_name,
1215
+ t.typname AS enum_name,
1216
+ e.enumlabel AS enum_value
1217
+ FROM pg_attribute a
1218
+ JOIN pg_class c ON a.attrelid = c.oid
1219
+ JOIN pg_namespace nc ON nc.oid = c.relnamespace
1220
+ JOIN pg_type t ON a.atttypid = t.oid
1221
+ JOIN pg_enum e ON t.oid = e.enumtypid
1222
+ WHERE c.relname = $1
1223
+ AND nc.nspname = $2
1224
+ AND a.attnum > 0
1225
+ AND NOT a.attisdropped
1226
+ ORDER BY a.attnum, e.enumsortorder;
1227
+ `;
1228
+ const nativeEnumRes = await this.getPool().query(nativeEnumQuery, [
1229
+ tableName,
1230
+ this.currentSchema
1231
+ ]);
1232
+ for (const r of nativeEnumRes.rows) {
1233
+ const colName = r.column_name;
1234
+ if (!enumMap.has(colName)) {
1235
+ enumMap.set(colName, []);
1236
+ }
1237
+ enumMap.get(colName).push(r.enum_value);
1238
+ }
1239
+ } catch {
1240
+ }
849
1241
  try {
850
1242
  const checkQuery = `
851
1243
  SELECT cc.check_clause
@@ -853,16 +1245,19 @@ var init_postgres = __esm({
853
1245
  JOIN information_schema.table_constraints tc
854
1246
  ON cc.constraint_name = tc.constraint_name
855
1247
  AND cc.constraint_schema = tc.constraint_schema
856
- WHERE tc.table_name = $1 AND tc.table_schema = 'public';
1248
+ WHERE tc.table_name = $1 AND tc.table_schema = $2;
857
1249
  `;
858
- const checkRes = await this.getPool().query(checkQuery, [tableName]);
1250
+ const checkRes = await this.getPool().query(checkQuery, [
1251
+ tableName,
1252
+ this.currentSchema
1253
+ ]);
859
1254
  for (const r of checkRes.rows) {
860
1255
  const inRegex = /["'`]?(\w+)["'`]?\s+(?:COLLATE\s+\w+\s+)?IN\s*\(([^)]+)\)/gi;
861
1256
  let inMatch;
862
1257
  while ((inMatch = inRegex.exec(r.check_clause)) !== null) {
863
1258
  const colName = inMatch[1];
864
1259
  const values = inMatch[2].split(",").map((s) => s.trim().replace(/^['"`]|['"`]$/g, "")).filter((s) => s.length > 0);
865
- if (values.length > 0) {
1260
+ if (values.length > 0 && !enumMap.has(colName)) {
866
1261
  enumMap.set(colName, values);
867
1262
  }
868
1263
  }
@@ -880,13 +1275,22 @@ var init_postgres = __esm({
880
1275
  onUpdate: col.fk_on_update && col.fk_on_update.toUpperCase() !== "NO ACTION" ? col.fk_on_update.toUpperCase() : void 0
881
1276
  };
882
1277
  }
1278
+ let colType = col.data_type;
1279
+ if (col.data_type === "USER-DEFINED" && col.udt_name) {
1280
+ colType = col.udt_name;
1281
+ }
883
1282
  return {
884
1283
  name: col.column_name,
885
- type: col.data_type,
1284
+ type: colType,
886
1285
  isPk: parseInt(col.is_pk) > 0,
887
1286
  nullable: parseInt(col.is_pk) > 0 ? false : col.is_nullable === "YES",
888
1287
  isUnique: parseInt(col.is_pk) > 0 || parseInt(col.is_unique) > 0,
889
- defaultValue: col.column_default != null ? String(col.column_default) : void 0,
1288
+ defaultValue: (() => {
1289
+ if (col.column_default == null) return void 0;
1290
+ const raw = String(col.column_default).trim();
1291
+ const castMatch = raw.match(/^('[\s\S]*')(?:::[\w\s()]+)$/);
1292
+ return castMatch ? castMatch[1] : raw;
1293
+ })(),
890
1294
  enumValues: enumMap.get(col.column_name),
891
1295
  fkTarget
892
1296
  };
@@ -900,22 +1304,22 @@ var init_postgres = __esm({
900
1304
  ix.indisunique as is_unique,
901
1305
  ix.indisprimary as is_primary
902
1306
  FROM
903
- pg_class t,
904
- pg_class i,
905
- pg_index ix,
906
- pg_attribute a
1307
+ pg_class t
1308
+ JOIN pg_namespace n ON n.oid = t.relnamespace
1309
+ JOIN pg_index ix ON t.oid = ix.indrelid
1310
+ JOIN pg_class i ON i.oid = ix.indexrelid
1311
+ JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = ANY(ix.indkey)
907
1312
  WHERE
908
- t.oid = ix.indrelid
909
- AND i.oid = ix.indexrelid
910
- AND a.attrelid = t.oid
911
- AND a.attnum = ANY(ix.indkey)
912
- AND t.relkind = 'r'
1313
+ t.relkind = 'r'
913
1314
  AND t.relname = $1
914
- AND t.relnamespace = 'public'::regnamespace
1315
+ AND n.nspname = $2
915
1316
  ORDER BY
916
1317
  i.relname, array_position(ix.indkey, a.attnum);
917
1318
  `;
918
- const res = await this.getPool().query(query, [tableName]);
1319
+ const res = await this.getPool().query(query, [
1320
+ tableName,
1321
+ this.currentSchema
1322
+ ]);
919
1323
  const rows = res.rows;
920
1324
  const indexMap = /* @__PURE__ */ new Map();
921
1325
  for (const row of rows) {
@@ -935,7 +1339,7 @@ var init_postgres = __esm({
935
1339
  async getData(tableName, limit = 50, offset = 0, whereClause, orderBy) {
936
1340
  const schema = await this.getSchema(tableName);
937
1341
  const columns = schema.map((col) => col.name);
938
- let sql = `SELECT * FROM ${this.quoteIdentifier(tableName)}`;
1342
+ let sql = `SELECT * FROM ${this.quoteTable(tableName)}`;
939
1343
  if (whereClause) {
940
1344
  sql += ` WHERE ${whereClause}`;
941
1345
  }
@@ -977,7 +1381,7 @@ var init_postgres = __esm({
977
1381
  await client.query("BEGIN");
978
1382
  for (const row of rows) {
979
1383
  const placeholders = cols.map((_, i) => `$${i + 1}`).join(", ");
980
- const sql = `INSERT INTO ${this.quoteIdentifier(tableName)} (${colsQuoted}) VALUES (${placeholders})`;
1384
+ const sql = `INSERT INTO ${this.quoteTable(tableName)} (${colsQuoted}) VALUES (${placeholders})`;
981
1385
  const values = cols.map((c) => row[c]);
982
1386
  await client.query(sql, values);
983
1387
  }
@@ -990,11 +1394,82 @@ var init_postgres = __esm({
990
1394
  }
991
1395
  }
992
1396
  async truncateTable(tableName) {
993
- const quoted = this.quoteIdentifier(tableName);
1397
+ const quoted = this.quoteTable(tableName);
994
1398
  await this.getPool().query(
995
1399
  `TRUNCATE TABLE ${quoted} RESTART IDENTITY CASCADE;`
996
1400
  );
997
1401
  }
1402
+ /**
1403
+ * Quote a table name with schema prefix, e.g. "zen_stream"."users".
1404
+ * When the schema is 'public', only the table name is quoted for simplicity.
1405
+ */
1406
+ quoteTable(tableName) {
1407
+ if (this.currentSchema === "public") {
1408
+ return this.quoteIdentifier(tableName);
1409
+ }
1410
+ return `${this.quoteIdentifier(this.currentSchema)}.${this.quoteIdentifier(tableName)}`;
1411
+ }
1412
+ /**
1413
+ * List all user-accessible schemas (excluding internal PostgreSQL schemas).
1414
+ */
1415
+ async getSchemas() {
1416
+ const query = `
1417
+ SELECT schema_name
1418
+ FROM information_schema.schemata
1419
+ WHERE schema_name NOT IN ('pg_catalog', 'information_schema', 'pg_toast')
1420
+ AND schema_name NOT LIKE 'pg_temp_%'
1421
+ AND schema_name NOT LIKE 'pg_toast_temp_%'
1422
+ ORDER BY schema_name;
1423
+ `;
1424
+ const res = await this.getPool().query(query);
1425
+ return res.rows.map((row) => row.schema_name);
1426
+ }
1427
+ getCurrentSchema() {
1428
+ return this.currentSchema;
1429
+ }
1430
+ async setSchema(schema) {
1431
+ this.currentSchema = schema;
1432
+ if (this.pool) {
1433
+ await this.pool.end();
1434
+ this.pool = null;
1435
+ }
1436
+ }
1437
+ /**
1438
+ * List all user-defined enum types in the current schema or public.
1439
+ * Prioritizes enums in the current active schema over public.
1440
+ */
1441
+ async getCustomEnums() {
1442
+ const query = `
1443
+ SELECT
1444
+ t.typname AS name,
1445
+ n.nspname AS schema_name,
1446
+ array_agg(e.enumlabel ORDER BY e.enumsortorder) AS values
1447
+ FROM pg_type t
1448
+ JOIN pg_enum e ON t.oid = e.enumtypid
1449
+ JOIN pg_namespace n ON n.oid = t.typnamespace
1450
+ WHERE n.nspname = $1 OR n.nspname = 'public'
1451
+ GROUP BY t.oid, t.typname, n.nspname
1452
+ ORDER BY (CASE WHEN n.nspname = $1 THEN 0 ELSE 1 END), t.typname;
1453
+ `;
1454
+ try {
1455
+ const res = await this.getPool().query(query, [this.currentSchema]);
1456
+ const seen = /* @__PURE__ */ new Set();
1457
+ const result = [];
1458
+ for (const row of res.rows) {
1459
+ const lower = row.name.toLowerCase();
1460
+ if (!seen.has(lower)) {
1461
+ seen.add(lower);
1462
+ result.push({
1463
+ name: row.name,
1464
+ values: Array.isArray(row.values) ? row.values : []
1465
+ });
1466
+ }
1467
+ }
1468
+ return result;
1469
+ } catch {
1470
+ return [];
1471
+ }
1472
+ }
998
1473
  };
999
1474
  }
1000
1475
  });
@@ -1265,245 +1740,20 @@ var init_adapters = __esm({
1265
1740
  // src/logic/factory.ts
1266
1741
  function createDBAdapter(config) {
1267
1742
  switch (config.type) {
1268
- case "sqlite":
1269
- return new SqliteAdapter(config.targetUrl);
1270
- case "postgres":
1271
- return new PostgresAdapter(config.targetUrl);
1272
- case "mysql":
1273
- return new MysqlAdapter(config.targetUrl);
1274
- default:
1275
- throw new Error(`Unsupported database type: ${config.type}`);
1276
- }
1277
- }
1278
- var init_factory = __esm({
1279
- "src/logic/factory.ts"() {
1280
- "use strict";
1281
- init_adapters();
1282
- }
1283
- });
1284
-
1285
- // src/logic/dialect.ts
1286
- function getDialect(type) {
1287
- switch (type) {
1288
- case "sqlite":
1289
- return new SqliteDialect();
1290
- case "postgres":
1291
- return new PostgresDialect();
1292
- case "mysql":
1293
- return new MysqlDialect();
1294
- default:
1295
- return new SqliteDialect();
1296
- }
1297
- }
1298
- function buildSearchWhereClause(adapter, dbType, columns, searchInput) {
1299
- const searchVal = (searchInput || "").trim();
1300
- if (!searchVal) return "";
1301
- const isSqlCondition = /[=<>]|LIKE|IN|AND|OR/i.test(searchVal);
1302
- if (isSqlCondition) {
1303
- return searchVal;
1304
- }
1305
- const strCols = columns.filter((c) => {
1306
- const t = c.type.toLowerCase();
1307
- return t.includes("char") || t.includes("text") || t.includes("string") || t.includes("uuid");
1308
- });
1309
- if (strCols.length > 0) {
1310
- const likeOp = dbType === "postgres" ? "ILIKE" : "LIKE";
1311
- const escaped = searchVal.replace(/'/g, "''");
1312
- const conditions = strCols.map(
1313
- (c) => `${adapter.quoteIdentifier(c.name)} ${likeOp} '%${escaped}%'`
1314
- );
1315
- return conditions.join(" OR ");
1316
- }
1317
- if (columns.length > 0) {
1318
- const col = columns[0];
1319
- const escaped = searchVal.replace(/'/g, "''");
1320
- return `${adapter.quoteIdentifier(col.name)} = '${escaped}'`;
1321
- }
1322
- return "";
1323
- }
1324
- var SqliteDialect, PostgresDialect, MysqlDialect;
1325
- var init_dialect = __esm({
1326
- "src/logic/dialect.ts"() {
1327
- "use strict";
1328
- SqliteDialect = class {
1329
- quoteIdentifier(name) {
1330
- return `"${name}"`;
1331
- }
1332
- escapeString(val) {
1333
- return val.replace(/'/g, "''");
1334
- }
1335
- buildCreateTable(tableName, columns) {
1336
- const lines = columns.map((col) => {
1337
- const isPk = !!(col.isPk || col.primaryKey);
1338
- const tLower = col.type.toLowerCase();
1339
- let typeStr = col.type;
1340
- if (tLower === "integer" || tLower === "int") typeStr = "INTEGER";
1341
- else if (tLower === "text" || tLower === "string") typeStr = "TEXT";
1342
- else if (tLower === "boolean" || tLower === "bool")
1343
- typeStr = "BOOLEAN";
1344
- else if (tLower === "decimal" || tLower === "numeric" || tLower === "float" || tLower === "double")
1345
- typeStr = "REAL";
1346
- else if (tLower === "datetime" || tLower === "timestamp")
1347
- typeStr = "DATETIME";
1348
- else if (col.enumValues && col.enumValues.length > 0) {
1349
- typeStr = `TEXT CHECK(${this.quoteIdentifier(col.name)} IN (${col.enumValues.map((v) => `'${this.escapeString(v)}'`).join(", ")}))`;
1350
- }
1351
- if (isPk && (tLower.includes("int") || typeStr === "INTEGER")) {
1352
- typeStr = "INTEGER PRIMARY KEY AUTOINCREMENT";
1353
- } else if (isPk) {
1354
- typeStr += " PRIMARY KEY";
1355
- } else {
1356
- if (!col.nullable) typeStr += " NOT NULL";
1357
- if (col.isUnique) typeStr += " UNIQUE";
1358
- }
1359
- if (col.defaultValue && col.defaultValue !== "AutoInc" && !col.defaultValue.startsWith("FK ->")) {
1360
- if (col.defaultValue === "Timestamp") {
1361
- typeStr += " DEFAULT CURRENT_TIMESTAMP";
1362
- } else {
1363
- typeStr += ` DEFAULT ${col.defaultValue}`;
1364
- }
1365
- }
1366
- return ` ${this.quoteIdentifier(col.name)} ${typeStr}`;
1367
- });
1368
- const fks = columns.filter(
1369
- (col) => col.fkTarget && col.fkTarget.table && col.fkTarget.column
1370
- ).map((col) => {
1371
- let fkStr = ` FOREIGN KEY (${this.quoteIdentifier(col.name)}) REFERENCES ${this.quoteIdentifier(col.fkTarget.table)}(${this.quoteIdentifier(col.fkTarget.column)})`;
1372
- if (col.fkTarget.onDelete && col.fkTarget.onDelete !== "NO ACTION") {
1373
- fkStr += ` ON DELETE ${col.fkTarget.onDelete}`;
1374
- }
1375
- if (col.fkTarget.onUpdate && col.fkTarget.onUpdate !== "NO ACTION") {
1376
- fkStr += ` ON UPDATE ${col.fkTarget.onUpdate}`;
1377
- }
1378
- return fkStr;
1379
- });
1380
- if (fks.length > 0) {
1381
- lines.push(...fks);
1382
- }
1383
- return `CREATE TABLE ${this.quoteIdentifier(tableName)} (
1384
- ${lines.join(",\n")}
1385
- );`;
1386
- }
1387
- };
1388
- PostgresDialect = class {
1389
- quoteIdentifier(name) {
1390
- return `"${name}"`;
1391
- }
1392
- escapeString(val) {
1393
- return val.replace(/'/g, "''");
1394
- }
1395
- buildCreateTable(tableName, columns) {
1396
- const lines = columns.map((col) => {
1397
- const isPk = !!(col.isPk || col.primaryKey);
1398
- const tLower = col.type.toLowerCase();
1399
- let typeStr = col.type;
1400
- if (isPk && (tLower === "integer" || tLower === "int")) {
1401
- typeStr = "SERIAL PRIMARY KEY";
1402
- } else {
1403
- if (tLower === "integer" || tLower === "int") typeStr = "INTEGER";
1404
- else if (tLower === "text" || tLower === "string") typeStr = "TEXT";
1405
- else if (tLower === "boolean" || tLower === "bool")
1406
- typeStr = "BOOLEAN";
1407
- else if (tLower === "decimal" || tLower === "numeric")
1408
- typeStr = "NUMERIC";
1409
- else if (tLower === "datetime" || tLower === "timestamp")
1410
- typeStr = "TIMESTAMP";
1411
- else if (col.enumValues && col.enumValues.length > 0) {
1412
- typeStr = `VARCHAR(255) CHECK(${this.quoteIdentifier(col.name)} IN (${col.enumValues.map((v) => `'${this.escapeString(v)}'`).join(", ")}))`;
1413
- }
1414
- if (isPk) typeStr += " PRIMARY KEY";
1415
- if (!col.nullable && !isPk) typeStr += " NOT NULL";
1416
- if (col.isUnique && !isPk) typeStr += " UNIQUE";
1417
- }
1418
- if (col.defaultValue && col.defaultValue !== "AutoInc" && !col.defaultValue.startsWith("FK ->")) {
1419
- if (col.defaultValue === "Timestamp") {
1420
- typeStr += " DEFAULT CURRENT_TIMESTAMP";
1421
- } else {
1422
- typeStr += ` DEFAULT ${col.defaultValue}`;
1423
- }
1424
- }
1425
- return ` ${this.quoteIdentifier(col.name)} ${typeStr}`;
1426
- });
1427
- const fks = columns.filter(
1428
- (col) => col.fkTarget && col.fkTarget.table && col.fkTarget.column
1429
- ).map((col) => {
1430
- let fkStr = ` FOREIGN KEY (${this.quoteIdentifier(col.name)}) REFERENCES ${this.quoteIdentifier(col.fkTarget.table)}(${this.quoteIdentifier(col.fkTarget.column)})`;
1431
- if (col.fkTarget.onDelete && col.fkTarget.onDelete !== "NO ACTION") {
1432
- fkStr += ` ON DELETE ${col.fkTarget.onDelete}`;
1433
- }
1434
- if (col.fkTarget.onUpdate && col.fkTarget.onUpdate !== "NO ACTION") {
1435
- fkStr += ` ON UPDATE ${col.fkTarget.onUpdate}`;
1436
- }
1437
- return fkStr;
1438
- });
1439
- if (fks.length > 0) {
1440
- lines.push(...fks);
1441
- }
1442
- return `CREATE TABLE ${this.quoteIdentifier(tableName)} (
1443
- ${lines.join(",\n")}
1444
- );`;
1445
- }
1446
- };
1447
- MysqlDialect = class {
1448
- quoteIdentifier(name) {
1449
- return `\`${name}\``;
1450
- }
1451
- escapeString(val) {
1452
- return val.replace(/'/g, "''");
1453
- }
1454
- buildCreateTable(tableName, columns) {
1455
- const lines = columns.map((col) => {
1456
- const isPk = !!(col.isPk || col.primaryKey);
1457
- const tLower = col.type.toLowerCase();
1458
- let typeStr = col.type;
1459
- if (tLower === "integer" || tLower === "int") typeStr = "INT";
1460
- else if (tLower === "text" || tLower === "string")
1461
- typeStr = "VARCHAR(255)";
1462
- else if (tLower === "boolean" || tLower === "bool")
1463
- typeStr = "BOOLEAN";
1464
- else if (tLower === "decimal" || tLower === "numeric")
1465
- typeStr = "DOUBLE";
1466
- else if (tLower === "datetime" || tLower === "timestamp")
1467
- typeStr = "DATETIME";
1468
- else if (col.enumValues && col.enumValues.length > 0) {
1469
- typeStr = `VARCHAR(255) CHECK(${this.quoteIdentifier(col.name)} IN (${col.enumValues.map((v) => `'${this.escapeString(v)}'`).join(", ")}))`;
1470
- }
1471
- if (isPk && (tLower.includes("int") || typeStr === "INT")) {
1472
- typeStr += " AUTO_INCREMENT PRIMARY KEY";
1473
- } else if (isPk) {
1474
- typeStr += " PRIMARY KEY";
1475
- }
1476
- if (!col.nullable && !isPk) typeStr += " NOT NULL";
1477
- if (col.isUnique && !isPk) typeStr += " UNIQUE";
1478
- if (col.defaultValue && col.defaultValue !== "AutoInc" && !col.defaultValue.startsWith("FK ->")) {
1479
- if (col.defaultValue === "Timestamp") {
1480
- typeStr += " DEFAULT CURRENT_TIMESTAMP";
1481
- } else {
1482
- typeStr += ` DEFAULT ${col.defaultValue}`;
1483
- }
1484
- }
1485
- return ` ${this.quoteIdentifier(col.name)} ${typeStr}`;
1486
- });
1487
- const fks = columns.filter(
1488
- (col) => col.fkTarget && col.fkTarget.table && col.fkTarget.column
1489
- ).map((col) => {
1490
- let fkStr = ` FOREIGN KEY (${this.quoteIdentifier(col.name)}) REFERENCES ${this.quoteIdentifier(col.fkTarget.table)}(${this.quoteIdentifier(col.fkTarget.column)})`;
1491
- if (col.fkTarget.onDelete && col.fkTarget.onDelete !== "NO ACTION") {
1492
- fkStr += ` ON DELETE ${col.fkTarget.onDelete}`;
1493
- }
1494
- if (col.fkTarget.onUpdate && col.fkTarget.onUpdate !== "NO ACTION") {
1495
- fkStr += ` ON UPDATE ${col.fkTarget.onUpdate}`;
1496
- }
1497
- return fkStr;
1498
- });
1499
- if (fks.length > 0) {
1500
- lines.push(...fks);
1501
- }
1502
- return `CREATE TABLE ${this.quoteIdentifier(tableName)} (
1503
- ${lines.join(",\n")}
1504
- );`;
1505
- }
1506
- };
1743
+ case "sqlite":
1744
+ return new SqliteAdapter(config.targetUrl);
1745
+ case "postgres":
1746
+ return new PostgresAdapter(config.targetUrl);
1747
+ case "mysql":
1748
+ return new MysqlAdapter(config.targetUrl);
1749
+ default:
1750
+ throw new Error(`Unsupported database type: ${config.type}`);
1751
+ }
1752
+ }
1753
+ var init_factory = __esm({
1754
+ "src/logic/factory.ts"() {
1755
+ "use strict";
1756
+ init_adapters();
1507
1757
  }
1508
1758
  });
1509
1759
 
@@ -1611,12 +1861,18 @@ async function batchMutateTableData(adapter, dbType, options) {
1611
1861
  }
1612
1862
  let executedCount = 0;
1613
1863
  try {
1864
+ await adapter.executeSql("BEGIN;");
1614
1865
  for (const sql of sqls) {
1615
1866
  await adapter.executeSql(sql);
1616
1867
  executedCount++;
1617
1868
  }
1869
+ await adapter.executeSql("COMMIT;");
1618
1870
  return ok({ modifiedCount: executedCount, executedCount });
1619
1871
  } catch (e) {
1872
+ try {
1873
+ await adapter.executeSql("ROLLBACK;");
1874
+ } catch {
1875
+ }
1620
1876
  return err(
1621
1877
  e.message || "Failed to execute database mutations",
1622
1878
  void 0,
@@ -1635,6 +1891,33 @@ var init_mutation = __esm({
1635
1891
  // src/logic/schema.ts
1636
1892
  async function createTable(adapter, dbType, tableName, columns) {
1637
1893
  try {
1894
+ if (dbType === "postgres") {
1895
+ const processedEnums = /* @__PURE__ */ new Set();
1896
+ for (const col of columns) {
1897
+ if (col.enumValues && col.enumValues.length > 0 && col.type && !["enum", "varchar", "varchar(255)", "text", "string"].includes(
1898
+ col.type.toLowerCase()
1899
+ ) && !processedEnums.has(col.type.toLowerCase())) {
1900
+ processedEnums.add(col.type.toLowerCase());
1901
+ const vals = col.enumValues.map((v) => `'${v.replace(/'/g, "''")}'`).join(", ");
1902
+ const enumTypeQuoted = adapter.quoteIdentifier(col.type);
1903
+ const escapedName = col.type.replace(/'/g, "''");
1904
+ const createTypeSql = `
1905
+ DO $$
1906
+ BEGIN
1907
+ IF NOT EXISTS (
1908
+ SELECT 1 FROM pg_type t
1909
+ JOIN pg_namespace n ON n.oid = t.typnamespace
1910
+ WHERE t.typname = '${escapedName}'
1911
+ AND (n.nspname = current_schema() OR n.nspname = 'public')
1912
+ ) THEN
1913
+ CREATE TYPE ${enumTypeQuoted} AS ENUM (${vals});
1914
+ END IF;
1915
+ END$$;
1916
+ `;
1917
+ await adapter.executeSql(createTypeSql);
1918
+ }
1919
+ }
1920
+ }
1638
1921
  const dialect = getDialect(dbType);
1639
1922
  const sql = dialect.buildCreateTable(tableName, columns);
1640
1923
  await adapter.executeSql(sql);
@@ -1725,6 +2008,7 @@ async function applySchemaChanges(adapter, dbType, options) {
1725
2008
  let nullable = edits.nullable !== void 0 ? !!edits.nullable : col.nullable;
1726
2009
  let defaultValue = edits.defaultValue !== void 0 ? edits.defaultValue : col.defaultValue;
1727
2010
  let isUnique = edits.isUnique !== void 0 ? !!edits.isUnique : col.isPk && !isPk ? false : !!col.isUnique;
2011
+ let enumValues = edits.enumValues !== void 0 ? edits.enumValues : col.enumValues;
1728
2012
  if (edits.name && edits.name !== col.name) {
1729
2013
  renames[col.name] = edits.name;
1730
2014
  }
@@ -1735,7 +2019,8 @@ async function applySchemaChanges(adapter, dbType, options) {
1735
2019
  nullable,
1736
2020
  defaultValue,
1737
2021
  isUnique,
1738
- fkTarget
2022
+ fkTarget,
2023
+ enumValues
1739
2024
  });
1740
2025
  }
1741
2026
  for (const ins of pendingInserts) {
@@ -1770,7 +2055,8 @@ async function applySchemaChanges(adapter, dbType, options) {
1770
2055
  nullable: ins.nullable !== void 0 ? !!ins.nullable : true,
1771
2056
  defaultValue: ins.defaultValue,
1772
2057
  isUnique: !!ins.isUnique,
1773
- fkTarget
2058
+ fkTarget,
2059
+ enumValues: ins.enumValues
1774
2060
  });
1775
2061
  }
1776
2062
  targetColumns = updated;
@@ -1828,8 +2114,32 @@ async function applySchemaChanges(adapter, dbType, options) {
1828
2114
  const quotedCol = dialect.quoteIdentifier(currentName);
1829
2115
  const type = edits.type || "TEXT";
1830
2116
  if (dbType === "postgres") {
2117
+ let pgType = type;
2118
+ if (edits.enumValues && edits.enumValues.length > 0) {
2119
+ if (edits.isNewEnum && edits.type) {
2120
+ const escapedName = edits.type.replace(/'/g, "''");
2121
+ const vals = edits.enumValues.map((v) => `'${dialect.escapeString(v)}'`).join(", ");
2122
+ const quotedType = dialect.quoteIdentifier(edits.type);
2123
+ sqls.push(`
2124
+ DO $$
2125
+ BEGIN
2126
+ IF NOT EXISTS (
2127
+ SELECT 1 FROM pg_type t
2128
+ JOIN pg_namespace n ON n.oid = t.typnamespace
2129
+ WHERE t.typname = '${escapedName}'
2130
+ AND (n.nspname = current_schema() OR n.nspname = 'public')
2131
+ ) THEN
2132
+ CREATE TYPE ${quotedType} AS ENUM (${vals});
2133
+ END IF;
2134
+ END$$;
2135
+ `);
2136
+ pgType = quotedType;
2137
+ } else if (edits.type) {
2138
+ pgType = dialect.quoteIdentifier(edits.type);
2139
+ }
2140
+ }
1831
2141
  sqls.push(
1832
- `ALTER TABLE ${quotedTable} ALTER COLUMN ${quotedCol} TYPE ${type};`
2142
+ `ALTER TABLE ${quotedTable} ALTER COLUMN ${quotedCol} TYPE ${pgType} USING ${quotedCol}::text::${pgType};`
1833
2143
  );
1834
2144
  if (edits.nullable === false) {
1835
2145
  sqls.push(
@@ -1840,17 +2150,29 @@ async function applySchemaChanges(adapter, dbType, options) {
1840
2150
  `ALTER TABLE ${quotedTable} ALTER COLUMN ${quotedCol} DROP NOT NULL;`
1841
2151
  );
1842
2152
  }
1843
- if (edits.defaultValue) {
1844
- const escapedDef = dialect.escapeString(edits.defaultValue);
1845
- sqls.push(
1846
- `ALTER TABLE ${quotedTable} ALTER COLUMN ${quotedCol} SET DEFAULT '${escapedDef}';`
1847
- );
2153
+ if (edits.defaultValue !== void 0) {
2154
+ const formatted = formatSqlDefaultValue(edits.defaultValue);
2155
+ if (formatted !== null) {
2156
+ sqls.push(
2157
+ `ALTER TABLE ${quotedTable} ALTER COLUMN ${quotedCol} SET DEFAULT ${formatted};`
2158
+ );
2159
+ } else {
2160
+ sqls.push(
2161
+ `ALTER TABLE ${quotedTable} ALTER COLUMN ${quotedCol} DROP DEFAULT;`
2162
+ );
2163
+ }
1848
2164
  }
1849
2165
  } else if (dbType === "mysql") {
2166
+ let mysqlType = type;
2167
+ if (edits.enumValues && edits.enumValues.length > 0) {
2168
+ const vals = edits.enumValues.map((v) => `'${dialect.escapeString(v)}'`).join(", ");
2169
+ mysqlType = `ENUM(${vals})`;
2170
+ }
1850
2171
  const nullStr = edits.nullable === false ? "NOT NULL" : "NULL";
1851
- const defStr = edits.defaultValue ? `DEFAULT '${dialect.escapeString(edits.defaultValue)}'` : "";
2172
+ const formatted = formatSqlDefaultValue(edits.defaultValue);
2173
+ const defStr = formatted !== null ? `DEFAULT ${formatted}` : "";
1852
2174
  sqls.push(
1853
- `ALTER TABLE ${quotedTable} MODIFY COLUMN ${quotedCol} ${type} ${nullStr} ${defStr};`
2175
+ `ALTER TABLE ${quotedTable} MODIFY COLUMN ${quotedCol} ${mysqlType} ${nullStr} ${defStr};`
1854
2176
  );
1855
2177
  }
1856
2178
  }
@@ -1858,9 +2180,36 @@ async function applySchemaChanges(adapter, dbType, options) {
1858
2180
  for (const ins of pendingInserts) {
1859
2181
  if (!ins.name || ins.name.trim() === "") continue;
1860
2182
  const quotedCol = dialect.quoteIdentifier(ins.name.trim());
1861
- const type = ins.type || "TEXT";
2183
+ let type = ins.type || "TEXT";
2184
+ if (dbType === "postgres" && ins.enumValues && ins.enumValues.length > 0) {
2185
+ if (ins.isNewEnum && ins.type) {
2186
+ const escapedName = ins.type.replace(/'/g, "''");
2187
+ const vals = ins.enumValues.map((v) => `'${dialect.escapeString(v)}'`).join(", ");
2188
+ const quotedType = dialect.quoteIdentifier(ins.type);
2189
+ sqls.push(`
2190
+ DO $$
2191
+ BEGIN
2192
+ IF NOT EXISTS (
2193
+ SELECT 1 FROM pg_type t
2194
+ JOIN pg_namespace n ON n.oid = t.typnamespace
2195
+ WHERE t.typname = '${escapedName}'
2196
+ AND (n.nspname = current_schema() OR n.nspname = 'public')
2197
+ ) THEN
2198
+ CREATE TYPE ${quotedType} AS ENUM (${vals});
2199
+ END IF;
2200
+ END$$;
2201
+ `);
2202
+ type = quotedType;
2203
+ } else if (ins.type) {
2204
+ type = dialect.quoteIdentifier(ins.type);
2205
+ }
2206
+ } else if (dbType === "mysql" && ins.enumValues && ins.enumValues.length > 0) {
2207
+ const vals = ins.enumValues.map((v) => `'${dialect.escapeString(v)}'`).join(", ");
2208
+ type = `ENUM(${vals})`;
2209
+ }
1862
2210
  const nullStr = ins.nullable === false ? "NOT NULL" : "";
1863
- const defStr = ins.defaultValue ? `DEFAULT '${dialect.escapeString(ins.defaultValue)}'` : "";
2211
+ const formattedDef = formatSqlDefaultValue(ins.defaultValue);
2212
+ const defStr = formattedDef !== null ? `DEFAULT ${formattedDef}` : "";
1864
2213
  sqls.push(
1865
2214
  `ALTER TABLE ${quotedTable} ADD COLUMN ${quotedCol} ${type} ${nullStr} ${defStr};`
1866
2215
  );
@@ -1899,7 +2248,7 @@ async function applySchemaChanges(adapter, dbType, options) {
1899
2248
  }
1900
2249
  async function getTableRowCount(adapter, tableName) {
1901
2250
  try {
1902
- const quoted = adapter.quoteIdentifier(tableName);
2251
+ const quoted = adapter.quoteTable ? adapter.quoteTable(tableName) : adapter.quoteIdentifier(tableName);
1903
2252
  const countRes = await adapter.query(
1904
2253
  `SELECT COUNT(*) as cnt FROM ${quoted}`
1905
2254
  );
@@ -2071,6 +2420,43 @@ async function exportDatabaseSchemaDdl(adapter, dbType) {
2071
2420
  );
2072
2421
  }
2073
2422
  }
2423
+ async function getDatabaseEnums(adapter) {
2424
+ try {
2425
+ if (adapter.getCustomEnums) {
2426
+ const enums = await adapter.getCustomEnums();
2427
+ return ok(enums);
2428
+ }
2429
+ const enumsMap = /* @__PURE__ */ new Map();
2430
+ try {
2431
+ const tables = await adapter.getTables();
2432
+ for (const t of tables.slice(0, 30)) {
2433
+ const cols = await adapter.getSchema(t);
2434
+ for (const col of cols) {
2435
+ if (col.enumValues && col.enumValues.length > 0) {
2436
+ const key = col.type && ![
2437
+ "enum",
2438
+ "varchar",
2439
+ "varchar(255)",
2440
+ "text",
2441
+ "string"
2442
+ ].includes(col.type.toLowerCase()) ? col.type : col.name;
2443
+ if (!enumsMap.has(key)) {
2444
+ enumsMap.set(key, col.enumValues);
2445
+ }
2446
+ }
2447
+ }
2448
+ }
2449
+ } catch {
2450
+ }
2451
+ const data = Array.from(enumsMap.entries()).map(([name, values]) => ({
2452
+ name,
2453
+ values
2454
+ }));
2455
+ return ok(data);
2456
+ } catch (e) {
2457
+ return err(e.message || "Failed to get database enums", void 0, e);
2458
+ }
2459
+ }
2074
2460
  var init_schema = __esm({
2075
2461
  "src/logic/schema.ts"() {
2076
2462
  "use strict";
@@ -2443,6 +2829,27 @@ var init_seeder = __esm({
2443
2829
  // src/logic/transfer.ts
2444
2830
  import fs2 from "fs/promises";
2445
2831
  import path2 from "path";
2832
+ async function getRestoreCandidates(dir) {
2833
+ const candidates = [];
2834
+ try {
2835
+ const entries = await fs2.readdir(dir, { withFileTypes: true });
2836
+ for (const entry of entries) {
2837
+ if (entry.isDirectory() && entry.name.startsWith("drixio_backup_")) {
2838
+ candidates.push({
2839
+ name: `\u{1F4C1} ${entry.name} (Backup Directory)`,
2840
+ value: entry.name
2841
+ });
2842
+ } else if (entry.isFile() && (entry.name.endsWith(".sql") || entry.name.endsWith(".json"))) {
2843
+ candidates.push({
2844
+ name: `\u{1F4C4} ${entry.name} (${entry.name.endsWith(".sql") ? "SQL Script" : "JSON Dump"})`,
2845
+ value: entry.name
2846
+ });
2847
+ }
2848
+ }
2849
+ } catch {
2850
+ }
2851
+ return candidates;
2852
+ }
2446
2853
  function formatCsvValue(val) {
2447
2854
  if (val === null || val === void 0) return "";
2448
2855
  const str = String(val);
@@ -2941,6 +3348,153 @@ function generateDatabaseSqlDumpStream(adapter, dbType) {
2941
3348
  }
2942
3349
  });
2943
3350
  }
3351
+ function nodeToWebStream(nodeStream) {
3352
+ return new ReadableStream({
3353
+ start(controller) {
3354
+ nodeStream.on("data", (chunk) => controller.enqueue(chunk));
3355
+ nodeStream.on("end", () => controller.close());
3356
+ nodeStream.on("error", (err2) => controller.error(err2));
3357
+ },
3358
+ cancel() {
3359
+ nodeStream.destroy();
3360
+ }
3361
+ });
3362
+ }
3363
+ async function exportDatabaseNative(dbType, connectionUrl, schemaOnly) {
3364
+ try {
3365
+ const executeNativeDump = (cmd, args, envName) => {
3366
+ return new Promise((resolve, reject) => {
3367
+ const cp = import("child_process").then((m) => m.spawn(cmd, args));
3368
+ let started = false;
3369
+ cp.then((cpInstance) => {
3370
+ const stream2 = nodeToWebStream(cpInstance.stdout);
3371
+ cpInstance.on("error", (e) => {
3372
+ if (!started)
3373
+ reject(
3374
+ new Error(
3375
+ `Native tool '${cmd}' not found. Please install ${envName}.`
3376
+ )
3377
+ );
3378
+ });
3379
+ setTimeout(() => {
3380
+ if (!cpInstance.killed) {
3381
+ started = true;
3382
+ resolve(stream2);
3383
+ }
3384
+ }, 100);
3385
+ });
3386
+ });
3387
+ };
3388
+ let stream;
3389
+ if (dbType === "sqlite") {
3390
+ const dbPath = connectionUrl.replace("file:", "");
3391
+ stream = await executeNativeDump(
3392
+ "sqlite3",
3393
+ [dbPath, schemaOnly ? ".schema" : ".dump"],
3394
+ "SQLite CLI"
3395
+ );
3396
+ } else if (dbType === "mysql") {
3397
+ const parsed = new URL(connectionUrl);
3398
+ const user = parsed.username;
3399
+ const pass = parsed.password;
3400
+ const host = parsed.hostname;
3401
+ const port = parsed.port || "3306";
3402
+ const dbname = parsed.pathname.substring(1);
3403
+ const args = ["-u", user, `-p${pass}`, "-h", host, "-P", port];
3404
+ if (schemaOnly) args.push("--no-data");
3405
+ args.push(dbname);
3406
+ stream = await executeNativeDump(
3407
+ "mysqldump",
3408
+ args,
3409
+ "MySQL Client"
3410
+ );
3411
+ } else if (dbType === "postgres") {
3412
+ const args = [connectionUrl];
3413
+ if (schemaOnly) args.push("--schema-only");
3414
+ stream = await executeNativeDump(
3415
+ "pg_dump",
3416
+ args,
3417
+ "PostgreSQL CLI"
3418
+ );
3419
+ } else {
3420
+ return err("Unsupported database type for native export");
3421
+ }
3422
+ return ok(stream);
3423
+ } catch (e) {
3424
+ return err(e.message || "Failed to export database using native tool", void 0, e);
3425
+ }
3426
+ }
3427
+ async function importDatabaseNative(adapter, dbType, connectionUrl, sqlContent) {
3428
+ try {
3429
+ const executeNativeImport = (cmd, args, fileContent, envName) => {
3430
+ return new Promise((resolve, reject) => {
3431
+ import("child_process").then((m) => {
3432
+ const cp = m.spawn(cmd, args);
3433
+ let started = false;
3434
+ let errStr = "";
3435
+ cp.on("error", (e) => {
3436
+ if (!started)
3437
+ reject(
3438
+ new Error(
3439
+ `Native tool '${cmd}' not found. Please install ${envName}.`
3440
+ )
3441
+ );
3442
+ });
3443
+ cp.stderr.on("data", (d) => errStr += d.toString());
3444
+ cp.on("close", (code) => {
3445
+ if (code === 0) resolve();
3446
+ else reject(new Error(`Native import failed: ${errStr}`));
3447
+ });
3448
+ cp.stdin.write(fileContent);
3449
+ cp.stdin.end();
3450
+ started = true;
3451
+ });
3452
+ });
3453
+ };
3454
+ try {
3455
+ if (dbType === "sqlite") {
3456
+ const dbPath = connectionUrl.replace("file:", "");
3457
+ await executeNativeImport(
3458
+ "sqlite3",
3459
+ [dbPath],
3460
+ sqlContent,
3461
+ "SQLite CLI"
3462
+ );
3463
+ } else if (dbType === "mysql") {
3464
+ const parsed = new URL(connectionUrl);
3465
+ const user = parsed.username;
3466
+ const pass = parsed.password;
3467
+ const host = parsed.hostname;
3468
+ const port = parsed.port || "3306";
3469
+ const dbname = parsed.pathname.substring(1);
3470
+ await executeNativeImport(
3471
+ "mysql",
3472
+ ["-u", user, `-p${pass}`, "-h", host, "-P", port, dbname],
3473
+ sqlContent,
3474
+ "MySQL Client"
3475
+ );
3476
+ } else if (dbType === "postgres") {
3477
+ await executeNativeImport(
3478
+ "psql",
3479
+ [connectionUrl],
3480
+ sqlContent,
3481
+ "PostgreSQL CLI"
3482
+ );
3483
+ } else {
3484
+ return err("Unsupported database type for native import");
3485
+ }
3486
+ } catch (e) {
3487
+ if (e.message.includes("Native tool")) {
3488
+ await adapter.executeSql(sqlContent);
3489
+ } else {
3490
+ throw e;
3491
+ }
3492
+ }
3493
+ return ok({ message: "Import completed successfully" });
3494
+ } catch (e) {
3495
+ return err(`Import Failed: ${e.message}`, void 0, e);
3496
+ }
3497
+ }
2944
3498
  var init_transfer = __esm({
2945
3499
  "src/logic/transfer.ts"() {
2946
3500
  "use strict";
@@ -3000,6 +3554,10 @@ async function createDatabase(options) {
3000
3554
  });
3001
3555
  try {
3002
3556
  await conn.query(`CREATE DATABASE IF NOT EXISTS \`${dbName}\``);
3557
+ const quotedDbName = dbName.replace(/`/g, "``");
3558
+ await conn.query(
3559
+ `CREATE DATABASE IF NOT EXISTS \`${quotedDbName}\``
3560
+ );
3003
3561
  } finally {
3004
3562
  await conn.end();
3005
3563
  }
@@ -3030,6 +3588,8 @@ async function createDatabase(options) {
3030
3588
  );
3031
3589
  if (res.rowCount === 0) {
3032
3590
  await conn.query(`CREATE DATABASE "${dbName}"`);
3591
+ const quotedDbName = dbName.replace(/"/g, '""');
3592
+ await conn.query(`CREATE DATABASE "${quotedDbName}"`);
3033
3593
  }
3034
3594
  } finally {
3035
3595
  await conn.end();
@@ -3081,6 +3641,8 @@ async function dropDatabase(options) {
3081
3641
  });
3082
3642
  try {
3083
3643
  await conn.query(`DROP DATABASE IF EXISTS \`${dbName}\``);
3644
+ const quotedDbName = dbName.replace(/`/g, "``");
3645
+ await conn.query(`DROP DATABASE IF EXISTS \`${quotedDbName}\``);
3084
3646
  } finally {
3085
3647
  await conn.end();
3086
3648
  }
@@ -3107,6 +3669,8 @@ async function dropDatabase(options) {
3107
3669
  await conn.connect();
3108
3670
  try {
3109
3671
  await conn.query(`DROP DATABASE IF EXISTS "${dbName}"`);
3672
+ const quotedDbName = dbName.replace(/"/g, '""');
3673
+ await conn.query(`DROP DATABASE IF EXISTS "${quotedDbName}"`);
3110
3674
  } finally {
3111
3675
  await conn.end();
3112
3676
  }
@@ -3838,7 +4402,10 @@ function generateMigrationSql(diff, dbType) {
3838
4402
  let typeStr = col.type || "TEXT";
3839
4403
  if (col.nullable === false) typeStr += " NOT NULL";
3840
4404
  if (col.defaultValue !== void 0 && col.defaultValue !== null) {
3841
- typeStr += ` DEFAULT '${dialect.escapeString(col.defaultValue)}'`;
4405
+ const formatted = formatSqlDefaultValue(col.defaultValue);
4406
+ if (formatted !== null) {
4407
+ typeStr += ` DEFAULT ${formatted}`;
4408
+ }
3842
4409
  }
3843
4410
  lines.push(`ALTER TABLE ${qTable} ADD COLUMN ${qCol} ${typeStr};`);
3844
4411
  }
@@ -3967,7 +4534,10 @@ function generateRollbackSql(diff, dbType) {
3967
4534
  let typeStr = col.type || "TEXT";
3968
4535
  if (col.nullable === false) typeStr += " NOT NULL";
3969
4536
  if (col.defaultValue !== void 0 && col.defaultValue !== null) {
3970
- typeStr += ` DEFAULT '${dialect.escapeString(col.defaultValue)}'`;
4537
+ const formatted = formatSqlDefaultValue(col.defaultValue);
4538
+ if (formatted !== null) {
4539
+ typeStr += ` DEFAULT ${formatted}`;
4540
+ }
3971
4541
  }
3972
4542
  lines.push(`ALTER TABLE ${qTable} ADD COLUMN ${qCol} ${typeStr};`);
3973
4543
  }
@@ -4208,9 +4778,18 @@ function extractSnippetParams(sql) {
4208
4778
  function substituteSnippetParams(sql, params, dbType = "sqlite") {
4209
4779
  if (!sql) return "";
4210
4780
  const dialect = getDialect(dbType);
4781
+ const identifierKeys = /* @__PURE__ */ new Set([
4782
+ "table",
4783
+ "column",
4784
+ "child_table",
4785
+ "parent_table",
4786
+ "fk_col",
4787
+ "pk_col"
4788
+ ]);
4211
4789
  let result = sql;
4212
4790
  for (const [key, rawValue] of Object.entries(params)) {
4213
4791
  const valStr = rawValue !== void 0 && rawValue !== null ? String(rawValue) : "";
4792
+ const isIdentifier = identifierKeys.has(key);
4214
4793
  const isNumber = !isNaN(Number(valStr)) && valStr.trim() !== "";
4215
4794
  const quotedMustache = new RegExp(`'\\{\\{\\s*${key}\\s*\\}\\}'`, "g");
4216
4795
  const quotedColon = new RegExp(`':${key}'`, "g");
@@ -4220,10 +4799,16 @@ function substituteSnippetParams(sql, params, dbType = "sqlite") {
4220
4799
  const rawMustache = new RegExp(`\\{\\{\\s*${key}\\s*\\}\\}`, "g");
4221
4800
  result = result.replace(rawMustache, () => {
4222
4801
  return isNumber ? valStr : valStr;
4802
+ if (isIdentifier) return dialect.quoteIdentifier(valStr);
4803
+ if (isNumber) return valStr;
4804
+ return `'${escapedVal}'`;
4223
4805
  });
4224
4806
  const rawColon = new RegExp(`(^|[^:]):${key}\\b`, "g");
4225
4807
  result = result.replace(rawColon, (_match, prefix) => {
4226
4808
  return `${prefix}${valStr}`;
4809
+ if (isIdentifier) return `${prefix}${dialect.quoteIdentifier(valStr)}`;
4810
+ if (isNumber) return `${prefix}${valStr}`;
4811
+ return `${prefix}'${escapedVal}'`;
4227
4812
  });
4228
4813
  }
4229
4814
  return result;
@@ -4282,8 +4867,8 @@ function getDrixioVersion() {
4282
4867
  if (cachedVersion) {
4283
4868
  return cachedVersion;
4284
4869
  }
4285
- if ("1.1.10") {
4286
- cachedVersion = "1.1.10";
4870
+ if ("1.2.0") {
4871
+ cachedVersion = "1.2.0";
4287
4872
  return cachedVersion;
4288
4873
  }
4289
4874
  try {
@@ -4340,6 +4925,30 @@ var init_serialization = __esm({
4340
4925
  }
4341
4926
  });
4342
4927
 
4928
+ // src/logic/repl.ts
4929
+ function parseReplCommand(rawSql) {
4930
+ const trimmed = rawSql.trim();
4931
+ const connectMatch = trimmed.match(/^CONNECT\s+([^\s;]+)\s*;?$/i);
4932
+ if (connectMatch) {
4933
+ return { type: "connect", url: connectMatch[1], originalSql: rawSql };
4934
+ }
4935
+ if (/^DISCONNECT\s*;?$/i.test(trimmed)) {
4936
+ return { type: "disconnect", originalSql: rawSql };
4937
+ }
4938
+ const createDbMatch = trimmed.match(
4939
+ /^CREATE\s+DATABASE\s+(?:IF\s+NOT\s+EXISTS\s+)?['"`]?([^'";`\s]+)['"`]?\s*;?$/i
4940
+ );
4941
+ if (createDbMatch) {
4942
+ return { type: "create_database", dbName: createDbMatch[1], originalSql: rawSql };
4943
+ }
4944
+ return { type: "sql", originalSql: rawSql };
4945
+ }
4946
+ var init_repl = __esm({
4947
+ "src/logic/repl.ts"() {
4948
+ "use strict";
4949
+ }
4950
+ });
4951
+
4343
4952
  // src/logic/index.ts
4344
4953
  var init_logic = __esm({
4345
4954
  "src/logic/index.ts"() {
@@ -4360,6 +4969,7 @@ var init_logic = __esm({
4360
4969
  init_snippets();
4361
4970
  init_version();
4362
4971
  init_serialization();
4972
+ init_repl();
4363
4973
  }
4364
4974
  });
4365
4975
 
@@ -5862,21 +6472,8 @@ var init_diffWizard = __esm({
5862
6472
 
5863
6473
  // src/studio/api.ts
5864
6474
  import { Hono } from "hono";
5865
- import { spawn } from "child_process";
5866
6475
  import path18 from "path";
5867
6476
  import fs18 from "fs";
5868
- function nodeToWebStream(nodeStream) {
5869
- return new ReadableStream({
5870
- start(controller) {
5871
- nodeStream.on("data", (chunk) => controller.enqueue(chunk));
5872
- nodeStream.on("end", () => controller.close());
5873
- nodeStream.on("error", (err2) => controller.error(err2));
5874
- },
5875
- cancel() {
5876
- nodeStream.destroy();
5877
- }
5878
- });
5879
- }
5880
6477
  function registerApiRoutes(app, dbConfig) {
5881
6478
  const api = new Hono();
5882
6479
  let currentDbConfig = { ...dbConfig };
@@ -5962,7 +6559,11 @@ function registerApiRoutes(app, dbConfig) {
5962
6559
  });
5963
6560
  const detectHostEnvironment = (cfg) => {
5964
6561
  if (cfg.type === "sqlite") {
5965
- return { isRemote: false, host: "local", badgeLabel: "LOCAL (SQLite)" };
6562
+ return {
6563
+ isRemote: false,
6564
+ host: "local",
6565
+ badgeLabel: "LOCAL (SQLite)"
6566
+ };
5966
6567
  }
5967
6568
  if (cfg.type === "mysql" || cfg.type === "postgres") {
5968
6569
  let host = "localhost";
@@ -6241,7 +6842,7 @@ function registerApiRoutes(app, dbConfig) {
6241
6842
  return c.json({ success: false, error: e.message }, 500);
6242
6843
  }
6243
6844
  });
6244
- api.post("/analyze-query", async (c) => {
6845
+ api.post("/query/analyze", async (c) => {
6245
6846
  try {
6246
6847
  const { sql } = await c.req.json().catch(() => ({}));
6247
6848
  const result = analyzeDangerousQuery(sql);
@@ -6259,10 +6860,9 @@ function registerApiRoutes(app, dbConfig) {
6259
6860
  400
6260
6861
  );
6261
6862
  }
6262
- const trimmed = sql.trim();
6263
- const connectMatch = trimmed.match(/^CONNECT\s+([^\s;]+)\s*;?$/i);
6264
- if (connectMatch) {
6265
- const rawUrl = connectMatch[1];
6863
+ const replCmd = parseReplCommand(sql);
6864
+ if (replCmd.type === "connect" && replCmd.url) {
6865
+ const rawUrl = replCmd.url;
6266
6866
  const newConfig = await detectDatabase(rawUrl);
6267
6867
  const newAdapter = createDBAdapter(newConfig);
6268
6868
  await newAdapter.getTables();
@@ -6294,7 +6894,7 @@ function registerApiRoutes(app, dbConfig) {
6294
6894
  }
6295
6895
  });
6296
6896
  }
6297
- if (/^DISCONNECT\s*;?$/i.test(trimmed)) {
6897
+ if (replCmd.type === "disconnect") {
6298
6898
  if (currentAdapter) {
6299
6899
  try {
6300
6900
  await currentAdapter.close();
@@ -6321,11 +6921,8 @@ function registerApiRoutes(app, dbConfig) {
6321
6921
  }
6322
6922
  });
6323
6923
  }
6324
- const createDbMatch = trimmed.match(
6325
- /^CREATE\s+DATABASE\s+(?:IF\s+NOT\s+EXISTS\s+)?['"`]?([^'";`\s]+)['"`]?\s*;?$/i
6326
- );
6327
- if (createDbMatch) {
6328
- const dbName = createDbMatch[1];
6924
+ if (replCmd.type === "create_database" && replCmd.dbName) {
6925
+ const dbName = replCmd.dbName;
6329
6926
  if (currentAdapter && (currentDbConfig.type === "postgres" || currentDbConfig.type === "mysql")) {
6330
6927
  const quote = currentDbConfig.type === "mysql" ? "`" : '"';
6331
6928
  await currentAdapter.executeSql(
@@ -6407,24 +7004,19 @@ function registerApiRoutes(app, dbConfig) {
6407
7004
  );
6408
7005
  }
6409
7006
  const adapter = getAdapter();
6410
- const cleanSql = sql.trim().replace(/;+$/, "");
6411
- let explainSql = "";
6412
- if (currentDbConfig.type === "sqlite") {
6413
- explainSql = `EXPLAIN QUERY PLAN ${cleanSql}`;
6414
- } else if (currentDbConfig.type === "postgres") {
6415
- explainSql = `EXPLAIN (ANALYZE, COSTS, VERBOSE, BUFFERS) ${cleanSql}`;
6416
- } else if (currentDbConfig.type === "mysql") {
6417
- explainSql = `EXPLAIN ${cleanSql}`;
6418
- } else {
6419
- explainSql = `EXPLAIN ${cleanSql}`;
7007
+ const explainRes = generateExplainQuery(sql, currentDbConfig.type);
7008
+ if (explainRes.error || !explainRes.sql) {
7009
+ return c.json({ success: false, error: explainRes.error }, 400);
6420
7010
  }
7011
+ let explainSql = explainRes.sql;
6421
7012
  const startTime = performance.now();
6422
7013
  let res;
6423
7014
  try {
6424
7015
  res = await adapter.query(explainSql);
6425
7016
  } catch (primaryErr) {
6426
7017
  if (currentDbConfig.type === "postgres") {
6427
- explainSql = `EXPLAIN ${cleanSql}`;
7018
+ const fallbackRes = generateExplainQuery(sql, "unknown");
7019
+ explainSql = fallbackRes.sql;
6428
7020
  res = await adapter.query(explainSql);
6429
7021
  } else {
6430
7022
  throw primaryErr;
@@ -6517,68 +7109,25 @@ function registerApiRoutes(app, dbConfig) {
6517
7109
  }
6518
7110
  const type = currentDbConfig.type;
6519
7111
  const url = currentDbConfig.targetUrl;
6520
- let child;
6521
7112
  let filename = "backup.sql";
6522
- const executeNativeDump = (cmd, args, envName) => {
6523
- return new Promise((resolve, reject) => {
6524
- const cp = spawn(cmd, args);
6525
- let started = false;
6526
- const stream = nodeToWebStream(cp.stdout);
6527
- cp.on("error", (err2) => {
6528
- if (!started)
6529
- reject(
6530
- new Error(
6531
- `Native tool '${cmd}' not found. Please install ${envName}.`
6532
- )
6533
- );
6534
- });
6535
- setTimeout(() => {
6536
- if (!cp.killed) {
6537
- started = true;
6538
- resolve(stream);
6539
- }
6540
- }, 100);
6541
- });
6542
- };
6543
7113
  try {
6544
- let stream;
6545
7114
  const baseName = getDbName();
6546
7115
  const timeStr = getDatetimeStr();
6547
7116
  filename = `${baseName}_backup_${timeStr}.sql`;
6548
- if (type === "sqlite") {
6549
- const dbPath = url.replace("file:", "");
6550
- stream = await executeNativeDump(
6551
- "sqlite3",
6552
- [dbPath, ".dump"],
6553
- "SQLite CLI"
6554
- );
6555
- } else if (type === "mysql") {
6556
- const parsed = new URL(url);
6557
- const user = parsed.username;
6558
- const pass = parsed.password;
6559
- const host = parsed.hostname;
6560
- const port = parsed.port || "3306";
6561
- const dbname = parsed.pathname.substring(1);
6562
- stream = await executeNativeDump(
6563
- "mysqldump",
6564
- ["-u", user, `-p${pass}`, "-h", host, "-P", port, dbname],
6565
- "MySQL Client"
6566
- );
6567
- } else if (type === "postgres") {
6568
- stream = await executeNativeDump(
6569
- "pg_dump",
6570
- [url],
6571
- "PostgreSQL CLI"
6572
- );
6573
- } else {
6574
- throw new Error("Unsupported database type for native export");
7117
+ const exportRes = await exportDatabaseNative(
7118
+ type,
7119
+ url,
7120
+ false
7121
+ );
7122
+ if (!exportRes.success) {
7123
+ throw new Error(exportRes.error);
6575
7124
  }
6576
7125
  c.header(
6577
7126
  "Content-Disposition",
6578
7127
  `attachment; filename="${filename}"`
6579
7128
  );
6580
7129
  c.header("Content-Type", "application/sql");
6581
- return c.body(stream);
7130
+ return c.body(exportRes.data);
6582
7131
  } catch (err2) {
6583
7132
  if (err2.message.includes("Native tool")) {
6584
7133
  const baseName = getDbName();
@@ -6634,67 +7183,14 @@ function registerApiRoutes(app, dbConfig) {
6634
7183
  const sqlContent = await file.text();
6635
7184
  const type = currentDbConfig.type;
6636
7185
  const url = currentDbConfig.targetUrl;
6637
- const executeNativeImport = (cmd, args, fileContent, envName) => {
6638
- return new Promise((resolve, reject) => {
6639
- const cp = spawn(cmd, args);
6640
- let started = false;
6641
- let errStr = "";
6642
- cp.on("error", (err2) => {
6643
- if (!started)
6644
- reject(
6645
- new Error(
6646
- `Native tool '${cmd}' not found. Please install ${envName}.`
6647
- )
6648
- );
6649
- });
6650
- cp.stderr.on("data", (d) => errStr += d.toString());
6651
- cp.on("close", (code) => {
6652
- if (code === 0) resolve();
6653
- else reject(new Error(`Native import failed: ${errStr}`));
6654
- });
6655
- cp.stdin.write(fileContent);
6656
- cp.stdin.end();
6657
- started = true;
6658
- });
6659
- };
6660
- try {
6661
- if (type === "sqlite") {
6662
- const dbPath = url.replace("file:", "");
6663
- await executeNativeImport(
6664
- "sqlite3",
6665
- [dbPath],
6666
- sqlContent,
6667
- "SQLite CLI"
6668
- );
6669
- } else if (type === "mysql") {
6670
- const parsed = new URL(url);
6671
- const user = parsed.username;
6672
- const pass = parsed.password;
6673
- const host = parsed.hostname;
6674
- const port = parsed.port || "3306";
6675
- const dbname = parsed.pathname.substring(1);
6676
- await executeNativeImport(
6677
- "mysql",
6678
- ["-u", user, `-p${pass}`, "-h", host, "-P", port, dbname],
6679
- sqlContent,
6680
- "MySQL Client"
6681
- );
6682
- } else if (type === "postgres") {
6683
- await executeNativeImport(
6684
- "psql",
6685
- [url],
6686
- sqlContent,
6687
- "PostgreSQL CLI"
6688
- );
6689
- } else {
6690
- throw new Error("Unsupported database type for native import");
6691
- }
6692
- } catch (err2) {
6693
- if (err2.message.includes("Native tool")) {
6694
- await getAdapter().executeSql(sqlContent);
6695
- } else {
6696
- throw err2;
6697
- }
7186
+ const importRes = await importDatabaseNative(
7187
+ getAdapter(),
7188
+ type,
7189
+ url,
7190
+ sqlContent
7191
+ );
7192
+ if (!importRes.success) {
7193
+ return c.json({ success: false, error: importRes.error }, 500);
6698
7194
  }
6699
7195
  return c.json({
6700
7196
  success: true,
@@ -7034,8 +7530,7 @@ function registerApiRoutes(app, dbConfig) {
7034
7530
  const dialect = (dbType || "sqlite").toLowerCase();
7035
7531
  if (dialect === "sqlite") {
7036
7532
  const rawPath = body.url || body.sqlitePath || dbName || "drixio.sqlite";
7037
- const cleanPath = rawPath.replace(/^file:/, "").trim();
7038
- const fullPath = path18.isAbsolute(cleanPath) ? cleanPath : path18.resolve(process.cwd(), cleanPath);
7533
+ const fullPath = resolveLocalDbPath(rawPath, process.cwd());
7039
7534
  if (mode === "create") {
7040
7535
  const dir = path18.dirname(fullPath);
7041
7536
  if (!fs18.existsSync(dir)) {
@@ -7095,12 +7590,7 @@ function registerApiRoutes(app, dbConfig) {
7095
7590
  }
7096
7591
  let targetUrl = "";
7097
7592
  if (mode === "create") {
7098
- const targetHost = host || "localhost";
7099
- const targetPort = port || (dialect === "postgres" ? "5432" : "3306");
7100
- const targetUser = user || (dialect === "postgres" ? "postgres" : "root");
7101
- const auth = password ? `${encodeURIComponent(targetUser)}:${encodeURIComponent(password)}` : encodeURIComponent(targetUser);
7102
- const defaultDb = dialect === "postgres" ? "postgres" : "";
7103
- targetUrl = `${dialect}://${auth}@${targetHost}:${targetPort}/${defaultDb}`;
7593
+ targetUrl = assembleConnectionUrl(dialect, host, port, user, password, "");
7104
7594
  } else {
7105
7595
  const rawUrl = body.url;
7106
7596
  if (!rawUrl) {
@@ -7168,6 +7658,73 @@ function registerApiRoutes(app, dbConfig) {
7168
7658
  return c.json({ success: false, error: e.message }, 500);
7169
7659
  }
7170
7660
  });
7661
+ api.get("/schemas", async (c) => {
7662
+ try {
7663
+ const adapter = currentAdapter;
7664
+ const supported = !!adapter && typeof adapter.getSchemas === "function" && currentDbConfig.type === "postgres";
7665
+ if (!supported || !adapter) {
7666
+ return c.json({
7667
+ success: true,
7668
+ data: { supported: false, schemas: [], currentSchema: "public" }
7669
+ });
7670
+ }
7671
+ const schemas = await adapter.getSchemas();
7672
+ const currentSchema = adapter.getCurrentSchema ? adapter.getCurrentSchema() : "public";
7673
+ return c.json({
7674
+ success: true,
7675
+ data: { supported: true, schemas, currentSchema }
7676
+ });
7677
+ } catch (e) {
7678
+ return c.json({ success: false, error: e.message }, 500);
7679
+ }
7680
+ });
7681
+ api.post("/schemas/switch", async (c) => {
7682
+ try {
7683
+ const { schema } = await c.req.json().catch(() => ({}));
7684
+ if (!schema || typeof schema !== "string") {
7685
+ return c.json(
7686
+ { success: false, error: "Schema name is required" },
7687
+ 400
7688
+ );
7689
+ }
7690
+ const adapter = currentAdapter;
7691
+ if (!adapter || typeof adapter.setSchema !== "function" || currentDbConfig.type !== "postgres") {
7692
+ return c.json(
7693
+ {
7694
+ success: false,
7695
+ error: "Schema switching is only supported for PostgreSQL"
7696
+ },
7697
+ 400
7698
+ );
7699
+ }
7700
+ await adapter.setSchema(schema);
7701
+ const tables = await adapter.getTables();
7702
+ return c.json({
7703
+ success: true,
7704
+ data: {
7705
+ schema,
7706
+ tableCount: tables.length
7707
+ }
7708
+ });
7709
+ } catch (e) {
7710
+ return c.json({ success: false, error: e.message }, 500);
7711
+ }
7712
+ });
7713
+ api.get("/database/enums", async (c) => {
7714
+ try {
7715
+ const adapter = currentAdapter;
7716
+ if (!adapter) {
7717
+ return c.json({ success: true, data: [] });
7718
+ }
7719
+ const result = await getDatabaseEnums(adapter);
7720
+ if (!result.success) {
7721
+ return c.json({ success: false, error: result.error }, 500);
7722
+ }
7723
+ return c.json({ success: true, data: result.data });
7724
+ } catch (e) {
7725
+ return c.json({ success: false, error: e.message }, 500);
7726
+ }
7727
+ });
7171
7728
  app.route("/api", api);
7172
7729
  }
7173
7730
  var init_api = __esm({
@@ -7785,23 +8342,7 @@ async function runRestoreCommand(dbConfig, args, options = {}) {
7785
8342
  const { input: input11, select: select16, confirm: confirm8 } = await import("@inquirer/prompts");
7786
8343
  if (!targetPath) {
7787
8344
  try {
7788
- const entries = await fs9.readdir(process.cwd(), {
7789
- withFileTypes: true
7790
- });
7791
- const candidates = [];
7792
- for (const entry of entries) {
7793
- if (entry.isDirectory() && entry.name.startsWith("drixio_backup_")) {
7794
- candidates.push({
7795
- name: `\u{1F4C1} ${entry.name} (Backup Directory)`,
7796
- value: entry.name
7797
- });
7798
- } else if (entry.isFile() && (entry.name.endsWith(".sql") || entry.name.endsWith(".json"))) {
7799
- candidates.push({
7800
- name: `\u{1F4C4} ${entry.name} (${entry.name.endsWith(".sql") ? "SQL Script" : "JSON Dump"})`,
7801
- value: entry.name
7802
- });
7803
- }
7804
- }
8345
+ const candidates = await getRestoreCandidates(process.cwd());
7805
8346
  if (candidates.length > 0) {
7806
8347
  candidates.push({
7807
8348
  name: "\u270F\uFE0F Enter a custom path manually...",
@@ -8596,7 +9137,7 @@ Usage examples:`));
8596
9137
  }
8597
9138
  }
8598
9139
  let diffResult;
8599
- const isRemoteUrl = targetArg.startsWith("postgres://") || targetArg.startsWith("postgresql://") || targetArg.startsWith("mysql://") || targetArg.startsWith("file:");
9140
+ const isRemoteUrl = isConnectionString(targetArg) || targetArg.startsWith("file:");
8600
9141
  if (isRemoteUrl) {
8601
9142
  console.log(
8602
9143
  pc17.cyan(