Skip to content

Commit 89ba9b0

Browse files
authored
Merge pull request #149 from weaviate/jose/improve-tenant-ops
Improve tenants logic.
2 parents 178e7d0 + b8aa2b7 commit 89ba9b0

File tree

11 files changed

+597
-253
lines changed

11 files changed

+597
-253
lines changed

.claude/skills/contributing-to-weaviate-cli/references/code-review.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,23 @@ Commands must exit with code 1 on error for proper scripting support. Don't just
9494

9595
Every manager method that produces output should accept and use `json_output`.
9696

97+
### 6. Unstripped Comma-Separated Tenant Input
98+
99+
Wrong:
100+
```python
101+
tenants_list = tenants.split(",") if tenants else None
102+
```
103+
104+
Right:
105+
```python
106+
tenants_list = (
107+
[t.strip() for t in tenants.split(",") if t.strip()] if tenants else None
108+
)
109+
```
110+
111+
This prevents invalid tenant names like `" b"` or `""` when users pass `--tenants "a, b,"`.
112+
Apply the same pattern inside manager methods when splitting a `tenants` string argument.
113+
97114
## Code Conventions
98115

99116
### Formatting

.claude/skills/operating-weaviate-cli/SKILL.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,7 @@ See [references/data.md](references/data.md) and [references/search.md](referenc
167167

168168
```bash
169169
weaviate-cli create tenants --collection Movies --number_tenants 100 --tenant_suffix "Tenant" --state active --json
170+
weaviate-cli create tenants --collection Movies --tenants "Alice,Bob,Charlie" --state active --json
170171
weaviate-cli get tenants --collection Movies --json
171172
weaviate-cli get tenants --collection Movies --tenant_id "Tenant-1" --verbose --json
172173
weaviate-cli update tenants --collection Movies --state cold --number_tenants 100 --json

.claude/skills/operating-weaviate-cli/references/tenants.md

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
Manage tenants in multi-tenant Weaviate collections.
44

55
## Create Tenants
6+
7+
**Auto-generate N tenants** (named `Tenant-0` through `Tenant-99`):
68
```bash
79
weaviate-cli create tenants \
810
--collection "Movies" \
@@ -11,7 +13,16 @@ weaviate-cli create tenants \
1113
--state active \
1214
--json
1315
```
14-
Creates tenants named `Tenant-0` through `Tenant-99`.
16+
17+
**Create specific tenants by name** (comma-separated):
18+
```bash
19+
weaviate-cli create tenants \
20+
--collection "Movies" \
21+
--tenants "Alice,Bob,Charlie" \
22+
--state active \
23+
--json
24+
```
25+
When `--tenants` is provided it overrides `--tenant_suffix` and `--number_tenants`.
1526

1627
Options: `--tenant_batch_size N` for batched creation.
1728

test/unittests/test_managers/test_data_manager.py

Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,225 @@
44
import weaviate.classes.config as wvc
55

66

7+
# ---------------------------------------------------------------------------
8+
# _resolve_tenants_for_ingestion
9+
# ---------------------------------------------------------------------------
10+
11+
12+
def _make_col(name: str = "TestCollection") -> MagicMock:
13+
"""Return a minimal mock Collection with configurable tenants.get()."""
14+
col = MagicMock()
15+
col.name = name
16+
return col
17+
18+
19+
def _make_manager() -> DataManager:
20+
return DataManager(MagicMock())
21+
22+
23+
class TestResolveTenants:
24+
"""Unit tests for DataManager._resolve_tenants_for_ingestion."""
25+
26+
# ------------------------------------------------------------------
27+
# Case 1: non-MT collection
28+
# ------------------------------------------------------------------
29+
30+
def test_non_mt_no_options_returns_sentinel(self) -> None:
31+
col = _make_col()
32+
manager = _make_manager()
33+
result = manager._resolve_tenants_for_ingestion(
34+
col=col,
35+
mt_enabled=False,
36+
auto_tenant_creation_enabled=False,
37+
tenant_suffix="Tenant",
38+
auto_tenants=0,
39+
tenants_list=None,
40+
)
41+
assert result == ["None"]
42+
43+
def test_non_mt_with_tenants_list_raises(self) -> None:
44+
col = _make_col()
45+
manager = _make_manager()
46+
with pytest.raises(Exception, match="does not have multi-tenancy enabled"):
47+
manager._resolve_tenants_for_ingestion(
48+
col=col,
49+
mt_enabled=False,
50+
auto_tenant_creation_enabled=False,
51+
tenant_suffix="Tenant",
52+
auto_tenants=0,
53+
tenants_list=["Tenant-0"],
54+
)
55+
56+
def test_non_mt_with_auto_tenants_raises(self) -> None:
57+
col = _make_col()
58+
manager = _make_manager()
59+
with pytest.raises(Exception, match="does not have multi-tenancy enabled"):
60+
manager._resolve_tenants_for_ingestion(
61+
col=col,
62+
mt_enabled=False,
63+
auto_tenant_creation_enabled=False,
64+
tenant_suffix="Tenant",
65+
auto_tenants=3,
66+
tenants_list=None,
67+
)
68+
69+
# ------------------------------------------------------------------
70+
# Case 2: auto_tenants > 0
71+
# ------------------------------------------------------------------
72+
73+
def test_auto_tenants_without_auto_creation_raises(self) -> None:
74+
col = _make_col()
75+
manager = _make_manager()
76+
with pytest.raises(Exception, match="Auto tenant creation is not enabled"):
77+
manager._resolve_tenants_for_ingestion(
78+
col=col,
79+
mt_enabled=True,
80+
auto_tenant_creation_enabled=False,
81+
tenant_suffix="Tenant",
82+
auto_tenants=3,
83+
tenants_list=None,
84+
)
85+
86+
def test_auto_tenants_no_existing_generates_new(self) -> None:
87+
col = _make_col()
88+
col.tenants.get.return_value = {}
89+
manager = _make_manager()
90+
result = manager._resolve_tenants_for_ingestion(
91+
col=col,
92+
mt_enabled=True,
93+
auto_tenant_creation_enabled=True,
94+
tenant_suffix="Tenant",
95+
auto_tenants=3,
96+
tenants_list=None,
97+
)
98+
assert result == ["Tenant-0", "Tenant-1", "Tenant-2"]
99+
100+
def test_auto_tenants_continues_from_highest_index(self) -> None:
101+
col = _make_col()
102+
col.tenants.get.return_value = {
103+
"Tenant-0": MagicMock(),
104+
"Tenant-1": MagicMock(),
105+
}
106+
manager = _make_manager()
107+
result = manager._resolve_tenants_for_ingestion(
108+
col=col,
109+
mt_enabled=True,
110+
auto_tenant_creation_enabled=True,
111+
tenant_suffix="Tenant",
112+
auto_tenants=4,
113+
tenants_list=None,
114+
)
115+
# 2 existing + 2 new starting at index 2
116+
assert result == ["Tenant-0", "Tenant-1", "Tenant-2", "Tenant-3"]
117+
118+
def test_auto_tenants_enough_existing_returns_slice(self) -> None:
119+
col = _make_col()
120+
col.tenants.get.return_value = {
121+
"Tenant-0": MagicMock(),
122+
"Tenant-1": MagicMock(),
123+
"Tenant-2": MagicMock(),
124+
}
125+
manager = _make_manager()
126+
result = manager._resolve_tenants_for_ingestion(
127+
col=col,
128+
mt_enabled=True,
129+
auto_tenant_creation_enabled=True,
130+
tenant_suffix="Tenant",
131+
auto_tenants=2,
132+
tenants_list=None,
133+
)
134+
assert result == ["Tenant-0", "Tenant-1"]
135+
136+
def test_auto_tenants_non_numeric_suffix_skipped_in_indexing(self) -> None:
137+
"""Tenants whose suffix is non-numeric are included but don't affect index."""
138+
col = _make_col()
139+
col.tenants.get.return_value = {
140+
"Tenant-abc": MagicMock(), # non-numeric, skip for index
141+
}
142+
manager = _make_manager()
143+
result = manager._resolve_tenants_for_ingestion(
144+
col=col,
145+
mt_enabled=True,
146+
auto_tenant_creation_enabled=True,
147+
tenant_suffix="Tenant",
148+
auto_tenants=2,
149+
tenants_list=None,
150+
)
151+
# existing_matching has "Tenant-abc", highest_index stays -1 → new ones start at 0
152+
assert "Tenant-abc" in result
153+
assert "Tenant-0" in result
154+
assert len(result) == 2
155+
156+
# ------------------------------------------------------------------
157+
# Case 3: explicit tenants_list
158+
# ------------------------------------------------------------------
159+
160+
def test_explicit_tenants_list_returned_as_is(self) -> None:
161+
col = _make_col()
162+
manager = _make_manager()
163+
tenants = ["Alpha", "Beta", "Gamma"]
164+
result = manager._resolve_tenants_for_ingestion(
165+
col=col,
166+
mt_enabled=True,
167+
auto_tenant_creation_enabled=False,
168+
tenant_suffix="Tenant",
169+
auto_tenants=0,
170+
tenants_list=tenants,
171+
)
172+
assert result is tenants
173+
174+
# ------------------------------------------------------------------
175+
# Case 4: no options – fall back to existing tenants with suffix
176+
# ------------------------------------------------------------------
177+
178+
def test_fallback_existing_tenants_with_suffix_returned(self) -> None:
179+
col = _make_col()
180+
col.tenants.get.return_value = {
181+
"Tenant-0": MagicMock(),
182+
"Tenant-1": MagicMock(),
183+
"Other-0": MagicMock(), # different suffix, must be excluded
184+
}
185+
manager = _make_manager()
186+
result = manager._resolve_tenants_for_ingestion(
187+
col=col,
188+
mt_enabled=True,
189+
auto_tenant_creation_enabled=False,
190+
tenant_suffix="Tenant",
191+
auto_tenants=0,
192+
tenants_list=None,
193+
)
194+
assert sorted(result) == ["Tenant-0", "Tenant-1"]
195+
196+
def test_fallback_no_tenants_no_auto_creation_raises(self) -> None:
197+
col = _make_col()
198+
col.tenants.get.return_value = {}
199+
manager = _make_manager()
200+
with pytest.raises(Exception, match="No tenants present"):
201+
manager._resolve_tenants_for_ingestion(
202+
col=col,
203+
mt_enabled=True,
204+
auto_tenant_creation_enabled=False,
205+
tenant_suffix="Tenant",
206+
auto_tenants=0,
207+
tenants_list=None,
208+
)
209+
210+
def test_fallback_no_tenants_with_auto_creation_returns_empty(self) -> None:
211+
"""When auto_tenant_creation is on and no tenants exist, returns empty list."""
212+
col = _make_col()
213+
col.tenants.get.return_value = {}
214+
manager = _make_manager()
215+
result = manager._resolve_tenants_for_ingestion(
216+
col=col,
217+
mt_enabled=True,
218+
auto_tenant_creation_enabled=True,
219+
tenant_suffix="Tenant",
220+
auto_tenants=0,
221+
tenants_list=None,
222+
)
223+
assert result == []
224+
225+
7226
def test_ingest_data(mock_client):
8227
manager = DataManager(mock_client)
9228
mock_collections = MagicMock()

0 commit comments

Comments
 (0)