@plyaz/db 0.1.1 → 0.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/index.js CHANGED
@@ -193,6 +193,10 @@ function failure(error) {
193
193
  return { success: false, error };
194
194
  }
195
195
  __name(failure, "failure");
196
+ var DESCRIPTION_MAX_LENGTH = 60;
197
+ var FALLBACK_DESCRIPTION_LENGTH = 50;
198
+ var PROGRESS_LOG_INTERVAL = 10;
199
+ var ERROR_MESSAGE_MAX_LENGTH = 300;
196
200
  var MigrationManager = class {
197
201
  static {
198
202
  __name(this, "MigrationManager");
@@ -302,6 +306,116 @@ var MigrationManager = class {
302
306
  }
303
307
  return { upSQL: sql2.trim(), downSQL: null };
304
308
  }
309
+ /**
310
+ * Process dollar-quoted string delimiters ($$ or $tag$)
311
+ * Returns updated state for tracking if we're inside a dollar block
312
+ */
313
+ processDollarDelimiters(line, inDollarBlock, dollarTag) {
314
+ const dollarMatch = line.match(/\$([a-zA-Z_]*)\$/g);
315
+ if (!dollarMatch) return { inDollarBlock, dollarTag };
316
+ let currentInBlock = inDollarBlock;
317
+ let currentTag = dollarTag;
318
+ for (const match of dollarMatch) {
319
+ if (!currentInBlock) {
320
+ currentInBlock = true;
321
+ currentTag = match;
322
+ } else if (match === currentTag) {
323
+ currentInBlock = false;
324
+ currentTag = "";
325
+ }
326
+ }
327
+ return { inDollarBlock: currentInBlock, dollarTag: currentTag };
328
+ }
329
+ /**
330
+ * Filter out comment-only statements
331
+ */
332
+ isNonCommentStatement(statement) {
333
+ const withoutComments = statement.replace(/--.*$/gm, "").trim();
334
+ return withoutComments.length > 0;
335
+ }
336
+ /**
337
+ * Split SQL into individual statements for better error reporting
338
+ * Handles $$ delimited blocks (functions, triggers) correctly
339
+ */
340
+ splitSqlStatements(sql2) {
341
+ const statements = [];
342
+ let current = "";
343
+ let inDollarBlock = false;
344
+ let dollarTag = "";
345
+ for (const line of sql2.split("\n")) {
346
+ const trimmedLine = line.trim();
347
+ const isEmptyOrComment = trimmedLine === "" || trimmedLine.startsWith("--");
348
+ current += line + "\n";
349
+ if (isEmptyOrComment) continue;
350
+ const dollarState = this.processDollarDelimiters(line, inDollarBlock, dollarTag);
351
+ inDollarBlock = dollarState.inDollarBlock;
352
+ dollarTag = dollarState.dollarTag;
353
+ const isEndOfStatement = !inDollarBlock && trimmedLine.endsWith(";");
354
+ if (isEndOfStatement && current.trim()) {
355
+ statements.push(current.trim());
356
+ current = "";
357
+ }
358
+ }
359
+ if (current.trim()) {
360
+ statements.push(current.trim());
361
+ }
362
+ return statements.filter((s) => this.isNonCommentStatement(s));
363
+ }
364
+ /**
365
+ * Extract a short description from a SQL statement for logging
366
+ */
367
+ getStatementDescription(statement) {
368
+ const firstLine = statement.split("\n").find((l) => l.trim() && !l.trim().startsWith("--"))?.trim() ?? "";
369
+ const patterns = [
370
+ /^(CREATE\s+(?:OR\s+REPLACE\s+)?(?:TABLE|INDEX|UNIQUE\s+INDEX|TYPE|FUNCTION|TRIGGER|EXTENSION|SCHEMA|VIEW|POLICY))\s+(?:IF\s+NOT\s+EXISTS\s+)?([^\s(]+)/i,
371
+ /^(ALTER\s+TABLE)\s+([^\s]+)/i,
372
+ /^(DROP\s+(?:TABLE|INDEX|TYPE|FUNCTION|TRIGGER|EXTENSION|SCHEMA|VIEW|POLICY))\s+(?:IF\s+EXISTS\s+)?([^\s(;]+)/i,
373
+ /^(INSERT\s+INTO)\s+([^\s(]+)/i,
374
+ /^(COMMENT\s+ON\s+(?:TABLE|COLUMN|INDEX|FUNCTION|TYPE))\s+([^\s]+)/i,
375
+ /^(GRANT|REVOKE)\s+.+\s+ON\s+([^\s]+)/i
376
+ ];
377
+ for (const pattern of patterns) {
378
+ const match = firstLine.match(pattern);
379
+ if (match) {
380
+ return `${match[1]} ${match[2]}`.slice(0, DESCRIPTION_MAX_LENGTH);
381
+ }
382
+ }
383
+ const truncated = firstLine.slice(0, FALLBACK_DESCRIPTION_LENGTH);
384
+ const suffix = firstLine.length > FALLBACK_DESCRIPTION_LENGTH ? "..." : "";
385
+ return truncated + suffix;
386
+ }
387
+ /**
388
+ * Execute SQL statements individually with better error reporting
389
+ */
390
+ async executeSqlStatements(adapter, sql2, migrationVersion) {
391
+ const statements = this.splitSqlStatements(sql2);
392
+ const total = statements.length;
393
+ console.log(` → ${total} statements to execute`);
394
+ for (let i = 0; i < statements.length; i++) {
395
+ const statement = statements[i];
396
+ const description = this.getStatementDescription(statement);
397
+ try {
398
+ await adapter.query(statement);
399
+ const isInterval = (i + 1) % PROGRESS_LOG_INTERVAL === 0;
400
+ const isLast = i === total - 1;
401
+ const isSignificant = Boolean(description.match(/^(CREATE TABLE|CREATE FUNCTION|CREATE TRIGGER)/i));
402
+ if (isInterval || isLast || isSignificant) {
403
+ console.log(` ✓ [${i + 1}/${total}] ${description}`);
404
+ }
405
+ } catch (error) {
406
+ console.log(` ✗ [${i + 1}/${total}] ${description}`);
407
+ const rawMessage = error.message;
408
+ const errorMessage = rawMessage.replace(/^SQL Error:\s*/i, "").replace(/^Failed to execute query:.*?-\s*/i, "").slice(0, ERROR_MESSAGE_MAX_LENGTH);
409
+ throw new DatabaseError(
410
+ `Migration ${migrationVersion} failed at statement ${i + 1}/${total}:
411
+ Statement: ${description}
412
+ Error: ${errorMessage}`,
413
+ DATABASE_ERROR_CODES.QUERY_FAILED,
414
+ { cause: error }
415
+ );
416
+ }
417
+ }
418
+ }
305
419
  /**
306
420
  * Load SQL migration from file
307
421
  */
@@ -313,12 +427,20 @@ var MigrationManager = class {
313
427
  name: migrationFile.name,
314
428
  up: /* @__PURE__ */ __name(async (adapter) => {
315
429
  if (typeof adapter.query === "function") {
316
- await adapter.query(upSQL);
430
+ await this.executeSqlStatements(
431
+ adapter,
432
+ upSQL,
433
+ migrationFile.version
434
+ );
317
435
  }
318
436
  }, "up"),
319
437
  down: /* @__PURE__ */ __name(async (adapter) => {
320
438
  if (downSQL && typeof adapter.query === "function") {
321
- await adapter.query(downSQL);
439
+ await this.executeSqlStatements(
440
+ adapter,
441
+ downSQL,
442
+ migrationFile.version
443
+ );
322
444
  } else {
323
445
  console.warn(
324
446
  `[Migrations] No DOWN migration for ${migrationFile.version}`
@@ -425,6 +547,7 @@ var MigrationManager = class {
425
547
  /**
426
548
  * Run all pending migrations
427
549
  */
550
+ /* eslint-disable max-depth, complexity */
428
551
  async up(targetVersion) {
429
552
  try {
430
553
  await this.initialize();
@@ -445,9 +568,15 @@ var MigrationManager = class {
445
568
  const migration = await this.loadMigration(migrationFile);
446
569
  const startTime = Date.now();
447
570
  if (typeof this.adapter.transaction === "function") {
448
- await this.adapter.transaction(async () => {
571
+ const txResult = await this.adapter.transaction(async () => {
449
572
  await migration.up(this.adapter);
450
573
  });
574
+ if (!txResult.success) {
575
+ throw txResult.error ?? new DatabaseError(
576
+ `Migration ${migration.version} failed`,
577
+ DATABASE_ERROR_CODES.QUERY_FAILED
578
+ );
579
+ }
451
580
  } else {
452
581
  await migration.up(this.adapter);
453
582
  }
@@ -502,9 +631,15 @@ var MigrationManager = class {
502
631
  const migration = await this.loadMigration(migrationFile);
503
632
  const startTime = Date.now();
504
633
  if (typeof this.adapter.transaction === "function") {
505
- await this.adapter.transaction(async () => {
634
+ const txResult = await this.adapter.transaction(async () => {
506
635
  await migration.down(this.adapter);
507
636
  });
637
+ if (!txResult.success) {
638
+ throw txResult.error ?? new DatabaseError(
639
+ `Rollback ${appliedMigration.version} failed`,
640
+ DATABASE_ERROR_CODES.QUERY_FAILED
641
+ );
642
+ }
508
643
  } else {
509
644
  await migration.down(this.adapter);
510
645
  }
@@ -563,6 +698,10 @@ var MigrationManager = class {
563
698
  }
564
699
  }
565
700
  };
701
+ var DESCRIPTION_MAX_LENGTH2 = 60;
702
+ var FALLBACK_DESCRIPTION_LENGTH2 = 50;
703
+ var PROGRESS_LOG_INTERVAL2 = 10;
704
+ var ERROR_MESSAGE_MAX_LENGTH2 = 300;
566
705
  var SeedManager = class {
567
706
  static {
568
707
  __name(this, "SeedManager");
@@ -631,7 +770,7 @@ var SeedManager = class {
631
770
  const files = fs3.readdirSync(this.seedsPath);
632
771
  const seeds = [];
633
772
  for (const file of files) {
634
- const match = file.match(/^(\d+)_(.+)\.(ts|js)$/);
773
+ const match = file.match(/^(\d+)_(.+)\.(ts|js|sql)$/);
635
774
  if (match) {
636
775
  const [, order, name] = match;
637
776
  seeds.push({
@@ -644,9 +783,137 @@ var SeedManager = class {
644
783
  return seeds.sort((a, b) => a.order - b.order);
645
784
  }
646
785
  /**
647
- * Load seed from file
786
+ * Process dollar-quoted string delimiters ($$ or $tag$)
787
+ */
788
+ processDollarDelimiters(line, inDollarBlock, dollarTag) {
789
+ const dollarMatch = line.match(/\$([a-zA-Z_]*)\$/g);
790
+ if (!dollarMatch) return { inDollarBlock, dollarTag };
791
+ let currentInBlock = inDollarBlock;
792
+ let currentTag = dollarTag;
793
+ for (const match of dollarMatch) {
794
+ if (!currentInBlock) {
795
+ currentInBlock = true;
796
+ currentTag = match;
797
+ } else if (match === currentTag) {
798
+ currentInBlock = false;
799
+ currentTag = "";
800
+ }
801
+ }
802
+ return { inDollarBlock: currentInBlock, dollarTag: currentTag };
803
+ }
804
+ /**
805
+ * Filter out comment-only statements
806
+ */
807
+ isNonCommentStatement(statement) {
808
+ const withoutComments = statement.replace(/--.*$/gm, "").trim();
809
+ return withoutComments.length > 0;
810
+ }
811
+ /**
812
+ * Split SQL into individual statements for better error reporting
813
+ */
814
+ splitSqlStatements(sql2) {
815
+ const statements = [];
816
+ let current = "";
817
+ let inDollarBlock = false;
818
+ let dollarTag = "";
819
+ for (const line of sql2.split("\n")) {
820
+ const trimmedLine = line.trim();
821
+ const isEmptyOrComment = trimmedLine === "" || trimmedLine.startsWith("--");
822
+ current += line + "\n";
823
+ if (isEmptyOrComment) continue;
824
+ const dollarState = this.processDollarDelimiters(line, inDollarBlock, dollarTag);
825
+ inDollarBlock = dollarState.inDollarBlock;
826
+ dollarTag = dollarState.dollarTag;
827
+ if (!inDollarBlock && trimmedLine.endsWith(";") && current.trim()) {
828
+ statements.push(current.trim());
829
+ current = "";
830
+ }
831
+ }
832
+ if (current.trim()) {
833
+ statements.push(current.trim());
834
+ }
835
+ return statements.filter((s) => this.isNonCommentStatement(s));
836
+ }
837
+ /**
838
+ * Extract a short description from a SQL statement for logging
648
839
  */
840
+ getStatementDescription(statement) {
841
+ const firstLine = statement.split("\n").find((l) => l.trim() && !l.trim().startsWith("--"))?.trim() ?? "";
842
+ const patterns = [
843
+ /^(CREATE\s+(?:OR\s+REPLACE\s+)?(?:TABLE|INDEX|UNIQUE\s+INDEX|TYPE|FUNCTION|TRIGGER|EXTENSION|SCHEMA|VIEW|POLICY))\s+(?:IF\s+NOT\s+EXISTS\s+)?([^\s(]+)/i,
844
+ /^(ALTER\s+TABLE)\s+([^\s]+)/i,
845
+ /^(DROP\s+(?:TABLE|INDEX|TYPE|FUNCTION|TRIGGER|EXTENSION|SCHEMA|VIEW|POLICY))\s+(?:IF\s+EXISTS\s+)?([^\s(;]+)/i,
846
+ /^(INSERT\s+INTO)\s+([^\s(]+)/i,
847
+ /^(COMMENT\s+ON\s+(?:TABLE|COLUMN|INDEX|FUNCTION|TYPE))\s+([^\s]+)/i,
848
+ /^(GRANT|REVOKE)\s+.+\s+ON\s+([^\s]+)/i
849
+ ];
850
+ for (const pattern of patterns) {
851
+ const match = firstLine.match(pattern);
852
+ if (match) {
853
+ return `${match[1]} ${match[2]}`.slice(0, DESCRIPTION_MAX_LENGTH2);
854
+ }
855
+ }
856
+ const truncated = firstLine.slice(0, FALLBACK_DESCRIPTION_LENGTH2);
857
+ const suffix = firstLine.length > FALLBACK_DESCRIPTION_LENGTH2 ? "..." : "";
858
+ return truncated + suffix;
859
+ }
860
+ /**
861
+ * Execute SQL statements individually with better error reporting
862
+ */
863
+ async executeSqlStatements(sql2, seedName) {
864
+ const statements = this.splitSqlStatements(sql2);
865
+ const total = statements.length;
866
+ console.log(` → ${total} statements to execute`);
867
+ for (let i = 0; i < statements.length; i++) {
868
+ const statement = statements[i];
869
+ const description = this.getStatementDescription(statement);
870
+ try {
871
+ await this.adapter.query(statement);
872
+ const isInterval = (i + 1) % PROGRESS_LOG_INTERVAL2 === 0;
873
+ const isLast = i === total - 1;
874
+ const isSignificant = Boolean(description.match(/^(INSERT INTO)/i));
875
+ if (isInterval || isLast || isSignificant) {
876
+ console.log(` ✓ [${i + 1}/${total}] ${description}`);
877
+ }
878
+ } catch (error) {
879
+ console.log(` ✗ [${i + 1}/${total}] ${description}`);
880
+ const rawMessage = error.message;
881
+ const errorMessage = rawMessage.replace(/^SQL Error:\s*/i, "").replace(/^Failed to execute query:.*?-\s*/i, "").slice(0, ERROR_MESSAGE_MAX_LENGTH2);
882
+ throw new DatabaseError(
883
+ `Seed "${seedName}" failed at statement ${i + 1}/${total}:
884
+ Statement: ${description}
885
+ Error: ${errorMessage}`,
886
+ DATABASE_ERROR_CODES.QUERY_FAILED,
887
+ { cause: error }
888
+ );
889
+ }
890
+ }
891
+ }
892
+ /**
893
+ * Load SQL seed from file
894
+ */
895
+ loadSqlSeed(seedFile) {
896
+ const sql2 = fs3.readFileSync(seedFile.filePath, "utf-8");
897
+ return {
898
+ name: seedFile.name,
899
+ run: /* @__PURE__ */ __name(async () => {
900
+ if (typeof this.adapter.query === "function") {
901
+ await this.executeSqlStatements(sql2, seedFile.name);
902
+ }
903
+ }, "run"),
904
+ // SQL seeds don't have cleanup by default
905
+ cleanup: void 0
906
+ };
907
+ }
908
+ /**
909
+ * Load seed from file (supports .ts, .js, and .sql)
910
+ */
911
+ // eslint-disable-next-line complexity
649
912
  async loadSeed(seedFile) {
913
+ const ext = path4.extname(seedFile.filePath);
914
+ if (ext === ".sql") {
915
+ return this.loadSqlSeed(seedFile);
916
+ }
650
917
  const importPath = seedFile.filePath.startsWith("/") ? seedFile.filePath : new URL(`file:///${seedFile.filePath.replace(/\\/g, "/")}`).href;
651
918
  const seedModule = await import(importPath);
652
919
  return {
@@ -700,9 +967,15 @@ var SeedManager = class {
700
967
  */
701
968
  async executeSeed(seed) {
702
969
  if (typeof this.adapter.transaction === "function") {
703
- await this.adapter.transaction(async () => {
970
+ const txResult = await this.adapter.transaction(async () => {
704
971
  await seed.run(this.adapter);
705
972
  });
973
+ if (!txResult.success) {
974
+ throw txResult.error ?? new DatabaseError(
975
+ `Seed ${seed.name} failed`,
976
+ DATABASE_ERROR_CODES.QUERY_FAILED
977
+ );
978
+ }
706
979
  } else {
707
980
  await seed.run(this.adapter);
708
981
  }
@@ -759,9 +1032,15 @@ var SeedManager = class {
759
1032
  async executeCleanup(seed) {
760
1033
  if (!seed.cleanup) return;
761
1034
  if (typeof this.adapter.transaction === "function") {
762
- await this.adapter.transaction(async () => {
1035
+ const txResult = await this.adapter.transaction(async () => {
763
1036
  await seed.cleanup(this.adapter);
764
1037
  });
1038
+ if (!txResult.success) {
1039
+ throw txResult.error ?? new DatabaseError(
1040
+ `Seed cleanup for ${seed.name} failed`,
1041
+ DATABASE_ERROR_CODES.QUERY_FAILED
1042
+ );
1043
+ }
765
1044
  } else {
766
1045
  await seed.cleanup(this.adapter);
767
1046
  }
@@ -4395,6 +4674,7 @@ var SupabaseAdapter = class {
4395
4674
  return ops[operator]();
4396
4675
  }
4397
4676
  };
4677
+ var SQL_ERROR_TRUNCATE_LENGTH = 500;
4398
4678
  var SQLAdapter = class {
4399
4679
  static {
4400
4680
  __name(this, "SQLAdapter");
@@ -4405,6 +4685,7 @@ var SQLAdapter = class {
4405
4685
  idColumnMap = /* @__PURE__ */ new Map();
4406
4686
  configIdColumns;
4407
4687
  defaultSchema;
4688
+ showSqlInErrors;
4408
4689
  /**
4409
4690
  * Creates a new SQLAdapter instance.
4410
4691
  * @param {SQLAdapterConfig} config - Configuration for the SQL adapter.
@@ -4418,6 +4699,7 @@ var SQLAdapter = class {
4418
4699
  constructor(config) {
4419
4700
  this.config = config;
4420
4701
  this.defaultSchema = config.schema ?? "public";
4702
+ this.showSqlInErrors = config.showSqlInErrors ?? true;
4421
4703
  this.pool = new Pool({
4422
4704
  connectionString: config.connectionString,
4423
4705
  ...config.pool
@@ -4536,16 +4818,16 @@ var SQLAdapter = class {
4536
4818
  const result = await this.pool.query(sql2, params);
4537
4819
  return result.rows;
4538
4820
  } catch (error) {
4539
- throw new DatabaseError(
4540
- `Failed to execute query: ${sql2} - ${error.message}`,
4541
- DATABASE_ERROR_CODES.QUERY_FAILED,
4542
- {
4543
- context: {
4544
- source: "SQLAdapter.query"
4545
- },
4546
- cause: error
4547
- }
4548
- );
4821
+ const truncatedSql = sql2.slice(0, SQL_ERROR_TRUNCATE_LENGTH);
4822
+ const sqlSuffix = sql2.length > SQL_ERROR_TRUNCATE_LENGTH ? "..." : "";
4823
+ const errorMessage = this.showSqlInErrors ? `SQL Error: ${error.message}
4824
+ Query: ${truncatedSql}${sqlSuffix}` : `SQL Error: ${error.message}`;
4825
+ throw new DatabaseError(errorMessage, DATABASE_ERROR_CODES.QUERY_FAILED, {
4826
+ context: {
4827
+ source: "SQLAdapter.query"
4828
+ },
4829
+ cause: error
4830
+ });
4549
4831
  }
4550
4832
  }
4551
4833
  /**
@@ -7975,20 +8257,48 @@ async function createDatabaseService(config) {
7975
8257
  }
7976
8258
  __name(createDatabaseService, "createDatabaseService");
7977
8259
  var JSON_INDENT_SPACES = 2;
7978
- async function getPublicTables(adapter) {
8260
+ async function getUserSchemas(adapter) {
7979
8261
  const result = await adapter.query?.(`
7980
- SELECT tablename FROM pg_tables
7981
- WHERE schemaname = 'public'
7982
- AND tablename NOT IN ('schema_migrations', 'seed_history')
8262
+ SELECT schema_name FROM information_schema.schemata
8263
+ WHERE schema_name NOT IN ('pg_catalog', 'information_schema', 'pg_toast', 'mysql', 'sys', 'performance_schema')
8264
+ AND schema_name NOT LIKE 'pg_%'
8265
+ AND schema_name NOT LIKE 'pg_temp_%'
8266
+ AND schema_name NOT LIKE 'pg_toast_temp_%'
7983
8267
  `);
7984
- const tables = Array.isArray(result) ? result : result.rows || [];
7985
- return tables.map((t) => t.tablename);
8268
+ const schemas = Array.isArray(result) ? result : result.rows || [];
8269
+ return schemas.map((s) => s.schema_name);
8270
+ }
8271
+ __name(getUserSchemas, "getUserSchemas");
8272
+ async function getTablesFromSchemas(adapter, schemas) {
8273
+ const tables = [];
8274
+ for (const schema of schemas) {
8275
+ const result = await adapter.query?.(
8276
+ `
8277
+ SELECT table_name FROM information_schema.tables
8278
+ WHERE table_schema = $1
8279
+ AND table_type = 'BASE TABLE'
8280
+ AND table_name NOT IN ('schema_migrations', 'seed_history')
8281
+ `,
8282
+ [schema]
8283
+ );
8284
+ const schemaResult = Array.isArray(result) ? result : result.rows || [];
8285
+ for (const row of schemaResult) {
8286
+ tables.push({ schema, table: row.table_name });
8287
+ }
8288
+ }
8289
+ return tables;
7986
8290
  }
7987
- __name(getPublicTables, "getPublicTables");
8291
+ __name(getTablesFromSchemas, "getTablesFromSchemas");
7988
8292
  async function clearTables(adapter, tables) {
7989
- for (const table of tables) {
7990
- await adapter.query?.(`TRUNCATE TABLE ${table} CASCADE`);
7991
- console.log(` ✓ Cleared ${table}`);
8293
+ let currentSchema = "";
8294
+ for (const { schema, table } of tables) {
8295
+ if (schema !== currentSchema) {
8296
+ console.log(`
8297
+ 📁 Schema: ${schema}`);
8298
+ currentSchema = schema;
8299
+ }
8300
+ await adapter.query?.(`TRUNCATE TABLE "${schema}"."${table}" CASCADE`);
8301
+ console.log(` ✓ Cleared ${schema}.${table}`);
7992
8302
  }
7993
8303
  }
7994
8304
  __name(clearTables, "clearTables");
@@ -8335,7 +8645,13 @@ seedCommand.command("reset").description("Reset seeds (run cleanup functions)").
8335
8645
  process.exit(1);
8336
8646
  }
8337
8647
  });
8338
- program.command("clear").description("Clear all data from database (keep schema)").option("--confirm", "Confirm clear operation").option("-t, --tables <tables>", "Comma-separated list of tables to clear").action(async (options) => {
8648
+ program.command("clear").description("Clear all data from database (keep schema)").option("--confirm", "Confirm clear operation").option(
8649
+ "-t, --tables <tables>",
8650
+ "Comma-separated list of tables to clear (format: schema.table or just table for public)"
8651
+ ).option(
8652
+ "-s, --schemas <schemas>",
8653
+ "Comma-separated list of schemas to clear (default: all user schemas)"
8654
+ ).action(async (options) => {
8339
8655
  if (!options.confirm) {
8340
8656
  console.error("❌ Please use --confirm flag to confirm clear operation");
8341
8657
  process.exit(1);
@@ -8347,16 +8663,36 @@ program.command("clear").description("Clear all data from database (keep schema)
8347
8663
  console.error("❌ Clear operation not supported by this adapter");
8348
8664
  process.exit(1);
8349
8665
  }
8350
- const tablesToClear = options.tables ? options.tables.split(",").map((t) => t.trim()) : await getPublicTables(adapter);
8666
+ let tablesToClear;
8667
+ if (options.tables) {
8668
+ tablesToClear = options.tables.split(",").map((t) => {
8669
+ const trimmed = t.trim();
8670
+ if (trimmed.includes(".")) {
8671
+ const [schema, table] = trimmed.split(".");
8672
+ return { schema, table };
8673
+ }
8674
+ return { schema: "public", table: trimmed };
8675
+ });
8676
+ } else {
8677
+ const schemas = options.schemas ? options.schemas.split(",").map((s) => s.trim()) : await getUserSchemas(adapter);
8678
+ tablesToClear = await getTablesFromSchemas(adapter, schemas);
8679
+ }
8351
8680
  await clearTables(adapter, tablesToClear);
8352
- console.log("✅ Database cleared");
8681
+ console.log(`
8682
+ ✅ Cleared ${tablesToClear.length} tables`);
8353
8683
  process.exit(0);
8354
8684
  } catch (error) {
8355
8685
  console.error("❌ Error:", error.message);
8356
8686
  process.exit(1);
8357
8687
  }
8358
8688
  });
8359
- program.command("drop").description("Drop all tables from database").option("--confirm", "Confirm drop operation").action(async (options) => {
8689
+ program.command("drop").description("Drop all tables from database (all schemas)").option("--confirm", "Confirm drop operation").option(
8690
+ "-s, --schemas <schemas>",
8691
+ "Comma-separated list of schemas to drop from (default: all user schemas)"
8692
+ ).option(
8693
+ "--all",
8694
+ "Drop everything: tables, types, functions, views (not just tables)"
8695
+ ).option("--drop-schemas", "Also drop the schemas themselves (except public)").action(async (options) => {
8360
8696
  if (!options.confirm) {
8361
8697
  console.error("❌ Please use --confirm flag to confirm drop operation");
8362
8698
  process.exit(1);
@@ -8364,22 +8700,226 @@ program.command("drop").description("Drop all tables from database").option("--c
8364
8700
  try {
8365
8701
  const { adapter } = await initDatabase();
8366
8702
  console.log("🔄 Dropping all tables...");
8367
- if (typeof adapter.query === "function") {
8368
- const result = await adapter.query(`
8369
- SELECT tablename FROM pg_tables
8370
- WHERE schemaname = 'public'
8371
- `);
8372
- const tables = Array.isArray(result) ? result : result.rows || [];
8373
- for (const { tablename } of tables) {
8374
- await adapter.query(`DROP TABLE IF EXISTS ${tablename} CASCADE`);
8375
- console.log(` ✓ Dropped ${tablename}`);
8376
- }
8377
- console.log("✅ All tables dropped");
8378
- process.exit(0);
8379
- } else {
8703
+ if (typeof adapter.query !== "function") {
8380
8704
  console.error("❌ Drop operation not supported by this adapter");
8381
8705
  process.exit(1);
8382
8706
  }
8707
+ const schemas = options.schemas ? options.schemas.split(",").map((s) => s.trim()) : await getUserSchemas(adapter);
8708
+ let totalDropped = 0;
8709
+ for (const schema of schemas) {
8710
+ const result = await adapter.query(
8711
+ `
8712
+ SELECT table_name FROM information_schema.tables
8713
+ WHERE table_schema = $1
8714
+ AND table_type = 'BASE TABLE'
8715
+ `,
8716
+ [schema]
8717
+ );
8718
+ const tables = Array.isArray(result) ? result : result.rows || [];
8719
+ if (tables.length === 0) continue;
8720
+ console.log(`
8721
+ 📁 Schema: ${schema}`);
8722
+ for (const { table_name } of tables) {
8723
+ await adapter.query(
8724
+ `DROP TABLE IF EXISTS "${schema}"."${table_name}" CASCADE`
8725
+ );
8726
+ console.log(` ✓ Dropped ${schema}.${table_name}`);
8727
+ totalDropped++;
8728
+ }
8729
+ }
8730
+ if (options.all) {
8731
+ console.log("\n🗑️ Dropping views...");
8732
+ for (const schema of schemas) {
8733
+ const viewResult = await adapter.query(
8734
+ `
8735
+ SELECT table_name FROM information_schema.views
8736
+ WHERE table_schema = $1
8737
+ `,
8738
+ [schema]
8739
+ );
8740
+ const views = Array.isArray(viewResult) ? viewResult : viewResult.rows || [];
8741
+ for (const { table_name } of views) {
8742
+ try {
8743
+ await adapter.query(
8744
+ `DROP VIEW IF EXISTS "${schema}"."${table_name}" CASCADE`
8745
+ );
8746
+ console.log(` ✓ Dropped view ${schema}.${table_name}`);
8747
+ } catch {
8748
+ }
8749
+ }
8750
+ }
8751
+ console.log("\n🗑️ Dropping triggers...");
8752
+ for (const schema of schemas) {
8753
+ try {
8754
+ const triggerResult = await adapter.query(
8755
+ `
8756
+ SELECT DISTINCT trigger_name, event_object_table
8757
+ FROM information_schema.triggers
8758
+ WHERE trigger_schema = $1
8759
+ `,
8760
+ [schema]
8761
+ );
8762
+ const triggers = Array.isArray(triggerResult) ? triggerResult : triggerResult.rows || [];
8763
+ for (const { trigger_name, event_object_table } of triggers) {
8764
+ try {
8765
+ await adapter.query(
8766
+ `DROP TRIGGER IF EXISTS "${trigger_name}" ON "${schema}"."${event_object_table}" CASCADE`
8767
+ );
8768
+ console.log(` ✓ Dropped trigger ${schema}.${trigger_name}`);
8769
+ } catch {
8770
+ }
8771
+ }
8772
+ } catch {
8773
+ }
8774
+ }
8775
+ console.log("\n🗑️ Dropping functions...");
8776
+ for (const schema of schemas) {
8777
+ try {
8778
+ const funcResult = await adapter.query(
8779
+ `
8780
+ SELECT p.proname, pg_get_function_identity_arguments(p.oid) as args
8781
+ FROM pg_proc p
8782
+ JOIN pg_namespace n ON p.pronamespace = n.oid
8783
+ WHERE n.nspname = $1
8784
+ AND p.prokind = 'f'
8785
+ `,
8786
+ [schema]
8787
+ );
8788
+ const funcs = Array.isArray(funcResult) ? funcResult : funcResult.rows || [];
8789
+ for (const { proname, args } of funcs) {
8790
+ try {
8791
+ await adapter.query(
8792
+ `DROP FUNCTION IF EXISTS "${schema}"."${proname}"(${args}) CASCADE`
8793
+ );
8794
+ console.log(` ✓ Dropped function ${schema}.${proname}`);
8795
+ } catch {
8796
+ }
8797
+ }
8798
+ } catch {
8799
+ console.log(
8800
+ ` ⚠ Function dropping not supported for this database`
8801
+ );
8802
+ break;
8803
+ }
8804
+ }
8805
+ console.log("\n🗑️ Dropping procedures...");
8806
+ for (const schema of schemas) {
8807
+ try {
8808
+ const procResult = await adapter.query(
8809
+ `
8810
+ SELECT p.proname, pg_get_function_identity_arguments(p.oid) as args
8811
+ FROM pg_proc p
8812
+ JOIN pg_namespace n ON p.pronamespace = n.oid
8813
+ WHERE n.nspname = $1
8814
+ AND p.prokind = 'p'
8815
+ `,
8816
+ [schema]
8817
+ );
8818
+ const procs = Array.isArray(procResult) ? procResult : procResult.rows || [];
8819
+ for (const { proname, args } of procs) {
8820
+ try {
8821
+ await adapter.query(
8822
+ `DROP PROCEDURE IF EXISTS "${schema}"."${proname}"(${args}) CASCADE`
8823
+ );
8824
+ console.log(` ✓ Dropped procedure ${schema}.${proname}`);
8825
+ } catch {
8826
+ }
8827
+ }
8828
+ } catch {
8829
+ }
8830
+ }
8831
+ console.log("\n🗑️ Dropping sequences...");
8832
+ for (const schema of schemas) {
8833
+ try {
8834
+ const seqResult = await adapter.query(
8835
+ `
8836
+ SELECT sequence_name FROM information_schema.sequences
8837
+ WHERE sequence_schema = $1
8838
+ `,
8839
+ [schema]
8840
+ );
8841
+ const sequences = Array.isArray(seqResult) ? seqResult : seqResult.rows || [];
8842
+ for (const { sequence_name } of sequences) {
8843
+ try {
8844
+ await adapter.query(
8845
+ `DROP SEQUENCE IF EXISTS "${schema}"."${sequence_name}" CASCADE`
8846
+ );
8847
+ console.log(` ✓ Dropped sequence ${schema}.${sequence_name}`);
8848
+ } catch {
8849
+ }
8850
+ }
8851
+ } catch {
8852
+ }
8853
+ }
8854
+ console.log("\n🗑️ Dropping types...");
8855
+ for (const schema of schemas) {
8856
+ try {
8857
+ const typeResult = await adapter.query(
8858
+ `
8859
+ SELECT t.typname
8860
+ FROM pg_type t
8861
+ JOIN pg_namespace n ON t.typnamespace = n.oid
8862
+ WHERE n.nspname = $1
8863
+ AND t.typtype = 'e'
8864
+ `,
8865
+ [schema]
8866
+ );
8867
+ const types = Array.isArray(typeResult) ? typeResult : typeResult.rows || [];
8868
+ for (const { typname } of types) {
8869
+ try {
8870
+ await adapter.query(
8871
+ `DROP TYPE IF EXISTS "${schema}"."${typname}" CASCADE`
8872
+ );
8873
+ console.log(` ✓ Dropped type ${schema}.${typname}`);
8874
+ } catch {
8875
+ }
8876
+ }
8877
+ } catch {
8878
+ console.log(` ⚠ Type dropping not supported for this database`);
8879
+ break;
8880
+ }
8881
+ }
8882
+ console.log("\n🗑️ Dropping domains...");
8883
+ for (const schema of schemas) {
8884
+ try {
8885
+ const domainResult = await adapter.query(
8886
+ `
8887
+ SELECT domain_name FROM information_schema.domains
8888
+ WHERE domain_schema = $1
8889
+ `,
8890
+ [schema]
8891
+ );
8892
+ const domains = Array.isArray(domainResult) ? domainResult : domainResult.rows || [];
8893
+ for (const { domain_name } of domains) {
8894
+ try {
8895
+ await adapter.query(
8896
+ `DROP DOMAIN IF EXISTS "${schema}"."${domain_name}" CASCADE`
8897
+ );
8898
+ console.log(` ✓ Dropped domain ${schema}.${domain_name}`);
8899
+ } catch {
8900
+ }
8901
+ }
8902
+ } catch {
8903
+ }
8904
+ }
8905
+ }
8906
+ if (options.dropSchemas) {
8907
+ console.log("\n🗑️ Dropping schemas...");
8908
+ for (const schema of schemas) {
8909
+ if (schema === "public") continue;
8910
+ try {
8911
+ await adapter.query(`DROP SCHEMA IF EXISTS "${schema}" CASCADE`);
8912
+ console.log(` ✓ Dropped schema ${schema}`);
8913
+ } catch {
8914
+ console.log(` ⚠ Could not drop schema ${schema}`);
8915
+ }
8916
+ }
8917
+ }
8918
+ console.log(
8919
+ `
8920
+ ✅ Dropped ${totalDropped} tables${options.all ? " + types, functions, views" : ""}`
8921
+ );
8922
+ process.exit(0);
8383
8923
  } catch (error) {
8384
8924
  console.error("❌ Error:", error.message);
8385
8925
  process.exit(1);
@@ -8415,6 +8955,247 @@ program.command("health").description("Check database health").action(async () =
8415
8955
  process.exit(1);
8416
8956
  }
8417
8957
  });
8958
+ async function dropAllObjects(adapter, schemas, dropSchemas) {
8959
+ let totalTables = 0;
8960
+ let totalObjects = 0;
8961
+ console.log("\n🗑️ Dropping tables...");
8962
+ for (const schema of schemas) {
8963
+ const result = await adapter.query(
8964
+ `
8965
+ SELECT table_name FROM information_schema.tables
8966
+ WHERE table_schema = $1
8967
+ AND table_type = 'BASE TABLE'
8968
+ `,
8969
+ [schema]
8970
+ );
8971
+ const tables = Array.isArray(result) ? result : result.rows || [];
8972
+ if (tables.length === 0) continue;
8973
+ console.log(`
8974
+ 📁 Schema: ${schema}`);
8975
+ for (const { table_name } of tables) {
8976
+ await adapter.query(
8977
+ `DROP TABLE IF EXISTS "${schema}"."${table_name}" CASCADE`
8978
+ );
8979
+ console.log(` ✓ Dropped table ${schema}.${table_name}`);
8980
+ totalTables++;
8981
+ }
8982
+ }
8983
+ console.log("\n🗑️ Dropping views...");
8984
+ for (const schema of schemas) {
8985
+ const viewResult = await adapter.query(
8986
+ `SELECT table_name FROM information_schema.views WHERE table_schema = $1`,
8987
+ [schema]
8988
+ );
8989
+ const views = Array.isArray(viewResult) ? viewResult : viewResult.rows || [];
8990
+ for (const { table_name } of views) {
8991
+ try {
8992
+ await adapter.query(
8993
+ `DROP VIEW IF EXISTS "${schema}"."${table_name}" CASCADE`
8994
+ );
8995
+ console.log(` ✓ Dropped view ${schema}.${table_name}`);
8996
+ totalObjects++;
8997
+ } catch {
8998
+ }
8999
+ }
9000
+ }
9001
+ console.log("\n🗑️ Dropping triggers...");
9002
+ for (const schema of schemas) {
9003
+ try {
9004
+ const triggerResult = await adapter.query(
9005
+ `SELECT DISTINCT trigger_name, event_object_table FROM information_schema.triggers WHERE trigger_schema = $1`,
9006
+ [schema]
9007
+ );
9008
+ const triggers = Array.isArray(triggerResult) ? triggerResult : triggerResult.rows || [];
9009
+ for (const { trigger_name, event_object_table } of triggers) {
9010
+ try {
9011
+ await adapter.query(
9012
+ `DROP TRIGGER IF EXISTS "${trigger_name}" ON "${schema}"."${event_object_table}" CASCADE`
9013
+ );
9014
+ console.log(` ✓ Dropped trigger ${schema}.${trigger_name}`);
9015
+ totalObjects++;
9016
+ } catch {
9017
+ }
9018
+ }
9019
+ } catch {
9020
+ }
9021
+ }
9022
+ console.log("\n🗑️ Dropping functions...");
9023
+ for (const schema of schemas) {
9024
+ try {
9025
+ const funcResult = await adapter.query(
9026
+ `SELECT p.proname, pg_get_function_identity_arguments(p.oid) as args
9027
+ FROM pg_proc p JOIN pg_namespace n ON p.pronamespace = n.oid
9028
+ WHERE n.nspname = $1 AND p.prokind = 'f'`,
9029
+ [schema]
9030
+ );
9031
+ const funcs = Array.isArray(funcResult) ? funcResult : funcResult.rows || [];
9032
+ for (const { proname, args } of funcs) {
9033
+ try {
9034
+ await adapter.query(
9035
+ `DROP FUNCTION IF EXISTS "${schema}"."${proname}"(${args}) CASCADE`
9036
+ );
9037
+ console.log(` ✓ Dropped function ${schema}.${proname}`);
9038
+ totalObjects++;
9039
+ } catch {
9040
+ }
9041
+ }
9042
+ } catch {
9043
+ }
9044
+ }
9045
+ console.log("\n🗑️ Dropping procedures...");
9046
+ for (const schema of schemas) {
9047
+ try {
9048
+ const procResult = await adapter.query(
9049
+ `SELECT p.proname, pg_get_function_identity_arguments(p.oid) as args
9050
+ FROM pg_proc p JOIN pg_namespace n ON p.pronamespace = n.oid
9051
+ WHERE n.nspname = $1 AND p.prokind = 'p'`,
9052
+ [schema]
9053
+ );
9054
+ const procs = Array.isArray(procResult) ? procResult : procResult.rows || [];
9055
+ for (const { proname, args } of procs) {
9056
+ try {
9057
+ await adapter.query(
9058
+ `DROP PROCEDURE IF EXISTS "${schema}"."${proname}"(${args}) CASCADE`
9059
+ );
9060
+ console.log(` ✓ Dropped procedure ${schema}.${proname}`);
9061
+ totalObjects++;
9062
+ } catch {
9063
+ }
9064
+ }
9065
+ } catch {
9066
+ }
9067
+ }
9068
+ console.log("\n🗑️ Dropping sequences...");
9069
+ for (const schema of schemas) {
9070
+ try {
9071
+ const seqResult = await adapter.query(
9072
+ `SELECT sequence_name FROM information_schema.sequences WHERE sequence_schema = $1`,
9073
+ [schema]
9074
+ );
9075
+ const sequences = Array.isArray(seqResult) ? seqResult : seqResult.rows || [];
9076
+ for (const { sequence_name } of sequences) {
9077
+ try {
9078
+ await adapter.query(
9079
+ `DROP SEQUENCE IF EXISTS "${schema}"."${sequence_name}" CASCADE`
9080
+ );
9081
+ console.log(` ✓ Dropped sequence ${schema}.${sequence_name}`);
9082
+ totalObjects++;
9083
+ } catch {
9084
+ }
9085
+ }
9086
+ } catch {
9087
+ }
9088
+ }
9089
+ console.log("\n🗑️ Dropping types...");
9090
+ for (const schema of schemas) {
9091
+ try {
9092
+ const typeResult = await adapter.query(
9093
+ `SELECT t.typname FROM pg_type t
9094
+ JOIN pg_namespace n ON t.typnamespace = n.oid
9095
+ WHERE n.nspname = $1 AND t.typtype = 'e'`,
9096
+ [schema]
9097
+ );
9098
+ const types = Array.isArray(typeResult) ? typeResult : typeResult.rows || [];
9099
+ for (const { typname } of types) {
9100
+ try {
9101
+ await adapter.query(
9102
+ `DROP TYPE IF EXISTS "${schema}"."${typname}" CASCADE`
9103
+ );
9104
+ console.log(` ✓ Dropped type ${schema}.${typname}`);
9105
+ totalObjects++;
9106
+ } catch {
9107
+ }
9108
+ }
9109
+ } catch {
9110
+ }
9111
+ }
9112
+ console.log("\n🗑️ Dropping domains...");
9113
+ for (const schema of schemas) {
9114
+ try {
9115
+ const domainResult = await adapter.query(
9116
+ `SELECT domain_name FROM information_schema.domains WHERE domain_schema = $1`,
9117
+ [schema]
9118
+ );
9119
+ const domains = Array.isArray(domainResult) ? domainResult : domainResult.rows || [];
9120
+ for (const { domain_name } of domains) {
9121
+ try {
9122
+ await adapter.query(
9123
+ `DROP DOMAIN IF EXISTS "${schema}"."${domain_name}" CASCADE`
9124
+ );
9125
+ console.log(` ✓ Dropped domain ${schema}.${domain_name}`);
9126
+ totalObjects++;
9127
+ } catch {
9128
+ }
9129
+ }
9130
+ } catch {
9131
+ }
9132
+ }
9133
+ if (dropSchemas) {
9134
+ console.log("\n🗑️ Dropping schemas...");
9135
+ for (const schema of schemas) {
9136
+ if (schema === "public") continue;
9137
+ try {
9138
+ await adapter.query(`DROP SCHEMA IF EXISTS "${schema}" CASCADE`);
9139
+ console.log(` ✓ Dropped schema ${schema}`);
9140
+ } catch {
9141
+ console.log(` ⚠ Could not drop schema ${schema}`);
9142
+ }
9143
+ }
9144
+ }
9145
+ return { tables: totalTables, objects: totalObjects };
9146
+ }
9147
+ __name(dropAllObjects, "dropAllObjects");
9148
+ program.command("reset").description(
9149
+ "Full database reset: drop all objects from all schemas, optionally run migrations"
9150
+ ).option("--confirm", "Confirm reset operation").option(
9151
+ "-s, --schemas <schemas>",
9152
+ "Comma-separated list of schemas (default: all user schemas)"
9153
+ ).option("--drop-schemas", "Also drop schemas themselves (except public)").option("--migrate", "Run migrations after reset").action(async (options) => {
9154
+ if (!options.confirm) {
9155
+ console.error("❌ Please use --confirm flag to confirm reset operation");
9156
+ console.error(
9157
+ " This will DROP ALL database objects (tables, types, functions, etc.)"
9158
+ );
9159
+ process.exit(1);
9160
+ }
9161
+ try {
9162
+ const { adapter, config } = await initDatabase();
9163
+ if (typeof adapter.query !== "function") {
9164
+ console.error("❌ Reset operation not supported by this adapter");
9165
+ process.exit(1);
9166
+ }
9167
+ console.log("🔄 Resetting database (dropping all objects)...");
9168
+ const schemas = options.schemas ? options.schemas.split(",").map((s) => s.trim()) : await getUserSchemas(adapter);
9169
+ const { tables, objects } = await dropAllObjects(
9170
+ adapter,
9171
+ schemas,
9172
+ options.dropSchemas ?? false
9173
+ );
9174
+ console.log(
9175
+ `
9176
+ ✅ Reset complete: dropped ${tables} tables, ${objects} other objects`
9177
+ );
9178
+ if (options.migrate) {
9179
+ console.log("\n🔄 Running migrations...");
9180
+ const migrationManager = new MigrationManager({
9181
+ adapter,
9182
+ migrationsPath: config.migrationsPath ?? "./migrations",
9183
+ tableName: config.migrationsTable ?? "schema_migrations"
9184
+ });
9185
+ const result = await migrationManager.up();
9186
+ if (result.success) {
9187
+ console.log(`✅ Applied ${result.value} migration(s)`);
9188
+ } else {
9189
+ console.error("❌ Migration failed:", result.error?.message);
9190
+ process.exit(1);
9191
+ }
9192
+ }
9193
+ process.exit(0);
9194
+ } catch (error) {
9195
+ console.error("❌ Error:", error.message);
9196
+ process.exit(1);
9197
+ }
9198
+ });
8418
9199
  program.parse();
8419
9200
  //# sourceMappingURL=index.js.map
8420
9201
  //# sourceMappingURL=index.js.map