diff --git a/bin/couchbaseorm b/bin/couchbaseorm index 7c34d741..49903b55 100755 --- a/bin/couchbaseorm +++ b/bin/couchbaseorm @@ -20,6 +20,7 @@ def usage couchbaseorm index:migrate couchbaseorm index:rollback couchbaseorm index:status + couchbaseorm index:cleanup couchbaseorm index:schema:dump couchbaseorm index:schema:load couchbaseorm index:dump @@ -47,6 +48,14 @@ when 'index:rollback' CouchbaseOrm::IndexMigrator.rollback when 'index:status' CouchbaseOrm::IndexMigrator.status +when 'index:cleanup' + removed = CouchbaseOrm::IndexMigrator.cleanup + if removed.any? + puts 'Removed indexes:' + removed.each { |name| puts name } + else + puts 'No secondary indexes found' + end when 'index:schema:dump' path = CouchbaseOrm::IndexSchema::Dumper.new.dump puts path diff --git a/lib/couchbase-orm/index_migrator.rb b/lib/couchbase-orm/index_migrator.rb index 1d7d24d8..8d413adf 100644 --- a/lib/couchbase-orm/index_migrator.rb +++ b/lib/couchbase-orm/index_migrator.rb @@ -7,6 +7,10 @@ def migrate(**options) new(**options).migrate end + def cleanup(**options) + new(**options).cleanup + end + def rollback(**options) new(**options).rollback end @@ -65,5 +69,12 @@ def adopt @schema_migration.add_version(migration_def.version) migration_def.version end + + def cleanup + names = IndexMigration::IndexIntrospector.new.indexes.map { |row| row[:name] }.sort + migration = IndexMigration.new + names.each { |name| migration.remove_index(name) } + names + end end end diff --git a/spec/index_migrator_spec.rb b/spec/index_migrator_spec.rb index 41b9e5c4..9712e23e 100644 --- a/spec/index_migrator_spec.rb +++ b/spec/index_migrator_spec.rb @@ -7,6 +7,18 @@ let(:schema_migration) { instance_double(CouchbaseOrm::IndexSchemaMigration) } let(:out) { StringIO.new } + describe '.cleanup' do + it 'delegates to instance cleanup' do + migrator = instance_double(described_class) + allow(described_class).to receive(:new).and_return(migrator) + allow(migrator).to receive(:cleanup).and_return(['date_on_type']) + + result = described_class.cleanup + + expect(result).to eq(['date_on_type']) + end + end + context 'when applying and rolling back migrations' do let(:migration_class) { Class.new { def migrate(_direction); end } } let(:migration_def) do @@ -89,4 +101,38 @@ expect(adopted_version).to be_nil end + + describe '#cleanup' do + it 'drops all introspected non-primary indexes and returns sorted names' do + introspector = instance_double(CouchbaseOrm::IndexMigration::IndexIntrospector) + allow(CouchbaseOrm::IndexMigration::IndexIntrospector).to receive(:new).and_return(introspector) + allow(introspector).to receive(:indexes).and_return([ + { name: 'type_company' }, + { name: 'date_on_type' } + ]) + + migration = instance_double(CouchbaseOrm::IndexMigration) + allow(CouchbaseOrm::IndexMigration).to receive(:new).and_return(migration) + expect(migration).to receive(:remove_index).with('date_on_type').ordered + expect(migration).to receive(:remove_index).with('type_company').ordered + + result = described_class.new(context: context, schema_migration: schema_migration, out: out).cleanup + + expect(result).to eq(%w[date_on_type type_company]) + end + + it 'returns empty array when no secondary indexes are found' do + introspector = instance_double(CouchbaseOrm::IndexMigration::IndexIntrospector) + allow(CouchbaseOrm::IndexMigration::IndexIntrospector).to receive(:new).and_return(introspector) + allow(introspector).to receive(:indexes).and_return([]) + + migration = instance_double(CouchbaseOrm::IndexMigration) + allow(CouchbaseOrm::IndexMigration).to receive(:new).and_return(migration) + expect(migration).not_to receive(:remove_index) + + result = described_class.new(context: context, schema_migration: schema_migration, out: out).cleanup + + expect(result).to eq([]) + end + end end diff --git a/specs/007-add-index-cleanup/plan.md b/specs/007-add-index-cleanup/plan.md new file mode 100644 index 00000000..645467f4 --- /dev/null +++ b/specs/007-add-index-cleanup/plan.md @@ -0,0 +1,139 @@ +# Implementation Plan: Add Index Cleanup Command + +## Goal + +Add a CLI command to remove all secondary indexes from the configured bucket: + +```bash +bundle exec couchbaseorm index:cleanup +``` + +The command must: + +* delete all non-primary indexes; +* keep primary index untouched; +* operate only on the configured bucket; +* return deterministic output for scripting. + +--- + +## Phase 1 — Add Cleanup API to IndexMigrator + +Add class method: + +```ruby +CouchbaseOrm::IndexMigrator.cleanup +``` + +and instance method: + +```ruby +IndexMigrator#cleanup +``` + +`cleanup` should: + +1. introspect indexes through `IndexIntrospector`; +2. extract index names; +3. sort names; +4. drop each index; +5. return the sorted names. + +--- + +## Phase 2 — Reuse Existing Remove Path + +Implement dropping through existing migration execution flow, not ad-hoc queries. + +Expected behavior: + +```ruby +migration = CouchbaseOrm::IndexMigration.new +migration.remove_index(index_name) +``` + +This keeps SQL generation and execution consistent with other operations. + +--- + +## Phase 3 — Add CLI Command in bin/couchbaseorm + +Update usage output to include: + +```text +couchbaseorm index:cleanup +``` + +Add command branch: + +```ruby +when 'index:cleanup' + removed = CouchbaseOrm::IndexMigrator.cleanup +``` + +Output: + +* if any removed: + +```text +Removed indexes: + + +``` + +* if none: + +```text +No secondary indexes found +``` + +--- + +## Phase 4 — Error Behavior + +Preserve existing errors: + +* missing bucket config raises the same configuration error from introspection/query builder; +* failures while dropping an index are not swallowed. + +No retries or partial-failure handling in v1. + +--- + +## Phase 5 — Unit Tests for Migrator + +Add tests for: + +* `IndexMigrator.cleanup` delegates to instance method; +* cleanup drops all introspected indexes; +* cleanup returns sorted index names; +* cleanup returns `[]` when there are no secondary indexes. + +Use doubles for introspector and migration execution where practical. + +--- + +## Phase 6 — CLI Tests + +Add tests for `bin/couchbaseorm` behavior: + +* `index:cleanup` calls `CouchbaseOrm::IndexMigrator.cleanup`; +* command prints removed index names when present; +* command prints `No secondary indexes found` when empty; +* usage text includes `index:cleanup`. + +--- + +## V1 Complete + +Supported: + +* cleanup command for all secondary indexes in configured bucket; +* deterministic deletion order; +* clear CLI output for non-empty and empty cases. + +Not supported: + +* dry-run mode; +* confirmation prompt; +* selective cleanup filters. diff --git a/specs/007-add-index-cleanup/spec.md b/specs/007-add-index-cleanup/spec.md new file mode 100644 index 00000000..79909a14 --- /dev/null +++ b/specs/007-add-index-cleanup/spec.md @@ -0,0 +1,144 @@ +# Spec: Add Index Cleanup Command + +## Motivation + +In development and test environments, index state can drift from migration files. + +Common situations: + +* manually created indexes; +* stale indexes from previous experiments; +* failed migration attempts leaving partial state. + +Current commands support migrate, rollback, dump, and adopt, but there is no single command to reset all secondary indexes in one step. + +CouchbaseORM should provide an explicit cleanup command to delete all non-primary indexes from the configured bucket. + +--- + +# Goals + +* Add a CLI command in bin/couchbaseorm to remove all non-primary indexes. +* Keep primary index untouched. +* Operate only on the configured bucket. +* Reuse existing query/introspection building blocks. +* Provide deterministic output for automation scripts. + +--- + +# Non-Goals + +* Removing primary indexes. +* Creating or rebuilding indexes. +* Comparing with migrations or schema files. +* Automatic confirmation prompts. + +--- + +# Command + +Introduce: + +```bash +bundle exec couchbaseorm index:cleanup +``` + +Update usage output in: + +```text +bin/couchbaseorm +``` + +to include: + +```text +couchbaseorm index:cleanup +``` + +--- + +# Behavior + +`index:cleanup` performs: + +1. Introspect indexes from `system:indexes` for the configured bucket. +2. Ignore primary indexes. +3. Drop each remaining index. +4. Return/print cleaned index names in sorted order. + +If no secondary indexes are found, the command is a no-op and returns an empty result. + +--- + +# Internal API + +Introduce: + +```ruby +CouchbaseOrm::IndexMigrator.cleanup +``` + +and instance method: + +```ruby +IndexMigrator#cleanup +``` + +Implementation outline: + +```ruby +introspected = CouchbaseOrm::IndexMigration::IndexIntrospector.new.indexes +names = introspected.map { |row| row[:name] }.sort + +names.each do |name| + migration = CouchbaseOrm::IndexMigration.new + migration.remove_index(name) +end + +names +``` + +Notes: + +* `IndexIntrospector` already filters out primary indexes. +* `remove_index` uses the existing query builder and execution path. + +--- + +# CLI Output + +When indexes are removed: + +```text +Removed indexes: +date_on_type +type_company +``` + +When there is nothing to remove: + +```text +No secondary indexes found +``` + +--- + +# Error Handling + +* If index bucket configuration is missing, raise the existing bucket configuration error. +* If dropping one index fails, propagate the exception and stop execution. + +This keeps behavior consistent with other index commands. + +--- + +# Tests + +Add coverage for: + +* `IndexMigrator.cleanup` delegates to instance cleanup. +* cleanup drops all introspected non-primary indexes. +* cleanup returns sorted deleted names. +* cleanup returns empty array when no indexes are found. +* CLI command `index:cleanup` triggers cleanup and prints expected output. +* CLI usage text includes `index:cleanup`.