diff --git a/app/Console/Commands/ConversionStatusCommand.php b/app/Console/Commands/ConversionStatusCommand.php
new file mode 100644
index 000000000..9c555cbf3
--- /dev/null
+++ b/app/Console/Commands/ConversionStatusCommand.php
@@ -0,0 +1,106 @@
+ensureConversionsTableExists();
+
+ $this->showConversionStatus();
+
+ return 0;
+ }
+
+ /**
+ * Ensure that the conversions table exists.
+ *
+ * @return void
+ */
+ protected function ensureConversionsTableExists()
+ {
+ if (!DB::connection()->getSchemaBuilder()->hasTable('conversions')) {
+ DB::connection()->getSchemaBuilder()->create('conversions', function ($table) {
+ $table->increments('id');
+ $table->string('conversion')->unique();
+ $table->integer('batch');
+ $table->timestamps();
+ });
+ $this->info('Created conversions table.');
+ }
+ }
+
+ /**
+ * Display the status of each conversion file.
+ *
+ * @return void
+ */
+ protected function showConversionStatus()
+ {
+ // Retrieve all executed conversions with their batch and timestamps
+ $executedConversions = DB::table('conversions')->get();
+
+ // Create a map of executed conversions for quick lookup
+ $executedConversionsMap = $executedConversions->keyBy('conversion');
+
+ $files = File::glob(database_path('conversions') . '/*.php');
+
+ // Sort files for consistent display
+ sort($files);
+
+ $statusData = [];
+
+ foreach ($files as $file) {
+ $fileName = basename($file, '.php');
+
+ if ($executedConversionsMap->has($fileName)) {
+ $status = 'Yes';
+ $batchNumber = $executedConversionsMap->get($fileName)->batch;
+ $executedAt = $executedConversionsMap->get($fileName)->created_at;
+ } else {
+ $status = 'No';
+ $batchNumber = 'N/A';
+ $executedAt = 'N/A';
+ }
+
+ $statusData[] = [
+ 'Ran?' => $status,
+ 'Conversion' => $fileName,
+ 'Batch' => $batchNumber,
+ 'Executed At' => $executedAt,
+ ];
+ }
+
+ if (empty($statusData)) {
+ $this->info('No conversion files found.');
+ return;
+ }
+
+ // Display the table with the additional columns
+ $this->table(['Ran?', 'Conversion', 'Batch', 'Executed At'], $statusData);
+ }
+}
diff --git a/app/Console/Commands/MakeConversionCommand.php b/app/Console/Commands/MakeConversionCommand.php
new file mode 100644
index 000000000..2f98682c5
--- /dev/null
+++ b/app/Console/Commands/MakeConversionCommand.php
@@ -0,0 +1,169 @@
+files = $files;
+ $this->composer = $composer;
+ }
+
+ /**
+ * Execute the console command.
+ *
+ * @return int
+ */
+ public function handle()
+ {
+ // Get the name of the conversion
+ $name = trim($this->argument('name'));
+
+ // Generate the class name
+ $className = $this->getClassName($name);
+
+ // Generate the file path
+ $filePath = $this->getFilePath($name);
+
+ // Build the class content
+ $classContent = $this->buildClass($className);
+
+ // Create the conversion file
+ if ($this->files->exists($filePath)) {
+ $this->error("Conversion {$className} already exists!");
+ return 1;
+ }
+
+ $this->makeDirectory($filePath);
+
+ $this->files->put($filePath, $classContent);
+
+ $this->info("Created Conversion: " . basename($filePath));
+
+ // Dump the autoloads
+ $this->composer->dumpAutoloads();
+
+ return 0;
+ }
+
+ /**
+ * Get the class name from the conversion name.
+ *
+ * @param string $name
+ * @return string
+ */
+ protected function getClassName($name)
+ {
+ return Str::studly($name);
+ }
+
+ /**
+ * Get the file path for the conversion.
+ *
+ * @param string $name
+ * @return string
+ */
+ protected function getFilePath($name)
+ {
+ $timestamp = date('Y_m_d_His');
+ $fileName = $timestamp . '_' . $name . '.php';
+ return database_path('conversions') . '/' . $fileName;
+ }
+
+ /**
+ * Build the class content.
+ *
+ * @param string $className
+ * @return string
+ */
+ protected function buildClass($className)
+ {
+ return <<files->isDirectory($directory)) {
+ $this->files->makeDirectory($directory, 0755, true, true);
+ }
+ }
+}
diff --git a/app/Console/Commands/RunConversionsCommand.php b/app/Console/Commands/RunConversionsCommand.php
new file mode 100644
index 000000000..3839c4bbb
--- /dev/null
+++ b/app/Console/Commands/RunConversionsCommand.php
@@ -0,0 +1,183 @@
+ensureConversionsTableExists();
+
+ if ($this->option('rollback')) {
+ $steps = (int) $this->option('step');
+ $this->rollbackConversions($steps);
+ } else {
+ $this->runConversions();
+ }
+
+ return 0;
+ }
+
+ /**
+ * Ensure that the conversions table exists.
+ *
+ * @return void
+ */
+ protected function ensureConversionsTableExists()
+ {
+ if (!DB::connection()->getSchemaBuilder()->hasTable('conversions')) {
+ DB::connection()->getSchemaBuilder()->create('conversions', function ($table) {
+ $table->increments('id');
+ $table->string('conversion');
+ $table->integer('batch');
+ $table->timestamps();
+ });
+ $this->info('Created conversions table.');
+ }
+ }
+
+ /**
+ * Run all pending conversions.
+ *
+ * @return void
+ */
+ protected function runConversions()
+ {
+ $executed = DB::table('conversions')->pluck('conversion')->toArray();
+ $lastBatch = DB::table('conversions')->max('batch');
+ $batch = $lastBatch ? $lastBatch + 1 : 1;
+
+ $files = File::glob(database_path('conversions') . '/*.php');
+
+ // Sort files to ensure they run in order
+ sort($files);
+
+ foreach ($files as $file) {
+ $fileName = basename($file, '.php');
+
+ if (!in_array($fileName, $executed)) {
+ $this->line("Running: {$fileName}");
+
+ try {
+ $instance = include $file;
+
+ if (!is_object($instance)) {
+ $this->error("The file {$fileName}.php did not return a class instance.");
+ continue;
+ }
+
+ if (!method_exists($instance, 'up')) {
+ $this->error("Method 'up' does not exist in the conversion returned by {$fileName}.php");
+ continue;
+ }
+
+ $instance->up();
+
+ // If no exception occurred, consider it successful
+ DB::table('conversions')->insert([
+ 'conversion' => $fileName,
+ 'batch' => $batch,
+ 'created_at' => now(),
+ 'updated_at' => now(),
+ ]);
+
+ $this->info("Completed: {$fileName}");
+ } catch (\Exception $e) {
+ $this->error("Failed to run {$fileName}: {$e->getMessage()}");
+ }
+ }
+ }
+ }
+
+ /**
+ * Rollback the specified number of batches.
+ *
+ * @param int $steps
+ * @return void
+ */
+ protected function rollbackConversions($steps)
+ {
+ $lastBatch = DB::table('conversions')->max('batch');
+
+ if ($lastBatch === null) {
+ $this->info('No conversions to rollback.');
+ return;
+ }
+
+ $batches = DB::table('conversions')
+ ->select('batch')
+ ->distinct()
+ ->orderBy('batch', 'desc')
+ ->limit($steps)
+ ->pluck('batch')
+ ->toArray();
+
+ if (empty($batches)) {
+ $this->info('No conversions to rollback.');
+ return;
+ }
+
+ foreach ($batches as $batch) {
+ $conversions = DB::table('conversions')
+ ->where('batch', $batch)
+ ->orderBy('id', 'desc')
+ ->get();
+
+ foreach ($conversions as $conversion) {
+ $fileName = $conversion->conversion;
+ $file = database_path('conversions') . '/' . $fileName . '.php';
+
+ if (File::exists($file)) {
+ $this->line("Rolling back: {$fileName}");
+
+ try {
+ $instance = include $file;
+
+ if (!is_object($instance)) {
+ $this->error("The file {$fileName}.php did not return a class instance.");
+ continue;
+ }
+
+ if (!method_exists($instance, 'down')) {
+ $this->error("Method 'down' does not exist in the conversion returned by {$fileName}.php");
+ continue;
+ }
+
+ $instance->down();
+
+ DB::table('conversions')->where('id', $conversion->id)->delete();
+
+ $this->info("Rolled back: {$fileName}");
+ } catch (\Exception $e) {
+ $this->error("Failed to rollback {$fileName}: {$e->getMessage()}");
+ }
+ } else {
+ $this->error("Conversion file {$fileName}.php does not exist.");
+ }
+ }
+ }
+ }
+}
diff --git a/composer.json b/composer.json
index 93bc6fdfa..814376e9f 100644
--- a/composer.json
+++ b/composer.json
@@ -70,7 +70,8 @@
"Database\\Seeders\\": "database/seeders/",
"Database\\Seeders\\Omop\\": "database/seeders/omop",
"Database\\Migrations\\": "database/migrations/",
- "Database\\Beta\\": "database/beta/"
+ "Database\\Beta\\": "database/beta/",
+ "Database\\Conversions\\": "database/conversions/"
}
},
"autoload-dev": {
diff --git a/database/conversions/2024_11_08_135915_example.php b/database/conversions/2024_11_08_135915_example.php
new file mode 100644
index 000000000..0198a9620
--- /dev/null
+++ b/database/conversions/2024_11_08_135915_example.php
@@ -0,0 +1,25 @@
+id();
+ $table->string('conversion')->unique();
+ $table->integer('batch');
+ $table->timestamps();
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::dropIfExists('conversions');
+ }
+};