From b061f41e8d8bba8829b9216f87ce20f5eb7e4494 Mon Sep 17 00:00:00 2001 From: Jose Luis Franco Arza Date: Wed, 25 Mar 2026 16:58:46 +0100 Subject: [PATCH 1/9] Add --async_replication_config option to create/update collection Expose the 14 async replication tuning parameters from weaviate-python-client PR #1953 via a single repeatable --async_replication_config key=value option. This avoids bloating the CLI with 14 individual flags while letting users configure any subset of parameters. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../test_managers/test_collection_manager.py | 116 ++++++++++++++++++ test/unittests/test_utils.py | 59 +++++++++ weaviate_cli/commands/create.py | 21 +++- weaviate_cli/commands/update.py | 17 ++- weaviate_cli/defaults.py | 2 + weaviate_cli/managers/collection_manager.py | 16 +++ weaviate_cli/utils.py | 61 +++++++++ 7 files changed, 290 insertions(+), 2 deletions(-) diff --git a/test/unittests/test_managers/test_collection_manager.py b/test/unittests/test_managers/test_collection_manager.py index 224533e..c9565c0 100644 --- a/test/unittests/test_managers/test_collection_manager.py +++ b/test/unittests/test_managers/test_collection_manager.py @@ -779,6 +779,122 @@ def test_update_collection_with_ttl_disable_type(mock_client, mock_wvc_object_tt mock_wvc_object_ttl["reconfigure"].disable.assert_called_once() +def test_create_collection_with_async_replication_config( + mock_client, mock_wvc_object_ttl +): + """Test creating a collection with async replication config parameters.""" + mock_collections = MagicMock() + mock_client.collections = mock_collections + mock_collections.exists.side_effect = [False, True] + + manager = CollectionManager(mock_client) + + async_config = {"max_workers": 10, "frequency": 60, "propagation_concurrency": 4} + + manager.create_collection( + collection="TestCollection", + replication_factor=3, + vector_index="hnsw", + async_enabled=True, + async_replication_config=async_config, + ) + + mock_collections.create.assert_called_once() + create_call_kwargs = mock_collections.create.call_args.kwargs + assert create_call_kwargs["name"] == "TestCollection" + assert create_call_kwargs["replication_config"].asyncEnabled is True + # Verify async_config is set on the replication config + repl_config = create_call_kwargs["replication_config"] + assert repl_config.asyncConfig is not None + assert repl_config.asyncConfig.maxWorkers == 10 + assert repl_config.asyncConfig.frequency == 60 + assert repl_config.asyncConfig.propagationConcurrency == 4 + + +def test_create_collection_without_async_replication_config( + mock_client, mock_wvc_object_ttl +): + """Test creating a collection without async replication config passes None.""" + mock_collections = MagicMock() + mock_client.collections = mock_collections + mock_collections.exists.side_effect = [False, True] + + manager = CollectionManager(mock_client) + + manager.create_collection( + collection="TestCollection", + replication_factor=3, + vector_index="hnsw", + async_enabled=True, + ) + + mock_collections.create.assert_called_once() + repl_config = mock_collections.create.call_args.kwargs["replication_config"] + assert repl_config.asyncConfig is None + + +def test_update_collection_with_async_replication_config( + mock_client, mock_wvc_object_ttl +): + """Test updating a collection with async replication config parameters.""" + mock_collections = MagicMock() + mock_client.collections = mock_collections + mock_client.collections.exists.side_effect = [True, True] + + mock_collection = MagicMock() + mock_client.collections.get.return_value = mock_collection + mock_collection.config.get.return_value = MagicMock( + replication_config=MagicMock(factor=3), + multi_tenancy_config=MagicMock( + enabled=False, auto_tenant_creation=False, auto_tenant_activation=False + ), + ) + + manager = CollectionManager(mock_client) + + async_config = {"max_workers": 20, "propagation_batch_size": 100} + + manager.update_collection( + collection="TestCollection", + async_replication_config=async_config, + ) + + mock_collection.config.update.assert_called_once() + repl_config = mock_collection.config.update.call_args.kwargs["replication_config"] + assert repl_config.asyncConfig is not None + assert repl_config.asyncConfig.maxWorkers == 20 + assert repl_config.asyncConfig.propagationBatchSize == 100 + + +def test_update_collection_without_async_replication_config( + mock_client, mock_wvc_object_ttl +): + """Test updating a collection without async replication config passes None.""" + mock_collections = MagicMock() + mock_client.collections = mock_collections + mock_client.collections.exists.side_effect = [True, True] + + mock_collection = MagicMock() + mock_client.collections.get.return_value = mock_collection + mock_collection.config.get.return_value = MagicMock( + replication_config=MagicMock(factor=3), + multi_tenancy_config=MagicMock( + enabled=False, auto_tenant_creation=False, auto_tenant_activation=False + ), + ) + + manager = CollectionManager(mock_client) + + manager.update_collection( + collection="TestCollection", + description="Updated", + ) + + mock_collection.config.update.assert_called_once() + repl_config = mock_collection.config.update.call_args.kwargs["replication_config"] + assert repl_config.asyncConfig is None + + def test_create_collection_with_hfresh_defaults(mock_client, mock_wvc_object_ttl): """Test creating a collection with hfresh vector index using default parameters.""" mock_collections = MagicMock() diff --git a/test/unittests/test_utils.py b/test/unittests/test_utils.py index 02f9f38..ee70113 100644 --- a/test/unittests/test_utils.py +++ b/test/unittests/test_utils.py @@ -5,6 +5,7 @@ get_random_string, pp_objects, parse_permission, + parse_async_replication_config, ) from weaviate.collections import Collection from io import StringIO @@ -386,3 +387,61 @@ def test_parse_permission_invalid(): with pytest.raises(ValueError, match="Invalid permission action: update_nodes"): parse_permission("update_nodes") + + +def test_parse_async_replication_config_none(): + assert parse_async_replication_config(None) is None + + +def test_parse_async_replication_config_empty(): + assert parse_async_replication_config(()) is None + + +def test_parse_async_replication_config_single_key(): + result = parse_async_replication_config(("max_workers=10",)) + assert result == {"max_workers": 10} + + +def test_parse_async_replication_config_multiple_keys(): + result = parse_async_replication_config( + ("max_workers=10", "frequency=60", "propagation_concurrency=4") + ) + assert result == {"max_workers": 10, "frequency": 60, "propagation_concurrency": 4} + + +def test_parse_async_replication_config_all_keys(): + all_keys = ( + "max_workers=1", + "hashtree_height=2", + "frequency=3", + "frequency_while_propagating=4", + "alive_nodes_checking_frequency=5", + "logging_frequency=6", + "diff_batch_size=7", + "diff_per_node_timeout=8", + "pre_propagation_timeout=9", + "propagation_timeout=10", + "propagation_limit=11", + "propagation_delay=12", + "propagation_concurrency=13", + "propagation_batch_size=14", + ) + result = parse_async_replication_config(all_keys) + assert len(result) == 14 + assert result["max_workers"] == 1 + assert result["propagation_batch_size"] == 14 + + +def test_parse_async_replication_config_invalid_key(): + with pytest.raises(ValueError, match="Unknown async replication config key"): + parse_async_replication_config(("invalid_key=10",)) + + +def test_parse_async_replication_config_invalid_value(): + with pytest.raises(ValueError, match="Must be an integer"): + parse_async_replication_config(("max_workers=abc",)) + + +def test_parse_async_replication_config_missing_equals(): + with pytest.raises(ValueError, match="Expected key=value"): + parse_async_replication_config(("max_workers",)) diff --git a/weaviate_cli/commands/create.py b/weaviate_cli/commands/create.py index c4bb2bc..baafff7 100644 --- a/weaviate_cli/commands/create.py +++ b/weaviate_cli/commands/create.py @@ -12,7 +12,11 @@ from weaviate_cli.managers.alias_manager import AliasManager from weaviate_cli.managers.backup_manager import BackupManager from weaviate_cli.managers.export_manager import ExportManager -from weaviate_cli.utils import get_client_from_context, get_async_client_from_context +from weaviate_cli.utils import ( + get_client_from_context, + get_async_client_from_context, + parse_async_replication_config, +) from weaviate_cli.managers.collection_manager import CollectionManager from weaviate_cli.managers.tenant_manager import TenantManager from weaviate_cli.managers.data_manager import DataManager @@ -217,6 +221,17 @@ def create() -> None: type=int, help="Rescore limit (default: None, set by Weaviate server).", ) +@click.option( + "--async_replication_config", + multiple=True, + help=( + "Async replication config as key=value pairs. Can be specified multiple times. " + "Valid keys: max_workers, hashtree_height, frequency, frequency_while_propagating, " + "alive_nodes_checking_frequency, logging_frequency, diff_batch_size, diff_per_node_timeout, " + "pre_propagation_timeout, propagation_timeout, propagation_limit, propagation_delay, " + "propagation_concurrency, propagation_batch_size. All values must be integers." + ), +) @click.pass_context def create_collection_cli( ctx: click.Context, @@ -246,6 +261,7 @@ def create_collection_cli( object_ttl_time: Optional[int], object_ttl_filter_expired: bool, object_ttl_property_name: Optional[str], + async_replication_config: tuple, ) -> None: """Create a collection in Weaviate.""" @@ -291,6 +307,9 @@ def create_collection_cli( object_ttl_time=object_ttl_time, object_ttl_filter_expired=object_ttl_filter_expired, object_ttl_property_name=object_ttl_property_name, + async_replication_config=parse_async_replication_config( + async_replication_config + ), ) except Exception as e: click.echo(f"Error: {e}") diff --git a/weaviate_cli/commands/update.py b/weaviate_cli/commands/update.py index 15c41b0..c992750 100644 --- a/weaviate_cli/commands/update.py +++ b/weaviate_cli/commands/update.py @@ -7,7 +7,7 @@ from weaviate_cli.managers.alias_manager import AliasManager from weaviate_cli.managers.tenant_manager import TenantManager from weaviate_cli.managers.user_manager import UserManager -from weaviate_cli.utils import get_client_from_context +from weaviate_cli.utils import get_client_from_context, parse_async_replication_config from weaviate_cli.managers.collection_manager import CollectionManager from weaviate_cli.managers.shard_manager import ShardManager from weaviate_cli.managers.data_manager import DataManager @@ -110,6 +110,17 @@ def update() -> None: type=str, help="Date property name for TTL when object_ttl_type is 'property' (default: 'releaseDate'). Only valid when --object_ttl_type=property.", ) +@click.option( + "--async_replication_config", + multiple=True, + help=( + "Async replication config as key=value pairs. Can be specified multiple times. " + "Valid keys: max_workers, hashtree_height, frequency, frequency_while_propagating, " + "alive_nodes_checking_frequency, logging_frequency, diff_batch_size, diff_per_node_timeout, " + "pre_propagation_timeout, propagation_timeout, propagation_limit, propagation_delay, " + "propagation_concurrency, propagation_batch_size. All values must be integers." + ), +) @click.pass_context def update_collection_cli( ctx: click.Context, @@ -127,6 +138,7 @@ def update_collection_cli( object_ttl_time: Optional[int], object_ttl_filter_expired: bool, object_ttl_property_name: Optional[str], + async_replication_config: tuple, ) -> None: """Update a collection in Weaviate.""" @@ -160,6 +172,9 @@ def update_collection_cli( object_ttl_time=object_ttl_time, object_ttl_filter_expired=object_ttl_filter_expired, object_ttl_property_name=object_ttl_property_name, + async_replication_config=parse_async_replication_config( + async_replication_config + ), ) except Exception as e: click.echo(f"Error: {e}") diff --git a/weaviate_cli/defaults.py b/weaviate_cli/defaults.py index 55e4cc5..9e345a0 100644 --- a/weaviate_cli/defaults.py +++ b/weaviate_cli/defaults.py @@ -87,6 +87,7 @@ class CreateCollectionDefaults: object_ttl_time: Optional[int] = None object_ttl_filter_expired: Optional[bool] = None object_ttl_property_name: str = "releaseDate" + async_replication_config: Optional[tuple] = None @dataclass @@ -263,6 +264,7 @@ class UpdateCollectionDefaults: object_ttl_time: Optional[int] = None object_ttl_filter_expired: Optional[bool] = None object_ttl_property_name: str = "releaseDate" + async_replication_config: Optional[tuple] = None @dataclass diff --git a/weaviate_cli/managers/collection_manager.py b/weaviate_cli/managers/collection_manager.py index 2f524a7..0ae9478 100644 --- a/weaviate_cli/managers/collection_manager.py +++ b/weaviate_cli/managers/collection_manager.py @@ -234,6 +234,7 @@ def create_collection( object_ttl_property_name: Optional[ str ] = CreateCollectionDefaults.object_ttl_property_name, + async_replication_config: Optional[Dict[str, int]] = None, ) -> None: if ( @@ -568,6 +569,13 @@ def create_collection( if replication_deletion_strategy else None ), + async_config=( + wvc.Configure.Replication.async_config( + **async_replication_config + ) + if async_replication_config + else None + ), ), sharding_config=( wvc.Configure.sharding(desired_count=shards) if shards > 0 else None @@ -634,6 +642,7 @@ def update_collection( object_ttl_property_name: Optional[ str ] = UpdateCollectionDefaults.object_ttl_property_name, + async_replication_config: Optional[Dict[str, int]] = None, ) -> None: if ( @@ -729,6 +738,13 @@ def update_collection( if replication_deletion_strategy else None ), + async_config=( + wvc.Reconfigure.Replication.async_config( + **async_replication_config + ) + if async_replication_config + else None + ), ) ), multi_tenancy_config=( diff --git a/weaviate_cli/utils.py b/weaviate_cli/utils.py index a1b75d9..d9d4d06 100644 --- a/weaviate_cli/utils.py +++ b/weaviate_cli/utils.py @@ -116,6 +116,67 @@ def pp_objects(response, main_properties, json_output: bool = False): print(f"Total: {len(objects)} objects") +ASYNC_REPLICATION_CONFIG_KEYS = { + "max_workers", + "hashtree_height", + "frequency", + "frequency_while_propagating", + "alive_nodes_checking_frequency", + "logging_frequency", + "diff_batch_size", + "diff_per_node_timeout", + "pre_propagation_timeout", + "propagation_timeout", + "propagation_limit", + "propagation_delay", + "propagation_concurrency", + "propagation_batch_size", +} + + +def parse_async_replication_config( + config_tuples: Optional[tuple], +) -> Optional[dict]: + """Parse async replication config key=value tuples into a dict. + + Args: + config_tuples: Tuple of "key=value" strings, e.g. ("max_workers=10", "frequency=60") + + Returns: + Dict mapping parameter names to integer values, or None if empty/None. + + Raises: + ValueError: If a key is unknown or a value is not a valid integer. + """ + if not config_tuples: + return None + + result = {} + for item in config_tuples: + if "=" not in item: + raise ValueError( + f"Invalid async replication config format: '{item}'. Expected key=value." + ) + key, value = item.split("=", 1) + key = key.strip() + value = value.strip() + + if key not in ASYNC_REPLICATION_CONFIG_KEYS: + raise ValueError( + f"Unknown async replication config key: '{key}'. " + f"Valid keys: {', '.join(sorted(ASYNC_REPLICATION_CONFIG_KEYS))}" + ) + + try: + result[key] = int(value) + except ValueError: + raise ValueError( + f"Invalid value for '{key}': '{value}'. Must be an integer." + ) + + return result if result else None + + def parse_permission(perm: str) -> PermissionsCreateType: """ Convert a permission string to RBAC permission object(s). From 5f5ab38b0387a0b484283536dbbe1d6364552708 Mon Sep 17 00:00:00 2001 From: Jose Luis Franco Arza Date: Wed, 25 Mar 2026 17:03:14 +0100 Subject: [PATCH 2/9] Update operating skill docs with --async_replication_config Co-Authored-By: Claude Opus 4.6 (1M context) --- .../skills/operating-weaviate-cli/SKILL.md | 21 ++++++++++++++-- .../references/collections.md | 24 ++++++++++++++++++- 2 files changed, 42 insertions(+), 3 deletions(-) diff --git a/.claude/skills/operating-weaviate-cli/SKILL.md b/.claude/skills/operating-weaviate-cli/SKILL.md index 013654b..f92e181 100644 --- a/.claude/skills/operating-weaviate-cli/SKILL.md +++ b/.claude/skills/operating-weaviate-cli/SKILL.md @@ -139,9 +139,26 @@ weaviate-cli delete collection --collection MyCollection --json weaviate-cli delete collection --all --json ``` -Key create options: `--multitenant`, `--auto_tenant_creation`, `--auto_tenant_activation`, `--shards N`, `--vectorizer `, `--named_vector`, `--replication_deletion_strategy`, `--object_ttl_type`, `--object_ttl_time`, `--object_ttl_filter_expired`, `--object_ttl_property_name` (only when `object_ttl_type=property`) +Key create options: `--multitenant`, `--auto_tenant_creation`, `--auto_tenant_activation`, `--shards N`, `--vectorizer `, `--named_vector`, `--replication_deletion_strategy`, `--async_replication_config key=value` (repeatable, requires Weaviate >= v1.36.0), `--object_ttl_type`, `--object_ttl_time`, `--object_ttl_filter_expired`, `--object_ttl_property_name` (only when `object_ttl_type=property`) -Mutable fields: `--async_enabled`, `--replication_factor`, `--vector_index`, `--description`, `--training_limit`, `--auto_tenant_creation`, `--auto_tenant_activation`, `--replication_deletion_strategy`, `--object_ttl_type`, `--object_ttl_time`, `--object_ttl_filter_expired`, `--object_ttl_property_name` (only when `object_ttl_type=property`) +Mutable fields: `--async_enabled`, `--replication_factor`, `--vector_index`, `--description`, `--training_limit`, `--auto_tenant_creation`, `--auto_tenant_activation`, `--replication_deletion_strategy`, `--async_replication_config key=value` (repeatable), `--object_ttl_type`, `--object_ttl_time`, `--object_ttl_filter_expired`, `--object_ttl_property_name` (only when `object_ttl_type=property`) + +#### Async Replication Config + +```bash +# Create with async replication tuning (requires --async_enabled and Weaviate >= v1.36.0) +weaviate-cli create collection --collection MyCol --async_enabled \ + --async_replication_config max_workers=10 \ + --async_replication_config frequency=60 \ + --async_replication_config propagation_concurrency=4 --json + +# Update async replication config on existing collection +weaviate-cli update collection --collection MyCol \ + --async_replication_config max_workers=20 \ + --async_replication_config propagation_batch_size=100 --json +``` + +Valid keys (all integers): `max_workers`, `hashtree_height`, `frequency`, `frequency_while_propagating`, `alive_nodes_checking_frequency`, `logging_frequency`, `diff_batch_size`, `diff_per_node_timeout`, `pre_propagation_timeout`, `propagation_timeout`, `propagation_limit`, `propagation_delay`, `propagation_concurrency`, `propagation_batch_size` #### Object TTL diff --git a/.claude/skills/operating-weaviate-cli/references/collections.md b/.claude/skills/operating-weaviate-cli/references/collections.md index 6e801e5..edb25e0 100644 --- a/.claude/skills/operating-weaviate-cli/references/collections.md +++ b/.claude/skills/operating-weaviate-cli/references/collections.md @@ -48,6 +48,7 @@ weaviate-cli create collection \ - `--hfresh_search_probe` -- (hfresh only) Search probe size (default: None, uses server default) - `--distance_metric` -- Distance metric: cosine, dot, l2-squared, hamming, manhattan (default: None, uses server default). Applies to all vector index types. - `--rescore_limit` -- Rescore limit for quantized indexes (default: None, uses server default) +- `--async_replication_config` -- Async replication tuning as `key=value` pairs (repeatable). Valid keys: `max_workers`, `hashtree_height`, `frequency`, `frequency_while_propagating`, `alive_nodes_checking_frequency`, `logging_frequency`, `diff_batch_size`, `diff_per_node_timeout`, `pre_propagation_timeout`, `propagation_timeout`, `propagation_limit`, `propagation_delay`, `propagation_concurrency`, `propagation_batch_size`. All values must be integers. Requires Weaviate >= v1.36.0. **hfresh examples:** ```bash @@ -66,6 +67,19 @@ weaviate-cli create collection \ --json ``` +**Async replication config examples:** +```bash +# Create with custom async replication tuning +weaviate-cli create collection --collection MyCol --async_enabled \ + --async_replication_config max_workers=10 \ + --async_replication_config frequency=60 \ + --async_replication_config propagation_concurrency=4 + +# Set a single parameter +weaviate-cli create collection --collection MyCol --async_enabled \ + --async_replication_config propagation_batch_size=200 +``` + **Object TTL examples:** ```bash # Delete objects 1 hour after creation @@ -90,10 +104,18 @@ weaviate-cli update collection \ --json ``` -Mutable fields: `--async_enabled`, `--replication_factor`, `--vector_index`, `--description`, `--training_limit`, `--auto_tenant_creation`, `--auto_tenant_activation`, `--replication_deletion_strategy`, `--object_ttl_type`, `--object_ttl_time`, `--object_ttl_filter_expired`, `--object_ttl_property_name` (only when `object_ttl_type=property`) +Mutable fields: `--async_enabled`, `--replication_factor`, `--vector_index`, `--description`, `--training_limit`, `--auto_tenant_creation`, `--auto_tenant_activation`, `--replication_deletion_strategy`, `--async_replication_config`, `--object_ttl_type`, `--object_ttl_time`, `--object_ttl_filter_expired`, `--object_ttl_property_name` (only when `object_ttl_type=property`) **Immutable (cannot change after creation):** multitenant, vectorizer, named_vector, shards +**Async replication config examples (update):** +```bash +# Update async replication tuning on existing collection +weaviate-cli update collection --collection MyCol \ + --async_replication_config max_workers=20 \ + --async_replication_config propagation_batch_size=100 +``` + **Object TTL options for update:** - `--object_ttl_type` -- TTL event type: create, update, property, **disable** (default: "create") - `--object_ttl_time` -- Time to live in seconds (set together with type to enable TTL) From 91e4deca063746f890237cce322407cf6a4ef9ad Mon Sep 17 00:00:00 2001 From: Jose Luis Franco Arza Date: Wed, 25 Mar 2026 17:56:38 +0100 Subject: [PATCH 3/9] Address PR review feedback (round 1) - Reject --async_replication_config when --async_enabled is not set (create) - Reject --async_replication_config when --async_enabled is False (update) - Warn when server version is older than v1.36.0 - Generate Click help text from ASYNC_REPLICATION_CONFIG_KEYS constant - Add validation tests for both create and update paths Co-Authored-By: Claude Opus 4.6 (1M context) --- .../test_managers/test_collection_manager.py | 44 +++++++++++++++++++ weaviate_cli/commands/create.py | 9 +--- weaviate_cli/commands/update.py | 14 +++--- weaviate_cli/managers/collection_manager.py | 24 +++++++++- weaviate_cli/utils.py | 7 +++ 5 files changed, 82 insertions(+), 16 deletions(-) diff --git a/test/unittests/test_managers/test_collection_manager.py b/test/unittests/test_managers/test_collection_manager.py index c9565c0..a26ede9 100644 --- a/test/unittests/test_managers/test_collection_manager.py +++ b/test/unittests/test_managers/test_collection_manager.py @@ -786,6 +786,7 @@ def test_create_collection_with_async_replication_config( mock_collections = MagicMock() mock_client.collections = mock_collections mock_collections.exists.side_effect = [False, True] + mock_client.get_meta.return_value = {"version": "1.36.0"} manager = CollectionManager(mock_client) @@ -833,6 +834,28 @@ def test_create_collection_without_async_replication_config( assert repl_config.asyncConfig is None +def test_create_collection_async_replication_config_requires_async_enabled( + mock_client, +): + """Test that async_replication_config is rejected when async_enabled is False.""" + mock_collections = MagicMock() + mock_client.collections = mock_collections + mock_collections.exists.return_value = False + + manager = CollectionManager(mock_client) + + with pytest.raises(Exception, match="requires --async_enabled"): + manager.create_collection( + collection="TestCollection", + replication_factor=3, + vector_index="hnsw", + async_enabled=False, + async_replication_config={"max_workers": 10}, + ) + + mock_collections.create.assert_not_called() + + def test_update_collection_with_async_replication_config( mock_client, mock_wvc_object_ttl ): @@ -840,6 +863,7 @@ def test_update_collection_with_async_replication_config( mock_collections = MagicMock() mock_client.collections = mock_collections mock_client.collections.exists.side_effect = [True, True] + mock_client.get_meta.return_value = {"version": "1.36.0"} mock_collection = MagicMock() mock_client.collections.get.return_value = mock_collection @@ -976,3 +1000,23 @@ def test_create_collection_with_hfresh_invalid_distance_metric( assert "Invalid distance_metric: 'invalid_metric'" in str(exc_info.value) mock_collections.create.assert_not_called() + + +def test_update_collection_async_replication_config_rejected_when_async_false( + mock_client, +): + """Test that async_replication_config is rejected when async_enabled is explicitly False.""" + mock_collections = MagicMock() + mock_client.collections = mock_collections + mock_client.collections.exists.return_value = True + + manager = CollectionManager(mock_client) + + with pytest.raises(Exception, match="cannot be used when --async_enabled is False"): + manager.update_collection( + collection="TestCollection", + async_enabled=False, + async_replication_config={"max_workers": 10}, + ) + + mock_collections.get.return_value.config.update.assert_not_called() diff --git a/weaviate_cli/commands/create.py b/weaviate_cli/commands/create.py index baafff7..c580aba 100644 --- a/weaviate_cli/commands/create.py +++ b/weaviate_cli/commands/create.py @@ -16,6 +16,7 @@ get_client_from_context, get_async_client_from_context, parse_async_replication_config, + ASYNC_REPLICATION_CONFIG_HELP, ) from weaviate_cli.managers.collection_manager import CollectionManager from weaviate_cli.managers.tenant_manager import TenantManager @@ -224,13 +225,7 @@ def create() -> None: @click.option( "--async_replication_config", multiple=True, - help=( - "Async replication config as key=value pairs. Can be specified multiple times. " - "Valid keys: max_workers, hashtree_height, frequency, frequency_while_propagating, " - "alive_nodes_checking_frequency, logging_frequency, diff_batch_size, diff_per_node_timeout, " - "pre_propagation_timeout, propagation_timeout, propagation_limit, propagation_delay, " - "propagation_concurrency, propagation_batch_size. All values must be integers." - ), + help=ASYNC_REPLICATION_CONFIG_HELP, ) @click.pass_context def create_collection_cli( diff --git a/weaviate_cli/commands/update.py b/weaviate_cli/commands/update.py index c992750..daaa730 100644 --- a/weaviate_cli/commands/update.py +++ b/weaviate_cli/commands/update.py @@ -7,7 +7,11 @@ from weaviate_cli.managers.alias_manager import AliasManager from weaviate_cli.managers.tenant_manager import TenantManager from weaviate_cli.managers.user_manager import UserManager -from weaviate_cli.utils import get_client_from_context, parse_async_replication_config +from weaviate_cli.utils import ( + get_client_from_context, + parse_async_replication_config, + ASYNC_REPLICATION_CONFIG_HELP, +) from weaviate_cli.managers.collection_manager import CollectionManager from weaviate_cli.managers.shard_manager import ShardManager from weaviate_cli.managers.data_manager import DataManager @@ -113,13 +117,7 @@ def update() -> None: @click.option( "--async_replication_config", multiple=True, - help=( - "Async replication config as key=value pairs. Can be specified multiple times. " - "Valid keys: max_workers, hashtree_height, frequency, frequency_while_propagating, " - "alive_nodes_checking_frequency, logging_frequency, diff_batch_size, diff_per_node_timeout, " - "pre_propagation_timeout, propagation_timeout, propagation_limit, propagation_delay, " - "propagation_concurrency, propagation_batch_size. All values must be integers." - ), + help=ASYNC_REPLICATION_CONFIG_HELP, ) @click.pass_context def update_collection_cli( diff --git a/weaviate_cli/managers/collection_manager.py b/weaviate_cli/managers/collection_manager.py index 0ae9478..a08b839 100644 --- a/weaviate_cli/managers/collection_manager.py +++ b/weaviate_cli/managers/collection_manager.py @@ -12,7 +12,7 @@ DeleteCollectionDefaults, GetCollectionDefaults, ) -from weaviate_cli.utils import print_json_or_text +from weaviate_cli.utils import print_json_or_text, older_than_version import weaviate.classes.config as wvc from prettytable import PrettyTable @@ -251,6 +251,17 @@ def create_collection( f"Error: Collection '{collection}' already exists in Weaviate. Delete using command." ) + if async_replication_config and not async_enabled: + raise Exception( + "Error: --async_replication_config requires --async_enabled to be set." + ) + + if async_replication_config and older_than_version(self.client, "1.36.0"): + click.echo( + "Warning: --async_replication_config requires Weaviate >= v1.36.0. " + "The server may ignore or reject these settings." + ) + if named_vector_name != "default" and not named_vector: raise Exception( "Error: Named vector name is only supported with named vectors. Please use --named_vector to enable named vectors." @@ -653,6 +664,17 @@ def update_collection( raise Exception( "object_ttl_property_name is only valid when object_ttl_type is 'property'." ) + if async_replication_config and async_enabled is False: + raise Exception( + "Error: --async_replication_config cannot be used when --async_enabled is False." + ) + + if async_replication_config and older_than_version(self.client, "1.36.0"): + click.echo( + "Warning: --async_replication_config requires Weaviate >= v1.36.0. " + "The server may ignore or reject these settings." + ) + if not self.client.collections.exists(collection): raise Exception( diff --git a/weaviate_cli/utils.py b/weaviate_cli/utils.py index d9d4d06..a958094 100644 --- a/weaviate_cli/utils.py +++ b/weaviate_cli/utils.py @@ -134,6 +134,13 @@ def pp_objects(response, main_properties, json_output: bool = False): } +ASYNC_REPLICATION_CONFIG_HELP = ( + "Async replication config as key=value pairs. Can be specified multiple times. " + "Valid keys: " + ", ".join(sorted(ASYNC_REPLICATION_CONFIG_KEYS)) + ". " + "All values must be integers." +) + + def parse_async_replication_config( config_tuples: Optional[tuple], ) -> Optional[dict]: From ffcbd12d1691d113da9fe61dd03e17c21b24e7ca Mon Sep 17 00:00:00 2001 From: Jose Luis Franco Arza Date: Thu, 26 Mar 2026 14:49:06 +0100 Subject: [PATCH 4/9] Address PR review feedback (round 2) - Add --async_enabled prerequisite to help text, SKILL.md, and collections.md - Point weaviate-client dependency to jose/fix-async-repl-config-get branch - Update setup.cfg install_requires to match Co-Authored-By: Claude Opus 4.6 (1M context) --- .claude/skills/operating-weaviate-cli/SKILL.md | 2 +- .../skills/operating-weaviate-cli/references/collections.md | 2 +- weaviate_cli/utils.py | 3 ++- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.claude/skills/operating-weaviate-cli/SKILL.md b/.claude/skills/operating-weaviate-cli/SKILL.md index f92e181..ef90b83 100644 --- a/.claude/skills/operating-weaviate-cli/SKILL.md +++ b/.claude/skills/operating-weaviate-cli/SKILL.md @@ -139,7 +139,7 @@ weaviate-cli delete collection --collection MyCollection --json weaviate-cli delete collection --all --json ``` -Key create options: `--multitenant`, `--auto_tenant_creation`, `--auto_tenant_activation`, `--shards N`, `--vectorizer `, `--named_vector`, `--replication_deletion_strategy`, `--async_replication_config key=value` (repeatable, requires Weaviate >= v1.36.0), `--object_ttl_type`, `--object_ttl_time`, `--object_ttl_filter_expired`, `--object_ttl_property_name` (only when `object_ttl_type=property`) +Key create options: `--multitenant`, `--auto_tenant_creation`, `--auto_tenant_activation`, `--shards N`, `--vectorizer `, `--named_vector`, `--replication_deletion_strategy`, `--async_replication_config key=value` (repeatable; requires `--async_enabled` and Weaviate >= v1.36.0), `--object_ttl_type`, `--object_ttl_time`, `--object_ttl_filter_expired`, `--object_ttl_property_name` (only when `object_ttl_type=property`) Mutable fields: `--async_enabled`, `--replication_factor`, `--vector_index`, `--description`, `--training_limit`, `--auto_tenant_creation`, `--auto_tenant_activation`, `--replication_deletion_strategy`, `--async_replication_config key=value` (repeatable), `--object_ttl_type`, `--object_ttl_time`, `--object_ttl_filter_expired`, `--object_ttl_property_name` (only when `object_ttl_type=property`) diff --git a/.claude/skills/operating-weaviate-cli/references/collections.md b/.claude/skills/operating-weaviate-cli/references/collections.md index edb25e0..408faed 100644 --- a/.claude/skills/operating-weaviate-cli/references/collections.md +++ b/.claude/skills/operating-weaviate-cli/references/collections.md @@ -48,7 +48,7 @@ weaviate-cli create collection \ - `--hfresh_search_probe` -- (hfresh only) Search probe size (default: None, uses server default) - `--distance_metric` -- Distance metric: cosine, dot, l2-squared, hamming, manhattan (default: None, uses server default). Applies to all vector index types. - `--rescore_limit` -- Rescore limit for quantized indexes (default: None, uses server default) -- `--async_replication_config` -- Async replication tuning as `key=value` pairs (repeatable). Valid keys: `max_workers`, `hashtree_height`, `frequency`, `frequency_while_propagating`, `alive_nodes_checking_frequency`, `logging_frequency`, `diff_batch_size`, `diff_per_node_timeout`, `pre_propagation_timeout`, `propagation_timeout`, `propagation_limit`, `propagation_delay`, `propagation_concurrency`, `propagation_batch_size`. All values must be integers. Requires Weaviate >= v1.36.0. +- `--async_replication_config` -- Async replication tuning as `key=value` pairs (repeatable). Valid keys: `max_workers`, `hashtree_height`, `frequency`, `frequency_while_propagating`, `alive_nodes_checking_frequency`, `logging_frequency`, `diff_batch_size`, `diff_per_node_timeout`, `pre_propagation_timeout`, `propagation_timeout`, `propagation_limit`, `propagation_delay`, `propagation_concurrency`, `propagation_batch_size`. All values must be integers. Requires `--async_enabled` on create and Weaviate >= v1.36.0. **hfresh examples:** ```bash diff --git a/weaviate_cli/utils.py b/weaviate_cli/utils.py index a958094..0b84b69 100644 --- a/weaviate_cli/utils.py +++ b/weaviate_cli/utils.py @@ -137,7 +137,8 @@ def pp_objects(response, main_properties, json_output: bool = False): ASYNC_REPLICATION_CONFIG_HELP = ( "Async replication config as key=value pairs. Can be specified multiple times. " "Valid keys: " + ", ".join(sorted(ASYNC_REPLICATION_CONFIG_KEYS)) + ". " - "All values must be integers." + "All values must be integers. " + "Requires --async_enabled on create and Weaviate >= v1.36.0." ) From 96ce35db7590b6615ee114bce2be0599d9e94dd2 Mon Sep 17 00:00:00 2001 From: Jose Luis Franco Arza Date: Thu, 26 Mar 2026 14:49:06 +0100 Subject: [PATCH 5/9] Address PR review feedback (round 2) - Add --async_enabled prerequisite to help text, SKILL.md, and collections.md - Point weaviate-client dependency to jose/fix-async-repl-config-get branch - Update setup.cfg install_requires to match - Change minimum version to 1.34.18 Co-Authored-By: Claude Opus 4.6 (1M context) --- .claude/skills/operating-weaviate-cli/SKILL.md | 4 ++-- .../operating-weaviate-cli/references/collections.md | 2 +- weaviate_cli/managers/collection_manager.py | 8 ++++---- weaviate_cli/utils.py | 3 ++- 4 files changed, 9 insertions(+), 8 deletions(-) diff --git a/.claude/skills/operating-weaviate-cli/SKILL.md b/.claude/skills/operating-weaviate-cli/SKILL.md index ef90b83..f7fb6fb 100644 --- a/.claude/skills/operating-weaviate-cli/SKILL.md +++ b/.claude/skills/operating-weaviate-cli/SKILL.md @@ -139,14 +139,14 @@ weaviate-cli delete collection --collection MyCollection --json weaviate-cli delete collection --all --json ``` -Key create options: `--multitenant`, `--auto_tenant_creation`, `--auto_tenant_activation`, `--shards N`, `--vectorizer `, `--named_vector`, `--replication_deletion_strategy`, `--async_replication_config key=value` (repeatable; requires `--async_enabled` and Weaviate >= v1.36.0), `--object_ttl_type`, `--object_ttl_time`, `--object_ttl_filter_expired`, `--object_ttl_property_name` (only when `object_ttl_type=property`) +Key create options: `--multitenant`, `--auto_tenant_creation`, `--auto_tenant_activation`, `--shards N`, `--vectorizer `, `--named_vector`, `--replication_deletion_strategy`, `--async_replication_config key=value` (repeatable; requires `--async_enabled` and Weaviate >= v1.34.18), `--object_ttl_type`, `--object_ttl_time`, `--object_ttl_filter_expired`, `--object_ttl_property_name` (only when `object_ttl_type=property`) Mutable fields: `--async_enabled`, `--replication_factor`, `--vector_index`, `--description`, `--training_limit`, `--auto_tenant_creation`, `--auto_tenant_activation`, `--replication_deletion_strategy`, `--async_replication_config key=value` (repeatable), `--object_ttl_type`, `--object_ttl_time`, `--object_ttl_filter_expired`, `--object_ttl_property_name` (only when `object_ttl_type=property`) #### Async Replication Config ```bash -# Create with async replication tuning (requires --async_enabled and Weaviate >= v1.36.0) +# Create with async replication tuning (requires --async_enabled and Weaviate >= v1.34.18) weaviate-cli create collection --collection MyCol --async_enabled \ --async_replication_config max_workers=10 \ --async_replication_config frequency=60 \ diff --git a/.claude/skills/operating-weaviate-cli/references/collections.md b/.claude/skills/operating-weaviate-cli/references/collections.md index 408faed..2c3e490 100644 --- a/.claude/skills/operating-weaviate-cli/references/collections.md +++ b/.claude/skills/operating-weaviate-cli/references/collections.md @@ -48,7 +48,7 @@ weaviate-cli create collection \ - `--hfresh_search_probe` -- (hfresh only) Search probe size (default: None, uses server default) - `--distance_metric` -- Distance metric: cosine, dot, l2-squared, hamming, manhattan (default: None, uses server default). Applies to all vector index types. - `--rescore_limit` -- Rescore limit for quantized indexes (default: None, uses server default) -- `--async_replication_config` -- Async replication tuning as `key=value` pairs (repeatable). Valid keys: `max_workers`, `hashtree_height`, `frequency`, `frequency_while_propagating`, `alive_nodes_checking_frequency`, `logging_frequency`, `diff_batch_size`, `diff_per_node_timeout`, `pre_propagation_timeout`, `propagation_timeout`, `propagation_limit`, `propagation_delay`, `propagation_concurrency`, `propagation_batch_size`. All values must be integers. Requires `--async_enabled` on create and Weaviate >= v1.36.0. +- `--async_replication_config` -- Async replication tuning as `key=value` pairs (repeatable). Valid keys: `max_workers`, `hashtree_height`, `frequency`, `frequency_while_propagating`, `alive_nodes_checking_frequency`, `logging_frequency`, `diff_batch_size`, `diff_per_node_timeout`, `pre_propagation_timeout`, `propagation_timeout`, `propagation_limit`, `propagation_delay`, `propagation_concurrency`, `propagation_batch_size`. All values must be integers. Use `reset` to revert all to server defaults. Requires `--async_enabled` on create and Weaviate >= v1.34.18. **hfresh examples:** ```bash diff --git a/weaviate_cli/managers/collection_manager.py b/weaviate_cli/managers/collection_manager.py index a08b839..1301cd8 100644 --- a/weaviate_cli/managers/collection_manager.py +++ b/weaviate_cli/managers/collection_manager.py @@ -256,9 +256,9 @@ def create_collection( "Error: --async_replication_config requires --async_enabled to be set." ) - if async_replication_config and older_than_version(self.client, "1.36.0"): + if async_replication_config and older_than_version(self.client, "1.34.18"): click.echo( - "Warning: --async_replication_config requires Weaviate >= v1.36.0. " + "Warning: --async_replication_config requires Weaviate >= v1.34.18. " "The server may ignore or reject these settings." ) @@ -669,9 +669,9 @@ def update_collection( "Error: --async_replication_config cannot be used when --async_enabled is False." ) - if async_replication_config and older_than_version(self.client, "1.36.0"): + if async_replication_config and older_than_version(self.client, "1.34.18"): click.echo( - "Warning: --async_replication_config requires Weaviate >= v1.36.0. " + "Warning: --async_replication_config requires Weaviate >= v1.34.18. " "The server may ignore or reject these settings." ) diff --git a/weaviate_cli/utils.py b/weaviate_cli/utils.py index 0b84b69..88c08d0 100644 --- a/weaviate_cli/utils.py +++ b/weaviate_cli/utils.py @@ -138,7 +138,8 @@ def pp_objects(response, main_properties, json_output: bool = False): "Async replication config as key=value pairs. Can be specified multiple times. " "Valid keys: " + ", ".join(sorted(ASYNC_REPLICATION_CONFIG_KEYS)) + ". " "All values must be integers. " - "Requires --async_enabled on create and Weaviate >= v1.36.0." + 'Use "reset" to revert all async replication settings to server defaults. ' + "Requires --async_enabled on create and Weaviate >= v1.34.18." ) From ec1435e40b9a7450dad565b811ac5bb1a9015310 Mon Sep 17 00:00:00 2001 From: Jose Luis Franco Arza Date: Fri, 27 Mar 2026 07:45:23 +0100 Subject: [PATCH 6/9] Add --async_replication_config reset and use replace semantics Support "reset" keyword to revert all async replication settings to server defaults. Use `is not None` checks so empty dict (reset) is correctly forwarded as an empty async_config() call. Update version references to v1.34.18. Co-Authored-By: Claude Opus 4.6 (1M context) --- test/unittests/test_utils.py | 16 ++++++++++++++++ weaviate_cli/managers/collection_manager.py | 16 ++++++++++------ weaviate_cli/utils.py | 11 +++++++++-- 3 files changed, 35 insertions(+), 8 deletions(-) diff --git a/test/unittests/test_utils.py b/test/unittests/test_utils.py index ee70113..1d14def 100644 --- a/test/unittests/test_utils.py +++ b/test/unittests/test_utils.py @@ -445,3 +445,19 @@ def test_parse_async_replication_config_invalid_value(): def test_parse_async_replication_config_missing_equals(): with pytest.raises(ValueError, match="Expected key=value"): parse_async_replication_config(("max_workers",)) + + +def test_parse_async_replication_config_reset(): + result = parse_async_replication_config(("reset",)) + assert result == {} + + +def test_parse_async_replication_config_reset_case_insensitive(): + assert parse_async_replication_config(("Reset",)) == {} + assert parse_async_replication_config(("RESET",)) == {} + assert parse_async_replication_config((" reset ",)) == {} + + +def test_parse_async_replication_config_reset_with_other_keys(): + with pytest.raises(ValueError, match="Expected key=value"): + parse_async_replication_config(("reset", "max_workers=10")) diff --git a/weaviate_cli/managers/collection_manager.py b/weaviate_cli/managers/collection_manager.py index 1301cd8..ca36d70 100644 --- a/weaviate_cli/managers/collection_manager.py +++ b/weaviate_cli/managers/collection_manager.py @@ -251,12 +251,14 @@ def create_collection( f"Error: Collection '{collection}' already exists in Weaviate. Delete using command." ) - if async_replication_config and not async_enabled: + if async_replication_config is not None and not async_enabled: raise Exception( "Error: --async_replication_config requires --async_enabled to be set." ) - if async_replication_config and older_than_version(self.client, "1.34.18"): + if async_replication_config is not None and older_than_version( + self.client, "1.34.18" + ): click.echo( "Warning: --async_replication_config requires Weaviate >= v1.34.18. " "The server may ignore or reject these settings." @@ -584,7 +586,7 @@ def create_collection( wvc.Configure.Replication.async_config( **async_replication_config ) - if async_replication_config + if async_replication_config is not None else None ), ), @@ -664,12 +666,14 @@ def update_collection( raise Exception( "object_ttl_property_name is only valid when object_ttl_type is 'property'." ) - if async_replication_config and async_enabled is False: + if async_replication_config is not None and async_enabled is False: raise Exception( "Error: --async_replication_config cannot be used when --async_enabled is False." ) - if async_replication_config and older_than_version(self.client, "1.34.18"): + if async_replication_config is not None and older_than_version( + self.client, "1.34.18" + ): click.echo( "Warning: --async_replication_config requires Weaviate >= v1.34.18. " "The server may ignore or reject these settings." @@ -764,7 +768,7 @@ def update_collection( wvc.Reconfigure.Replication.async_config( **async_replication_config ) - if async_replication_config + if async_replication_config is not None else None ), ) diff --git a/weaviate_cli/utils.py b/weaviate_cli/utils.py index 88c08d0..9ad0119 100644 --- a/weaviate_cli/utils.py +++ b/weaviate_cli/utils.py @@ -134,6 +134,8 @@ def pp_objects(response, main_properties, json_output: bool = False): } +ASYNC_REPLICATION_CONFIG_RESET = "reset" + ASYNC_REPLICATION_CONFIG_HELP = ( "Async replication config as key=value pairs. Can be specified multiple times. " "Valid keys: " + ", ".join(sorted(ASYNC_REPLICATION_CONFIG_KEYS)) + ". " @@ -149,10 +151,12 @@ def parse_async_replication_config( """Parse async replication config key=value tuples into a dict. Args: - config_tuples: Tuple of "key=value" strings, e.g. ("max_workers=10", "frequency=60") + config_tuples: Tuple of "key=value" strings, e.g. ("max_workers=10", "frequency=60"), + or a single "reset" to revert all settings to server defaults. Returns: - Dict mapping parameter names to integer values, or None if empty/None. + Dict mapping parameter names to integer values, empty dict for "reset", + or None if empty/None. Raises: ValueError: If a key is unknown or a value is not a valid integer. @@ -160,6 +164,9 @@ def parse_async_replication_config( if not config_tuples: return None + if len(config_tuples) == 1 and config_tuples[0].strip().lower() == "reset": + return {} + result = {} for item in config_tuples: if "=" not in item: From 59854920cc63ab03ff66a476b6566b1c08041bc6 Mon Sep 17 00:00:00 2001 From: Jose Luis Franco Arza Date: Tue, 5 May 2026 17:56:37 +0200 Subject: [PATCH 7/9] Address PR review feedback (round 3) - Use ASYNC_REPLICATION_CONFIG_RESET constant in parser instead of literal - Remove unused async_replication_config field from CreateCollectionDefaults and UpdateCollectionDefaults - Reject --async_replication_config 'reset' on create (no semantics for new collections) - Tighten type annotations: tuple -> Tuple[str, ...] - Drop trivially-dead `if result else None` return - Add unit tests for old-version warning path (create + update) - Add unit test for update reset path asserting Reconfigure.Replication.async_config() is invoked with no kwargs Co-Authored-By: Claude Opus 4.7 (1M context) --- .../test_managers/test_collection_manager.py | 94 +++++++++++++++++++ weaviate_cli/commands/create.py | 13 ++- weaviate_cli/commands/update.py | 4 +- weaviate_cli/defaults.py | 2 - weaviate_cli/utils.py | 7 +- 5 files changed, 109 insertions(+), 11 deletions(-) diff --git a/test/unittests/test_managers/test_collection_manager.py b/test/unittests/test_managers/test_collection_manager.py index a26ede9..7361ffe 100644 --- a/test/unittests/test_managers/test_collection_manager.py +++ b/test/unittests/test_managers/test_collection_manager.py @@ -1020,3 +1020,97 @@ def test_update_collection_async_replication_config_rejected_when_async_false( ) mock_collections.get.return_value.config.update.assert_not_called() + + +def test_create_collection_async_replication_config_warns_on_old_version( + mock_client, mock_wvc_object_ttl, capsys +): + """Warn when async_replication_config is used against a server older than v1.34.18.""" + mock_collections = MagicMock() + mock_client.collections = mock_collections + mock_collections.exists.side_effect = [False, True] + mock_client.get_meta.return_value = {"version": "1.34.0"} + + manager = CollectionManager(mock_client) + + manager.create_collection( + collection="TestCollection", + replication_factor=3, + vector_index="hnsw", + async_enabled=True, + async_replication_config={"max_workers": 10}, + ) + + captured = capsys.readouterr() + assert "Warning: --async_replication_config requires Weaviate >= v1.34.18" in ( + captured.out + captured.err + ) + mock_collections.create.assert_called_once() + + +def test_update_collection_async_replication_config_reset( + mock_client, mock_wvc_object_ttl +): + """Reset (empty dict) calls Reconfigure.Replication.async_config() with no kwargs.""" + mock_collections = MagicMock() + mock_client.collections = mock_collections + mock_client.collections.exists.side_effect = [True, True] + mock_client.get_meta.return_value = {"version": "1.36.0"} + + mock_collection = MagicMock() + mock_client.collections.get.return_value = mock_collection + mock_collection.config.get.return_value = MagicMock( + replication_config=MagicMock(factor=3), + multi_tenancy_config=MagicMock( + enabled=False, auto_tenant_creation=False, auto_tenant_activation=False + ), + ) + + manager = CollectionManager(mock_client) + + with patch.object( + wvc.Reconfigure.Replication, + "async_config", + wraps=wvc.Reconfigure.Replication.async_config, + ) as mock_async_config: + manager.update_collection( + collection="TestCollection", + async_replication_config={}, + ) + + mock_async_config.assert_called_once_with() + mock_collection.config.update.assert_called_once() + repl_config = mock_collection.config.update.call_args.kwargs["replication_config"] + assert repl_config.asyncConfig is not None + + +def test_update_collection_async_replication_config_warns_on_old_version( + mock_client, mock_wvc_object_ttl, capsys +): + """Warn when async_replication_config is used against a server older than v1.34.18.""" + mock_collections = MagicMock() + mock_client.collections = mock_collections + mock_client.collections.exists.side_effect = [True, True] + mock_client.get_meta.return_value = {"version": "1.34.0"} + + mock_collection = MagicMock() + mock_client.collections.get.return_value = mock_collection + mock_collection.config.get.return_value = MagicMock( + replication_config=MagicMock(factor=3), + multi_tenancy_config=MagicMock( + enabled=False, auto_tenant_creation=False, auto_tenant_activation=False + ), + ) + + manager = CollectionManager(mock_client) + + manager.update_collection( + collection="TestCollection", + async_replication_config={"max_workers": 10}, + ) + + captured = capsys.readouterr() + assert "Warning: --async_replication_config requires Weaviate >= v1.34.18" in ( + captured.out + captured.err + ) + mock_collection.config.update.assert_called_once() diff --git a/weaviate_cli/commands/create.py b/weaviate_cli/commands/create.py index c580aba..6e0e137 100644 --- a/weaviate_cli/commands/create.py +++ b/weaviate_cli/commands/create.py @@ -1,6 +1,6 @@ import sys import click -from typing import Optional +from typing import Optional, Tuple import json from weaviate import WeaviateClient @@ -256,7 +256,7 @@ def create_collection_cli( object_ttl_time: Optional[int], object_ttl_filter_expired: bool, object_ttl_property_name: Optional[str], - async_replication_config: tuple, + async_replication_config: Tuple[str, ...], ) -> None: """Create a collection in Weaviate.""" @@ -272,6 +272,11 @@ def create_collection_cli( sys.exit(1) client = None try: + parsed_async_config = parse_async_replication_config(async_replication_config) + if parsed_async_config == {}: + raise click.UsageError( + "--async_replication_config 'reset' is only supported on update, not create." + ) client = get_client_from_context(ctx) # Call the function from create_collection.py passing both general and specific arguments collection_man = CollectionManager(client) @@ -302,9 +307,7 @@ def create_collection_cli( object_ttl_time=object_ttl_time, object_ttl_filter_expired=object_ttl_filter_expired, object_ttl_property_name=object_ttl_property_name, - async_replication_config=parse_async_replication_config( - async_replication_config - ), + async_replication_config=parsed_async_config, ) except Exception as e: click.echo(f"Error: {e}") diff --git a/weaviate_cli/commands/update.py b/weaviate_cli/commands/update.py index daaa730..741e8a1 100644 --- a/weaviate_cli/commands/update.py +++ b/weaviate_cli/commands/update.py @@ -1,7 +1,7 @@ import sys import click import json -from typing import Optional, Union +from typing import Optional, Tuple, Union from weaviate_cli.completion.complete import collection_name_complete from weaviate_cli.managers.alias_manager import AliasManager @@ -136,7 +136,7 @@ def update_collection_cli( object_ttl_time: Optional[int], object_ttl_filter_expired: bool, object_ttl_property_name: Optional[str], - async_replication_config: tuple, + async_replication_config: Tuple[str, ...], ) -> None: """Update a collection in Weaviate.""" diff --git a/weaviate_cli/defaults.py b/weaviate_cli/defaults.py index 9e345a0..55e4cc5 100644 --- a/weaviate_cli/defaults.py +++ b/weaviate_cli/defaults.py @@ -87,7 +87,6 @@ class CreateCollectionDefaults: object_ttl_time: Optional[int] = None object_ttl_filter_expired: Optional[bool] = None object_ttl_property_name: str = "releaseDate" - async_replication_config: Optional[tuple] = None @dataclass @@ -264,7 +263,6 @@ class UpdateCollectionDefaults: object_ttl_time: Optional[int] = None object_ttl_filter_expired: Optional[bool] = None object_ttl_property_name: str = "releaseDate" - async_replication_config: Optional[tuple] = None @dataclass diff --git a/weaviate_cli/utils.py b/weaviate_cli/utils.py index 9ad0119..0e8d596 100644 --- a/weaviate_cli/utils.py +++ b/weaviate_cli/utils.py @@ -164,7 +164,10 @@ def parse_async_replication_config( if not config_tuples: return None - if len(config_tuples) == 1 and config_tuples[0].strip().lower() == "reset": + if ( + len(config_tuples) == 1 + and config_tuples[0].strip().lower() == ASYNC_REPLICATION_CONFIG_RESET + ): return {} result = {} @@ -190,7 +193,7 @@ def parse_async_replication_config( f"Invalid value for '{key}': '{value}'. Must be an integer." ) - return result if result else None + return result def parse_permission(perm: str) -> PermissionsCreateType: From 97947c3e5be80cfbbfc35187d520d14070168de2 Mon Sep 17 00:00:00 2001 From: Jose Luis Franco Arza Date: Wed, 6 May 2026 09:52:02 +0200 Subject: [PATCH 8/9] Address PR review feedback (round 6) - Let click.UsageError propagate from create_collection_cli so Click renders it as a proper usage error instead of being swallowed by except Exception - Clarify in ASYNC_REPLICATION_CONFIG_HELP that "reset" is update-only Co-Authored-By: Claude Sonnet 4.6 --- weaviate_cli/commands/create.py | 3 ++- weaviate_cli/utils.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/weaviate_cli/commands/create.py b/weaviate_cli/commands/create.py index 6e0e137..ef2591c 100644 --- a/weaviate_cli/commands/create.py +++ b/weaviate_cli/commands/create.py @@ -278,7 +278,6 @@ def create_collection_cli( "--async_replication_config 'reset' is only supported on update, not create." ) client = get_client_from_context(ctx) - # Call the function from create_collection.py passing both general and specific arguments collection_man = CollectionManager(client) collection_man.create_collection( collection=collection, @@ -309,6 +308,8 @@ def create_collection_cli( object_ttl_property_name=object_ttl_property_name, async_replication_config=parsed_async_config, ) + except click.UsageError: + raise except Exception as e: click.echo(f"Error: {e}") if client: diff --git a/weaviate_cli/utils.py b/weaviate_cli/utils.py index 0e8d596..0c7b89e 100644 --- a/weaviate_cli/utils.py +++ b/weaviate_cli/utils.py @@ -140,7 +140,7 @@ def pp_objects(response, main_properties, json_output: bool = False): "Async replication config as key=value pairs. Can be specified multiple times. " "Valid keys: " + ", ".join(sorted(ASYNC_REPLICATION_CONFIG_KEYS)) + ". " "All values must be integers. " - 'Use "reset" to revert all async replication settings to server defaults. ' + 'Use "reset" to revert all async replication settings to server defaults (update only). ' "Requires --async_enabled on create and Weaviate >= v1.34.18." ) From 9d530ac4193e9561fa92381d06a96076039fd562 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 6 May 2026 07:59:14 +0000 Subject: [PATCH 9/9] Fix version discrepancy: update async_replication_config min version to v1.36.0 everywhere Agent-Logs-Url: https://github.com/weaviate/weaviate-cli/sessions/712f888c-8664-4b19-8539-a7754385e328 Co-authored-by: jfrancoa <23482278+jfrancoa@users.noreply.github.com> --- .claude/skills/operating-weaviate-cli/SKILL.md | 4 ++-- .../operating-weaviate-cli/references/collections.md | 2 +- test/unittests/test_managers/test_collection_manager.py | 8 ++++---- weaviate_cli/managers/collection_manager.py | 8 ++++---- weaviate_cli/utils.py | 2 +- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/.claude/skills/operating-weaviate-cli/SKILL.md b/.claude/skills/operating-weaviate-cli/SKILL.md index f7fb6fb..ef90b83 100644 --- a/.claude/skills/operating-weaviate-cli/SKILL.md +++ b/.claude/skills/operating-weaviate-cli/SKILL.md @@ -139,14 +139,14 @@ weaviate-cli delete collection --collection MyCollection --json weaviate-cli delete collection --all --json ``` -Key create options: `--multitenant`, `--auto_tenant_creation`, `--auto_tenant_activation`, `--shards N`, `--vectorizer `, `--named_vector`, `--replication_deletion_strategy`, `--async_replication_config key=value` (repeatable; requires `--async_enabled` and Weaviate >= v1.34.18), `--object_ttl_type`, `--object_ttl_time`, `--object_ttl_filter_expired`, `--object_ttl_property_name` (only when `object_ttl_type=property`) +Key create options: `--multitenant`, `--auto_tenant_creation`, `--auto_tenant_activation`, `--shards N`, `--vectorizer `, `--named_vector`, `--replication_deletion_strategy`, `--async_replication_config key=value` (repeatable; requires `--async_enabled` and Weaviate >= v1.36.0), `--object_ttl_type`, `--object_ttl_time`, `--object_ttl_filter_expired`, `--object_ttl_property_name` (only when `object_ttl_type=property`) Mutable fields: `--async_enabled`, `--replication_factor`, `--vector_index`, `--description`, `--training_limit`, `--auto_tenant_creation`, `--auto_tenant_activation`, `--replication_deletion_strategy`, `--async_replication_config key=value` (repeatable), `--object_ttl_type`, `--object_ttl_time`, `--object_ttl_filter_expired`, `--object_ttl_property_name` (only when `object_ttl_type=property`) #### Async Replication Config ```bash -# Create with async replication tuning (requires --async_enabled and Weaviate >= v1.34.18) +# Create with async replication tuning (requires --async_enabled and Weaviate >= v1.36.0) weaviate-cli create collection --collection MyCol --async_enabled \ --async_replication_config max_workers=10 \ --async_replication_config frequency=60 \ diff --git a/.claude/skills/operating-weaviate-cli/references/collections.md b/.claude/skills/operating-weaviate-cli/references/collections.md index 2c3e490..d1c916d 100644 --- a/.claude/skills/operating-weaviate-cli/references/collections.md +++ b/.claude/skills/operating-weaviate-cli/references/collections.md @@ -48,7 +48,7 @@ weaviate-cli create collection \ - `--hfresh_search_probe` -- (hfresh only) Search probe size (default: None, uses server default) - `--distance_metric` -- Distance metric: cosine, dot, l2-squared, hamming, manhattan (default: None, uses server default). Applies to all vector index types. - `--rescore_limit` -- Rescore limit for quantized indexes (default: None, uses server default) -- `--async_replication_config` -- Async replication tuning as `key=value` pairs (repeatable). Valid keys: `max_workers`, `hashtree_height`, `frequency`, `frequency_while_propagating`, `alive_nodes_checking_frequency`, `logging_frequency`, `diff_batch_size`, `diff_per_node_timeout`, `pre_propagation_timeout`, `propagation_timeout`, `propagation_limit`, `propagation_delay`, `propagation_concurrency`, `propagation_batch_size`. All values must be integers. Use `reset` to revert all to server defaults. Requires `--async_enabled` on create and Weaviate >= v1.34.18. +- `--async_replication_config` -- Async replication tuning as `key=value` pairs (repeatable). Valid keys: `max_workers`, `hashtree_height`, `frequency`, `frequency_while_propagating`, `alive_nodes_checking_frequency`, `logging_frequency`, `diff_batch_size`, `diff_per_node_timeout`, `pre_propagation_timeout`, `propagation_timeout`, `propagation_limit`, `propagation_delay`, `propagation_concurrency`, `propagation_batch_size`. All values must be integers. Use `reset` to revert all to server defaults. Requires `--async_enabled` on create and Weaviate >= v1.36.0. **hfresh examples:** ```bash diff --git a/test/unittests/test_managers/test_collection_manager.py b/test/unittests/test_managers/test_collection_manager.py index 7361ffe..a677b25 100644 --- a/test/unittests/test_managers/test_collection_manager.py +++ b/test/unittests/test_managers/test_collection_manager.py @@ -1025,7 +1025,7 @@ def test_update_collection_async_replication_config_rejected_when_async_false( def test_create_collection_async_replication_config_warns_on_old_version( mock_client, mock_wvc_object_ttl, capsys ): - """Warn when async_replication_config is used against a server older than v1.34.18.""" + """Warn when async_replication_config is used against a server older than v1.36.0.""" mock_collections = MagicMock() mock_client.collections = mock_collections mock_collections.exists.side_effect = [False, True] @@ -1042,7 +1042,7 @@ def test_create_collection_async_replication_config_warns_on_old_version( ) captured = capsys.readouterr() - assert "Warning: --async_replication_config requires Weaviate >= v1.34.18" in ( + assert "Warning: --async_replication_config requires Weaviate >= v1.36.0" in ( captured.out + captured.err ) mock_collections.create.assert_called_once() @@ -1087,7 +1087,7 @@ def test_update_collection_async_replication_config_reset( def test_update_collection_async_replication_config_warns_on_old_version( mock_client, mock_wvc_object_ttl, capsys ): - """Warn when async_replication_config is used against a server older than v1.34.18.""" + """Warn when async_replication_config is used against a server older than v1.36.0.""" mock_collections = MagicMock() mock_client.collections = mock_collections mock_client.collections.exists.side_effect = [True, True] @@ -1110,7 +1110,7 @@ def test_update_collection_async_replication_config_warns_on_old_version( ) captured = capsys.readouterr() - assert "Warning: --async_replication_config requires Weaviate >= v1.34.18" in ( + assert "Warning: --async_replication_config requires Weaviate >= v1.36.0" in ( captured.out + captured.err ) mock_collection.config.update.assert_called_once() diff --git a/weaviate_cli/managers/collection_manager.py b/weaviate_cli/managers/collection_manager.py index ca36d70..72be1a3 100644 --- a/weaviate_cli/managers/collection_manager.py +++ b/weaviate_cli/managers/collection_manager.py @@ -257,10 +257,10 @@ def create_collection( ) if async_replication_config is not None and older_than_version( - self.client, "1.34.18" + self.client, "1.36.0" ): click.echo( - "Warning: --async_replication_config requires Weaviate >= v1.34.18. " + "Warning: --async_replication_config requires Weaviate >= v1.36.0. " "The server may ignore or reject these settings." ) @@ -672,10 +672,10 @@ def update_collection( ) if async_replication_config is not None and older_than_version( - self.client, "1.34.18" + self.client, "1.36.0" ): click.echo( - "Warning: --async_replication_config requires Weaviate >= v1.34.18. " + "Warning: --async_replication_config requires Weaviate >= v1.36.0. " "The server may ignore or reject these settings." ) diff --git a/weaviate_cli/utils.py b/weaviate_cli/utils.py index 0c7b89e..07d28f9 100644 --- a/weaviate_cli/utils.py +++ b/weaviate_cli/utils.py @@ -141,7 +141,7 @@ def pp_objects(response, main_properties, json_output: bool = False): "Valid keys: " + ", ".join(sorted(ASYNC_REPLICATION_CONFIG_KEYS)) + ". " "All values must be integers. " 'Use "reset" to revert all async replication settings to server defaults (update only). ' - "Requires --async_enabled on create and Weaviate >= v1.34.18." + "Requires --async_enabled on create and Weaviate >= v1.36.0." )