MongoDB workload generation
bondi.mongodb.MongoGenerationConfig
dataclass
All field, predicate and query-option settings in one place.
Attributes:
| Name |
Type |
Description |
min_predicates |
int
|
Minimum distinct filter fields (default 1).
|
max_predicates |
int
|
Maximum distinct filter fields (default 8).
|
include_fields |
list[str] | None
|
Optional exact normalized Mongo paths; takes precedence over exclusions.
|
exclude_fields |
list[str]
|
Exact paths excluded from filters, sorts and projections. Defaults to _id.
|
field_weights |
dict
|
Positive default weight plus ordered glob overrides; last match wins.
|
operator_config |
dict
|
Per-type operator/weight lists and conditional special-operator
enabled/chance settings. See the generation guide for the full structure.
|
sort_probability |
float
|
Fraction of pattern slots requesting sort.
|
sort_on_filter_field_ratio |
float
|
Preference for selecting sort fields from the filter.
|
sort_two_field_ratio |
float
|
Probability of requesting two sort fields instead of one.
|
sort_ascending_ratio |
float
|
Probability of ascending sort direction.
|
skip_probability |
float
|
Fraction of pattern slots requesting skip.
|
limit_probability |
float
|
Fraction of pattern slots requesting limit.
|
skip_min |
int
|
Minimum generated skip (inclusive).
|
skip_max |
int
|
Maximum generated skip (inclusive).
|
limit_min |
int
|
Minimum generated limit (inclusive).
|
limit_max |
int
|
Maximum generated limit (inclusive).
|
skip_requires_limit |
bool
|
Force a limit for slots requesting skip.
|
projection_probability |
float
|
Fraction of pattern slots requesting inclusion projection.
|
projection_filter_overlap_ratio |
float
|
Desired fraction of projection fields from the filter.
|
projection_min_fields |
int
|
Minimum requested projection fields.
|
projection_max_fields |
int
|
Maximum requested projection fields.
|
projection_exclude_id_ratio |
float
|
Probability of adding _id: 0, unless _id is projected.
|
Probabilities describe proposals, not accepted-workload frequencies. Rejection
can change the distribution. Values are sampled uniformly from distinct values,
even with a full-retention Schema. Configs are mutable; generators snapshot
and validate them at the start of each call. Do not mutate during generation.
Source code in src/bondi/mongodb/generation_config.py
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168 | @dataclass
class MongoGenerationConfig:
"""All field, predicate and query-option settings in one place.
Attributes:
min_predicates: Minimum distinct filter fields (default 1).
max_predicates: Maximum distinct filter fields (default 8).
include_fields: Optional exact normalized Mongo paths; takes precedence over exclusions.
exclude_fields: Exact paths excluded from filters, sorts and projections. Defaults to _id.
field_weights: Positive default weight plus ordered glob overrides; last match wins.
operator_config: Per-type operator/weight lists and conditional special-operator
enabled/chance settings. See the generation guide for the full structure.
sort_probability: Fraction of pattern slots requesting sort.
sort_on_filter_field_ratio: Preference for selecting sort fields from the filter.
sort_two_field_ratio: Probability of requesting two sort fields instead of one.
sort_ascending_ratio: Probability of ascending sort direction.
skip_probability: Fraction of pattern slots requesting skip.
limit_probability: Fraction of pattern slots requesting limit.
skip_min: Minimum generated skip (inclusive).
skip_max: Maximum generated skip (inclusive).
limit_min: Minimum generated limit (inclusive).
limit_max: Maximum generated limit (inclusive).
skip_requires_limit: Force a limit for slots requesting skip.
projection_probability: Fraction of pattern slots requesting inclusion projection.
projection_filter_overlap_ratio: Desired fraction of projection fields from the filter.
projection_min_fields: Minimum requested projection fields.
projection_max_fields: Maximum requested projection fields.
projection_exclude_id_ratio: Probability of adding _id: 0, unless _id is projected.
Probabilities describe proposals, not accepted-workload frequencies. Rejection
can change the distribution. Values are sampled uniformly from distinct values,
even with a full-retention Schema. Configs are mutable; generators snapshot
and validate them at the start of each call. Do not mutate during generation.
"""
min_predicates: int = 1
max_predicates: int = 8
include_fields: list[str] | None = None
exclude_fields: list[str] = field(default_factory=lambda: ["_id"])
field_weights: dict = field(default_factory=lambda: {"default": 1.0, "overrides": {}})
operator_config: dict = field(default_factory=_operators)
sort_probability: float = 0.3
sort_on_filter_field_ratio: float = 0.7
sort_two_field_ratio: float = 0.3
sort_ascending_ratio: float = 0.8
skip_probability: float = 0.0
limit_probability: float = 0.2
skip_min: int = 0
skip_max: int = 1000
limit_min: int = 1
limit_max: int = 100
skip_requires_limit: bool = True
projection_probability: float = 0.0
projection_filter_overlap_ratio: float = 0.5
projection_min_fields: int = 1
projection_max_fields: int = 5
projection_exclude_id_ratio: float = 0.5
def __post_init__(self):
self.validate()
def validate(self) -> None:
"""Validate all settings, including nested operator and weight mappings."""
for low, high, minimum in (
("min_predicates", "max_predicates", 1),
("skip_min", "skip_max", 0),
("limit_min", "limit_max", 1),
("projection_min_fields", "projection_max_fields", 1),
):
_positive(low, getattr(self, low), minimum)
_positive(high, getattr(self, high), minimum)
if getattr(self, low) > getattr(self, high):
raise ValueError(f"{low} must not exceed {high}")
for name in self.__dataclass_fields__:
if name.endswith(("_probability", "_ratio")):
_fraction(name, getattr(self, name))
if type(self.skip_requires_limit) is not bool:
raise TypeError("skip_requires_limit must be bool")
for name in ("include_fields", "exclude_fields"):
value = getattr(self, name)
if name == "include_fields" and value is None:
continue
if not isinstance(value, list) or any(not isinstance(v, str) or not v for v in value):
raise TypeError(f"{name} must be a list of nonempty paths")
weights = self.field_weights
if not isinstance(weights, dict) or set(weights) - {"default", "overrides"}:
raise ValueError("field_weights accepts default and overrides only")
overrides = weights.get("overrides", {})
if not isinstance(overrides, dict) or any(not isinstance(k, str) for k in overrides):
raise TypeError("field weight overrides must map glob strings to weights")
for value in (weights.get("default", 1), *overrides.values()):
_weight(value, positive=True)
config = self.operator_config
if not isinstance(config, dict) or set(config) - {"types", "operators"}:
raise ValueError("operator_config accepts types and operators only")
types = config.get("types")
if not isinstance(types, dict) or not types:
raise ValueError("operator_config.types must be a nonempty mapping")
for name, settings in types.items():
if name not in _operators()["types"]:
raise ValueError(f"Unsupported generation type: {name}")
if not isinstance(settings, dict) or set(settings) != {"operators", "weights"}:
raise ValueError("Each type requires operators and weights")
ops, ws = settings["operators"], settings["weights"]
if (
not isinstance(ops, list)
or not isinstance(ws, list)
or not ops
or len(ops) != len(ws)
or any(not isinstance(op, str) for op in ops)
or len(set(ops)) != len(ops)
or set(ops) - {"eq", "ne", "lt", "lte", "gt", "gte", "in", "nin"}
):
raise ValueError("Invalid operator names or mismatched weights")
for weight in ws:
_weight(weight)
if not any(ws):
raise ValueError("At least one operator weight must be positive per type")
specials = config.get("operators", {})
if not isinstance(specials, dict) or set(specials) - {"$exists", "$type", "$size"}:
raise ValueError("Unsupported special operator")
for settings in specials.values():
if not isinstance(settings, dict) or set(settings) - {"enabled", "chance"}:
raise ValueError("Special operators accept enabled and chance only")
if type(settings.get("enabled", False)) is not bool:
raise TypeError("enabled must be bool")
_fraction("chance", settings.get("chance", 0))
|
validate()
Validate all settings, including nested operator and weight mappings.
Source code in src/bondi/mongodb/generation_config.py
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168 | def validate(self) -> None:
"""Validate all settings, including nested operator and weight mappings."""
for low, high, minimum in (
("min_predicates", "max_predicates", 1),
("skip_min", "skip_max", 0),
("limit_min", "limit_max", 1),
("projection_min_fields", "projection_max_fields", 1),
):
_positive(low, getattr(self, low), minimum)
_positive(high, getattr(self, high), minimum)
if getattr(self, low) > getattr(self, high):
raise ValueError(f"{low} must not exceed {high}")
for name in self.__dataclass_fields__:
if name.endswith(("_probability", "_ratio")):
_fraction(name, getattr(self, name))
if type(self.skip_requires_limit) is not bool:
raise TypeError("skip_requires_limit must be bool")
for name in ("include_fields", "exclude_fields"):
value = getattr(self, name)
if name == "include_fields" and value is None:
continue
if not isinstance(value, list) or any(not isinstance(v, str) or not v for v in value):
raise TypeError(f"{name} must be a list of nonempty paths")
weights = self.field_weights
if not isinstance(weights, dict) or set(weights) - {"default", "overrides"}:
raise ValueError("field_weights accepts default and overrides only")
overrides = weights.get("overrides", {})
if not isinstance(overrides, dict) or any(not isinstance(k, str) for k in overrides):
raise TypeError("field weight overrides must map glob strings to weights")
for value in (weights.get("default", 1), *overrides.values()):
_weight(value, positive=True)
config = self.operator_config
if not isinstance(config, dict) or set(config) - {"types", "operators"}:
raise ValueError("operator_config accepts types and operators only")
types = config.get("types")
if not isinstance(types, dict) or not types:
raise ValueError("operator_config.types must be a nonempty mapping")
for name, settings in types.items():
if name not in _operators()["types"]:
raise ValueError(f"Unsupported generation type: {name}")
if not isinstance(settings, dict) or set(settings) != {"operators", "weights"}:
raise ValueError("Each type requires operators and weights")
ops, ws = settings["operators"], settings["weights"]
if (
not isinstance(ops, list)
or not isinstance(ws, list)
or not ops
or len(ops) != len(ws)
or any(not isinstance(op, str) for op in ops)
or len(set(ops)) != len(ops)
or set(ops) - {"eq", "ne", "lt", "lte", "gt", "gte", "in", "nin"}
):
raise ValueError("Invalid operator names or mismatched weights")
for weight in ws:
_weight(weight)
if not any(ws):
raise ValueError("At least one operator weight must be positive per type")
specials = config.get("operators", {})
if not isinstance(specials, dict) or set(specials) - {"$exists", "$type", "$size"}:
raise ValueError("Unsupported special operator")
for settings in specials.values():
if not isinstance(settings, dict) or set(settings) - {"enabled", "chance"}:
raise ValueError("Special operators accept enabled and chance only")
if type(settings.get("enabled", False)) is not bool:
raise TypeError("enabled must be bool")
_fraction("chance", settings.get("chance", 0))
|
bondi.mongodb.MongoPredicateTemplate
dataclass
One field/operator slot exposed through MongoQueryPattern.predicates.
Attributes:
| Name |
Type |
Description |
field |
str
|
Normalized MongoDB dot path.
|
operator |
str
|
Operator name without '$'.
|
value_type |
str | None
|
BSON-compatible Python type label, or None for fixed literals.
|
arity |
int
|
Number of operands; in uses 2–10, nin uses 1–10, others use one.
|
fixed_value |
bool | str | None
|
Boolean for exists or BSON type name for type; otherwise None.
|
Source code in src/bondi/mongodb/pattern.py
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98 | @dataclass(frozen=True)
class MongoPredicateTemplate:
"""One field/operator slot exposed through MongoQueryPattern.predicates.
Attributes:
field: Normalized MongoDB dot path.
operator: Operator name without '$'.
value_type: BSON-compatible Python type label, or None for fixed literals.
arity: Number of operands; in uses 2–10, nin uses 1–10, others use one.
fixed_value: Boolean for exists or BSON type name for type; otherwise None.
"""
field: str
operator: str
value_type: str | None = None
arity: int = 1
fixed_value: bool | str | None = None
def __post_init__(self):
_field_path(self.field)
if self.operator not in {
"eq",
"ne",
"lt",
"lte",
"gt",
"gte",
"in",
"nin",
"exists",
"type",
"size",
}:
raise ValueError("Unsupported pattern operator")
if type(self.arity) is not int or self.arity < 1:
raise ValueError("arity must be a positive integer")
if self.operator in {"in", "nin"}:
if not (2 if self.operator == "in" else 1) <= self.arity <= 10:
raise ValueError("Unsupported membership arity")
elif self.arity != 1:
raise ValueError("Scalar operators have arity=1")
if self.operator in {"exists", "type"}:
if self.value_type is not None:
raise ValueError("Fixed predicates have no sampled type")
if self.operator == "exists" and type(self.fixed_value) is not bool:
raise ValueError("exists requires a fixed bool")
if self.operator == "type" and self.fixed_value not in _BSON_TYPES:
raise ValueError("type requires a supported BSON type name")
elif self.value_type not in _TYPES or self.fixed_value is not None:
raise ValueError("Sampled predicates require a supported type and no fixed value")
if self.operator == "size" and self.value_type != "int":
raise ValueError("size requires int operands")
|
bondi.mongodb.MongoQueryPattern
dataclass
Complete immutable flat-find shape, independent of operand values.
Field/operator/type/membership length and all query options identify a pattern.
Filter and projection order are canonicalized; sort order is significant.
Patterns may be inspected or extracted from saved queries with from_query().
Logical expressions, object/array equality and multiple operators on one field
are outside this initial subset. They remain representable as MongoFindQuery.
Source code in src/bondi/mongodb/pattern.py
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199 | @dataclass(frozen=True)
class MongoQueryPattern:
"""Complete immutable flat-find shape, independent of operand values.
Field/operator/type/membership length and all query options identify a pattern.
Filter and projection order are canonicalized; sort order is significant.
Patterns may be inspected or extracted from saved queries with from_query().
Logical expressions, object/array equality and multiple operators on one field
are outside this initial subset. They remain representable as MongoFindQuery.
"""
target: MongoCollectionRef
predicates: tuple[MongoPredicateTemplate, ...]
sort: tuple[tuple[str, int], ...] = ()
projection: tuple[tuple[str, int], ...] | None = None
skip: int = 0
limit: int | None = None
def __post_init__(self):
if (
not isinstance(self.predicates, tuple)
or not self.predicates
or not all(isinstance(p, MongoPredicateTemplate) for p in self.predicates)
):
raise ValueError("Patterns require a nonempty tuple of MongoPredicateTemplate")
fields = [p.field for p in self.predicates]
if len(fields) != len(set(fields)):
raise ValueError("One predicate per field is supported")
object.__setattr__(
self, "predicates", tuple(sorted(self.predicates, key=lambda p: p.field))
)
MongoFindQuery(self.target, {}, sort=self.sort, skip=self.skip, limit=self.limit)
if len({p[0] for p in self.sort}) != len(self.sort):
raise ValueError("Duplicate sort field")
for name, _ in self.sort:
_field_path(name)
if self.projection is not None:
if not isinstance(self.projection, tuple) or any(
not isinstance(p, tuple)
or len(p) != 2
or type(p[1]) is not int
or p[1] not in (0, 1)
for p in self.projection
):
raise ValueError("Projection patterns require tuple pairs with 0/1 values")
names = [p[0] for p in self.projection]
for name in names:
_field_path(name)
if len(names) != len(set(names)):
raise ValueError("Duplicate projection field")
non_id = {v for k, v in self.projection if k != "_id"}
if len(non_id) > 1:
raise ValueError("Cannot mix inclusion and exclusion projection")
object.__setattr__(self, "projection", tuple(sorted(self.projection)))
@classmethod
def from_query(cls, query: MongoFindQuery) -> Self:
"""Extract a shape from a supported native find query, including after JSONL loading."""
if not isinstance(query, MongoFindQuery):
raise TypeError("Expected MongoFindQuery")
query.validate()
predicates = []
for name, expression in query.filter.items():
op, operand = "eq", expression
if isinstance(expression, dict):
if len(expression) != 1:
raise ValueError("Only one operator per field is supported")
key, operand = next(iter(expression.items()))
if not isinstance(key, str) or not key.startswith("$"):
raise ValueError("Literal object equality is not a pattern operand")
op = key[1:]
if op in ("exists", "type"):
predicate = MongoPredicateTemplate(name, op, fixed_value=operand)
else:
values = operand if op in ("in", "nin") else [operand]
if not isinstance(values, list) or not values:
raise ValueError("Membership operands require a nonempty list")
kind = _value_type(values[0])
if any(_value_type(v) != kind for v in values) or len(set(values)) != len(values):
raise ValueError("Membership operands must be distinct and of one type")
if op == "size" and (type(operand) is not int or operand < 0):
raise ValueError("size requires a nonnegative integer")
predicate = MongoPredicateTemplate(name, op, kind, len(values))
predicates.append(predicate)
return cls(
query.target,
tuple(predicates),
query.sort,
None if query.projection is None else tuple(query.projection.items()),
query.skip,
query.limit,
)
def matches(self, query: MongoFindQuery) -> bool:
"""Whether the query has this shape; unsupported shapes return False."""
try:
return self == self.from_query(query)
except (ValueError, TypeError):
return False
|
from_query(query)
classmethod
Extract a shape from a supported native find query, including after JSONL loading.
Source code in src/bondi/mongodb/pattern.py
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192 | @classmethod
def from_query(cls, query: MongoFindQuery) -> Self:
"""Extract a shape from a supported native find query, including after JSONL loading."""
if not isinstance(query, MongoFindQuery):
raise TypeError("Expected MongoFindQuery")
query.validate()
predicates = []
for name, expression in query.filter.items():
op, operand = "eq", expression
if isinstance(expression, dict):
if len(expression) != 1:
raise ValueError("Only one operator per field is supported")
key, operand = next(iter(expression.items()))
if not isinstance(key, str) or not key.startswith("$"):
raise ValueError("Literal object equality is not a pattern operand")
op = key[1:]
if op in ("exists", "type"):
predicate = MongoPredicateTemplate(name, op, fixed_value=operand)
else:
values = operand if op in ("in", "nin") else [operand]
if not isinstance(values, list) or not values:
raise ValueError("Membership operands require a nonempty list")
kind = _value_type(values[0])
if any(_value_type(v) != kind for v in values) or len(set(values)) != len(values):
raise ValueError("Membership operands must be distinct and of one type")
if op == "size" and (type(operand) is not int or operand < 0):
raise ValueError("size requires a nonnegative integer")
predicate = MongoPredicateTemplate(name, op, kind, len(values))
predicates.append(predicate)
return cls(
query.target,
tuple(predicates),
query.sort,
None if query.projection is None else tuple(query.projection.items()),
query.skip,
query.limit,
)
|
matches(query)
Whether the query has this shape; unsupported shapes return False.
Source code in src/bondi/mongodb/pattern.py
| def matches(self, query: MongoFindQuery) -> bool:
"""Whether the query has this shape; unsupported shapes return False."""
try:
return self == self.from_query(query)
except (ValueError, TypeError):
return False
|
bondi.mongodb.MongoWorkloadGenerator
Generate flat-find patterns for one collection, with optional selectivity checks.
Parameters:
| Name |
Type |
Description |
Default |
target
|
MongoCollectionRef
|
Collection targeted by every generated query.
|
required
|
schema
|
Schema
|
Observed Schema retaining distinct or full values. Frequencies are ignored.
|
required
|
estimator
|
CardinalityEstimator[MongoFindQuery] | None
|
Optional prepared synchronous CardinalityEstimator[MongoFindQuery].
Required only when selectivity bounds are active. Never opened or closed here.
|
None
|
config
|
MongoGenerationConfig | None
|
One MongoGenerationConfig; omitted settings use documented defaults.
|
None
|
No database connection or event loop is created. Schema and config are retained
by reference and read at each generate() call; do not mutate inputs during a
call. Calls on one generator must not overlap. Patterns support flat filters
only; logical expressions, elemMatch and aggregation generation are deferred.
Source code in src/bondi/mongodb/generation.py
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508 | class MongoWorkloadGenerator:
"""Generate flat-find patterns for one collection, with optional selectivity checks.
Args:
target: Collection targeted by every generated query.
schema: Observed Schema retaining distinct or full values. Frequencies are ignored.
estimator: Optional prepared synchronous CardinalityEstimator[MongoFindQuery].
Required only when selectivity bounds are active. Never opened or closed here.
config: One MongoGenerationConfig; omitted settings use documented defaults.
No database connection or event loop is created. Schema and config are retained
by reference and read at each generate() call; do not mutate inputs during a
call. Calls on one generator must not overlap. Patterns support flat filters
only; logical expressions, elemMatch and aggregation generation are deferred.
"""
def __init__(
self,
*,
target: MongoCollectionRef,
schema: Schema,
estimator: CardinalityEstimator[MongoFindQuery] | None = None,
config: MongoGenerationConfig | None = None,
):
if not isinstance(target, MongoCollectionRef) or not isinstance(schema, Schema):
raise TypeError("Expected MongoCollectionRef target and Schema schema")
if config is not None and not isinstance(config, MongoGenerationConfig):
raise TypeError("config must be MongoGenerationConfig")
if estimator is not None and (
not callable(getattr(estimator, "estimate", None))
or inspect.iscoroutinefunction(estimator.estimate)
):
raise TypeError("estimator must expose a synchronous estimate(query) method")
if schema.value_mode == "none":
raise ValueError("Generation requires Schema values='distinct' or values='full'")
self.target, self.schema, self.estimator = target, schema, estimator
self.config = config if config is not None else MongoGenerationConfig()
self.config.validate()
self._last_stats = {}
@property
def last_stats(self) -> Mapping[str, Any]:
"""Read-only snapshot of the latest generation call, including interrupted/failed calls."""
return _snapshot(self._last_stats)
@overload
def generate(
self,
*,
num_patterns: int,
instances_per_pattern: int = 1,
return_patterns: Literal[False] = False,
seed: int | None = None,
min_selectivity: float = 0.0,
max_selectivity: float = 1.0,
validate_pattern: Callable | None = None,
max_candidate_attempts: int = 10_000,
max_instance_attempts: int = 1000,
timeout_seconds: float = 60,
) -> Workload: ...
@overload
def generate(
self,
*,
num_patterns: int,
instances_per_pattern: int = 1,
return_patterns: Literal[True],
seed: int | None = None,
min_selectivity: float = 0.0,
max_selectivity: float = 1.0,
validate_pattern: Callable | None = None,
max_candidate_attempts: int = 10_000,
max_instance_attempts: int = 1000,
timeout_seconds: float = 60,
) -> tuple[Workload, list[MongoQueryPattern]]: ...
def generate(
self,
*,
num_patterns: int,
instances_per_pattern: int = 1,
return_patterns: bool = False,
seed: int | None = None,
min_selectivity: float = 0.0,
max_selectivity: float = 1.0,
validate_pattern: Callable | None = None,
max_candidate_attempts: int = 10_000,
max_instance_attempts: int = 1000,
timeout_seconds: float = 60,
) -> Workload | tuple[Workload, list[MongoQueryPattern]]:
"""Return exactly N patterns of K instances, or raise WorkloadGenerationError.
Args:
num_patterns: Positive number of distinct complete patterns.
instances_per_pattern: Positive group size; one gives unique-pattern queries.
return_patterns: Also return a list indexed by each entry's pattern_id.
seed: Local RNG seed. Reproducibility requires fixed inputs and estimator responses.
min_selectivity: Inclusive lower estimated matching fraction, before skip/limit.
max_selectivity: Inclusive upper bound; [0, 1] disables estimation.
validate_pattern: Callback(candidate, tuple_of_completed_patterns) -> bool,
before estimation. Exceptions propagate unchanged.
max_candidate_attempts: Global budget for discovering patterns.
max_instance_attempts: Attempts per additional instance, not per workload.
timeout_seconds: Monotonic deadline, checked between operations including
estimator/callback calls. Does not interrupt a running external call.
Each group fixes fields/operators/types/membership arity and exact options;
exists/type literals are fixed. Duplicate full instances are allowed.
Output is shuffled. No partial workload is returned; errors expose stats.
Estimator errors propagate. The estimator owns its operation timeout.
"""
self._last_stats = {}
for name, value in [
("num_patterns", num_patterns),
("instances_per_pattern", instances_per_pattern),
("max_candidate_attempts", max_candidate_attempts),
("max_instance_attempts", max_instance_attempts),
]:
_positive(name, value)
if type(return_patterns) is not bool:
raise TypeError("return_patterns must be bool")
if seed is not None and type(seed) is not int:
raise TypeError("seed must be int or None")
_fraction("min_selectivity", min_selectivity)
_fraction("max_selectivity", max_selectivity)
if min_selectivity > max_selectivity:
raise ValueError("min_selectivity must not exceed max_selectivity")
if (
type(timeout_seconds) not in (int, float)
or not math.isfinite(timeout_seconds)
or timeout_seconds <= 0
):
raise ValueError("timeout_seconds must be positive and finite")
if validate_pattern is not None and (
not callable(validate_pattern) or inspect.iscoroutinefunction(validate_pattern)
):
raise TypeError("validate_pattern must be a synchronous callable")
constrained = min_selectivity > 0 or max_selectivity < 1
if constrained and self.estimator is None:
raise ValueError("Selectivity constraints require a prepared estimator")
config = deepcopy(self.config)
config.validate()
rng, started = random.Random(seed), time.monotonic()
stats = dict(
candidate_attempts=0,
value_attempts=0,
duplicate_candidates=0,
structural_rejections=0,
validation_rejections=0,
selectivity_rejections=0,
discarded_groups=0,
estimator_calls=0,
completed_patterns=0,
elapsed_seconds=0.0,
)
def check():
stats["elapsed_seconds"] = time.monotonic() - started
if stats["elapsed_seconds"] >= timeout_seconds:
raise WorkloadGenerationError("Generation deadline exceeded", stats)
def accepted(query):
check()
if not constrained:
return True
stats["estimator_calls"] += 1
# Isolate user-supplied estimators from the candidate retained for output.
result = self.estimator.estimate(deepcopy(query))
if inspect.iscoroutine(result):
result.close()
raise TypeError("Use a synchronous estimator")
if not isinstance(result, CardinalityEstimate):
raise TypeError("estimate() must return CardinalityEstimate")
if (
type(result.population_size) is not int
or result.population_size < 0
or type(result.cardinality) not in (int, float)
or not math.isfinite(result.cardinality)
or not 0 <= result.cardinality <= result.population_size
):
raise ValueError("Estimator returned invalid cardinality/population")
check()
ok = min_selectivity <= result.selectivity <= max_selectivity
if not ok:
stats["selectivity_rejections"] += 1
return ok
try:
fields = _fields(self.schema, config, check)
if len(fields) < config.min_predicates:
raise WorkloadGenerationError("Insufficient eligible fields", stats)
patterns, entries, seen = [], [], set()
slot = None
while (
len(patterns) < num_patterns
and stats["candidate_attempts"] < max_candidate_attempts
):
check()
if slot is None:
count = rng.randint(
config.min_predicates, min(config.max_predicates, len(fields))
)
presence = {
name: rng.random() < getattr(config, name + "_probability")
for name in ("sort", "skip", "limit", "projection")
}
if presence["skip"] and config.skip_requires_limit:
presence["limit"] = True
slot = count, presence
stats["pending_pattern_target"] = dict(
pattern_id=len(patterns), num_predicates=count, **presence
)
count, presence = slot
stats["candidate_attempts"] += 1
names = _sample_fields(rng, fields, count, config)
predicates, pools = {}, {}
for name in names:
predicate = _predicate(fields[name], config, rng)
if predicate is not None:
predicates[name], pools[name] = predicate
if len(predicates) != count:
stats["structural_rejections"] += 1
continue
query = MongoFindQuery(
self.target, predicates, **_options(names, list(fields), config, presence, rng)
)
pattern = MongoQueryPattern.from_query(query)
if pattern in seen:
stats["duplicate_candidates"] += 1
continue
if validate_pattern is not None:
valid = validate_pattern(pattern, tuple(patterns))
if inspect.iscoroutine(valid):
valid.close()
if type(valid) is not bool:
raise TypeError("validate_pattern must return bool")
check()
if not valid:
stats["validation_rejections"] += 1
continue
if not accepted(query):
continue
group = [query]
while len(group) < instances_per_pattern:
for _ in range(max_instance_attempts):
check()
stats["value_attempts"] += 1
query = _instantiate(pattern, pools, rng)
if accepted(query):
group.append(query)
break
else:
break
if len(group) != instances_per_pattern:
stats["discarded_groups"] += 1
continue
entries.extend(WorkloadEntry(q, len(patterns)) for q in group)
patterns.append(pattern)
seen.add(pattern)
stats["completed_patterns"] = len(patterns)
stats.pop("pending_pattern_target", None)
slot = None
check()
if len(patterns) != num_patterns:
raise WorkloadGenerationError("Candidate attempt budget exhausted", stats)
rng.shuffle(entries)
workload = Workload(entries)
check()
return (workload, patterns) if return_patterns else workload
finally:
stats["elapsed_seconds"] = time.monotonic() - started
self._last_stats = deepcopy(stats)
|
last_stats
property
Read-only snapshot of the latest generation call, including interrupted/failed calls.
generate(*, num_patterns, instances_per_pattern=1, return_patterns=False, seed=None, min_selectivity=0.0, max_selectivity=1.0, validate_pattern=None, max_candidate_attempts=10000, max_instance_attempts=1000, timeout_seconds=60)
generate(
*,
num_patterns: int,
instances_per_pattern: int = 1,
return_patterns: Literal[False] = False,
seed: int | None = None,
min_selectivity: float = 0.0,
max_selectivity: float = 1.0,
validate_pattern: Callable | None = None,
max_candidate_attempts: int = 10000,
max_instance_attempts: int = 1000,
timeout_seconds: float = 60,
) -> Workload
generate(
*,
num_patterns: int,
instances_per_pattern: int = 1,
return_patterns: Literal[True],
seed: int | None = None,
min_selectivity: float = 0.0,
max_selectivity: float = 1.0,
validate_pattern: Callable | None = None,
max_candidate_attempts: int = 10000,
max_instance_attempts: int = 1000,
timeout_seconds: float = 60,
) -> tuple[Workload, list[MongoQueryPattern]]
Return exactly N patterns of K instances, or raise WorkloadGenerationError.
Parameters:
| Name |
Type |
Description |
Default |
num_patterns
|
int
|
Positive number of distinct complete patterns.
|
required
|
instances_per_pattern
|
int
|
Positive group size; one gives unique-pattern queries.
|
1
|
return_patterns
|
bool
|
Also return a list indexed by each entry's pattern_id.
|
False
|
seed
|
int | None
|
Local RNG seed. Reproducibility requires fixed inputs and estimator responses.
|
None
|
min_selectivity
|
float
|
Inclusive lower estimated matching fraction, before skip/limit.
|
0.0
|
max_selectivity
|
float
|
Inclusive upper bound; [0, 1] disables estimation.
|
1.0
|
validate_pattern
|
Callable | None
|
Callback(candidate, tuple_of_completed_patterns) -> bool,
before estimation. Exceptions propagate unchanged.
|
None
|
max_candidate_attempts
|
int
|
Global budget for discovering patterns.
|
10000
|
max_instance_attempts
|
int
|
Attempts per additional instance, not per workload.
|
1000
|
timeout_seconds
|
float
|
Monotonic deadline, checked between operations including
estimator/callback calls. Does not interrupt a running external call.
|
60
|
Each group fixes fields/operators/types/membership arity and exact options;
exists/type literals are fixed. Duplicate full instances are allowed.
Output is shuffled. No partial workload is returned; errors expose stats.
Estimator errors propagate. The estimator owns its operation timeout.
Source code in src/bondi/mongodb/generation.py
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508 | def generate(
self,
*,
num_patterns: int,
instances_per_pattern: int = 1,
return_patterns: bool = False,
seed: int | None = None,
min_selectivity: float = 0.0,
max_selectivity: float = 1.0,
validate_pattern: Callable | None = None,
max_candidate_attempts: int = 10_000,
max_instance_attempts: int = 1000,
timeout_seconds: float = 60,
) -> Workload | tuple[Workload, list[MongoQueryPattern]]:
"""Return exactly N patterns of K instances, or raise WorkloadGenerationError.
Args:
num_patterns: Positive number of distinct complete patterns.
instances_per_pattern: Positive group size; one gives unique-pattern queries.
return_patterns: Also return a list indexed by each entry's pattern_id.
seed: Local RNG seed. Reproducibility requires fixed inputs and estimator responses.
min_selectivity: Inclusive lower estimated matching fraction, before skip/limit.
max_selectivity: Inclusive upper bound; [0, 1] disables estimation.
validate_pattern: Callback(candidate, tuple_of_completed_patterns) -> bool,
before estimation. Exceptions propagate unchanged.
max_candidate_attempts: Global budget for discovering patterns.
max_instance_attempts: Attempts per additional instance, not per workload.
timeout_seconds: Monotonic deadline, checked between operations including
estimator/callback calls. Does not interrupt a running external call.
Each group fixes fields/operators/types/membership arity and exact options;
exists/type literals are fixed. Duplicate full instances are allowed.
Output is shuffled. No partial workload is returned; errors expose stats.
Estimator errors propagate. The estimator owns its operation timeout.
"""
self._last_stats = {}
for name, value in [
("num_patterns", num_patterns),
("instances_per_pattern", instances_per_pattern),
("max_candidate_attempts", max_candidate_attempts),
("max_instance_attempts", max_instance_attempts),
]:
_positive(name, value)
if type(return_patterns) is not bool:
raise TypeError("return_patterns must be bool")
if seed is not None and type(seed) is not int:
raise TypeError("seed must be int or None")
_fraction("min_selectivity", min_selectivity)
_fraction("max_selectivity", max_selectivity)
if min_selectivity > max_selectivity:
raise ValueError("min_selectivity must not exceed max_selectivity")
if (
type(timeout_seconds) not in (int, float)
or not math.isfinite(timeout_seconds)
or timeout_seconds <= 0
):
raise ValueError("timeout_seconds must be positive and finite")
if validate_pattern is not None and (
not callable(validate_pattern) or inspect.iscoroutinefunction(validate_pattern)
):
raise TypeError("validate_pattern must be a synchronous callable")
constrained = min_selectivity > 0 or max_selectivity < 1
if constrained and self.estimator is None:
raise ValueError("Selectivity constraints require a prepared estimator")
config = deepcopy(self.config)
config.validate()
rng, started = random.Random(seed), time.monotonic()
stats = dict(
candidate_attempts=0,
value_attempts=0,
duplicate_candidates=0,
structural_rejections=0,
validation_rejections=0,
selectivity_rejections=0,
discarded_groups=0,
estimator_calls=0,
completed_patterns=0,
elapsed_seconds=0.0,
)
def check():
stats["elapsed_seconds"] = time.monotonic() - started
if stats["elapsed_seconds"] >= timeout_seconds:
raise WorkloadGenerationError("Generation deadline exceeded", stats)
def accepted(query):
check()
if not constrained:
return True
stats["estimator_calls"] += 1
# Isolate user-supplied estimators from the candidate retained for output.
result = self.estimator.estimate(deepcopy(query))
if inspect.iscoroutine(result):
result.close()
raise TypeError("Use a synchronous estimator")
if not isinstance(result, CardinalityEstimate):
raise TypeError("estimate() must return CardinalityEstimate")
if (
type(result.population_size) is not int
or result.population_size < 0
or type(result.cardinality) not in (int, float)
or not math.isfinite(result.cardinality)
or not 0 <= result.cardinality <= result.population_size
):
raise ValueError("Estimator returned invalid cardinality/population")
check()
ok = min_selectivity <= result.selectivity <= max_selectivity
if not ok:
stats["selectivity_rejections"] += 1
return ok
try:
fields = _fields(self.schema, config, check)
if len(fields) < config.min_predicates:
raise WorkloadGenerationError("Insufficient eligible fields", stats)
patterns, entries, seen = [], [], set()
slot = None
while (
len(patterns) < num_patterns
and stats["candidate_attempts"] < max_candidate_attempts
):
check()
if slot is None:
count = rng.randint(
config.min_predicates, min(config.max_predicates, len(fields))
)
presence = {
name: rng.random() < getattr(config, name + "_probability")
for name in ("sort", "skip", "limit", "projection")
}
if presence["skip"] and config.skip_requires_limit:
presence["limit"] = True
slot = count, presence
stats["pending_pattern_target"] = dict(
pattern_id=len(patterns), num_predicates=count, **presence
)
count, presence = slot
stats["candidate_attempts"] += 1
names = _sample_fields(rng, fields, count, config)
predicates, pools = {}, {}
for name in names:
predicate = _predicate(fields[name], config, rng)
if predicate is not None:
predicates[name], pools[name] = predicate
if len(predicates) != count:
stats["structural_rejections"] += 1
continue
query = MongoFindQuery(
self.target, predicates, **_options(names, list(fields), config, presence, rng)
)
pattern = MongoQueryPattern.from_query(query)
if pattern in seen:
stats["duplicate_candidates"] += 1
continue
if validate_pattern is not None:
valid = validate_pattern(pattern, tuple(patterns))
if inspect.iscoroutine(valid):
valid.close()
if type(valid) is not bool:
raise TypeError("validate_pattern must return bool")
check()
if not valid:
stats["validation_rejections"] += 1
continue
if not accepted(query):
continue
group = [query]
while len(group) < instances_per_pattern:
for _ in range(max_instance_attempts):
check()
stats["value_attempts"] += 1
query = _instantiate(pattern, pools, rng)
if accepted(query):
group.append(query)
break
else:
break
if len(group) != instances_per_pattern:
stats["discarded_groups"] += 1
continue
entries.extend(WorkloadEntry(q, len(patterns)) for q in group)
patterns.append(pattern)
seen.add(pattern)
stats["completed_patterns"] = len(patterns)
stats.pop("pending_pattern_target", None)
slot = None
check()
if len(patterns) != num_patterns:
raise WorkloadGenerationError("Candidate attempt budget exhausted", stats)
rng.shuffle(entries)
workload = Workload(entries)
check()
return (workload, patterns) if return_patterns else workload
finally:
stats["elapsed_seconds"] = time.monotonic() - started
self._last_stats = deepcopy(stats)
|
bondi.mongodb.WorkloadGenerationError
Bases: RuntimeError
Generation exhausted a budget without a complete workload; stats describe progress.
Source code in src/bondi/mongodb/generation.py
| class WorkloadGenerationError(RuntimeError):
"""Generation exhausted a budget without a complete workload; stats describe progress."""
def __init__(self, message: str, stats: dict):
self.stats = _snapshot(stats)
super().__init__(message)
|
Paths and supported values
Generation supports strings, booleans, BSON-range integers, finite floats,
datetimes, ObjectIds and null. Large Python integers normalize to Int64; dates
normalize to BSON UTC millisecond precision. Retention of other Python types by
Schema does not imply support for generating MongoDB predicates from them; exclude
those fields or explicitly transform source data first.
Array traversal markers are removed when generating MongoDB dot paths. Values
remain in their chosen typed source pool throughout a pattern. Mixed scalar and
array sources can map to the same Mongo path, but only one predicate is selected
per path. Missingness used for exists proposals comes from schema observations;
MongoDB/your estimator evaluates the actual semantics.
Literal dotted keys, empty/dollar-prefixed path segments, positional numeric
segments and literal [] segments are rejected rather than reinterpreted. Use
schema display paths in exclusions for literal dotted keys, e.g. r"literal\.dot".
Container-only paths without primitive value pools are not candidates. Generated
queries still obey native MongoDB execution restrictions; for example, sorting
on parallel arrays is not generally valid. Use field inclusion to restrict an
experiment to suitable sortable fields.
Custom constraints, budgets and diagnostics
def accept_pattern(candidate, accepted_patterns):
fields = {predicate.field for predicate in candidate.predicates}
return not {"start_station", "end_station"} <= fields
workload = generator.generate(
num_patterns=5,
instances_per_pattern=20,
validate_pattern=accept_pattern,
max_candidate_attempts=10_000,
max_instance_attempts=1_000,
timeout_seconds=60,
seed=42,
)
print(dict(generator.last_stats))
The callback runs before estimation, receives only fully completed prior patterns
as a tuple, and must return bool. The candidate budget is global; the instance
budget applies to each additional instance. Failed groups are discarded completely.
Deadline checks occur between operations; they cannot interrupt a running estimator
or callback. Configure the estimator's executor timeout separately.
On budget exhaustion, WorkloadGenerationError.stats reports progress. There is
no partial workload result. generator.last_stats is a read-only snapshot with
attempts, rejection counts, estimator calls, discarded groups, completed patterns,
elapsed seconds and, when unfinished, the pending slot targets. Exceptions and
interruptions also update diagnostics after generation starts. Input-validation
failures clear the previous snapshot. Calls on one generator must not overlap.
Reproducibility and persistence
Fixed schema contents, config, seed and estimator responses give reproducible
queries without changing Python's global random state. Value pools are canonically
ordered, independent of hash seed and value frequencies. Generator seeds do not
control database sampling or elapsed timings. Do not mutate inputs during a call.
from bondi.workload import Workload
from bondi.mongodb import MongoQueryPattern
workload.save("workload.jsonl")
loaded = Workload.load("workload.jsonl")
pattern = MongoQueryPattern.from_query(loaded[0].query)
Existing JSONL preserves queries and integer pattern IDs. Patterns can be extracted
from saved queries; generation statistics are separate and are not added to the
workload file. Queries remain mutable, so edits can invalidate their original shape
or selectivity. The returned patterns are immutable snapshots.
See notebooks/workload_generation.ipynb for a toy example and optional live
selectivity checks against the Docker MongoDB demo collection.
One configuration
MongoGenerationConfig contains all settings. Change only what your experiment needs:
config = MongoGenerationConfig(
min_predicates=2,
max_predicates=4,
exclude_fields=["_id", "name", "address"],
field_weights={"default": 1.0, "overrides": {"attributes.*": 0.05}},
sort_probability=0.3,
limit_probability=1.0,
limit_min=10,
limit_max=100,
projection_probability=0.2,
)
| Settings |
Defaults and meaning |
min_predicates, max_predicates |
1, 8; maximum capped by available fields, minimum never relaxed |
include_fields, exclude_fields |
None, ["_id"]; exact normalized paths, include takes precedence |
field_weights |
Default 1; positive weights with case-sensitive glob overrides, last match wins |
sort_probability |
0.3 |
sort_on_filter_field_ratio |
0.7 |
sort_two_field_ratio, sort_ascending_ratio |
0.3, 0.8 |
skip_probability, limit_probability |
0.0, 0.2 |
skip_min, skip_max |
0, 1000, inclusive |
limit_min, limit_max |
1, 100, inclusive |
skip_requires_limit |
True |
projection_probability |
0.0; inclusion projections |
projection_filter_overlap_ratio |
0.5 |
projection_min_fields, projection_max_fields |
1, 5 requested fields |
projection_exclude_id_ratio |
0.5; adds _id: 0 unless an _id path is projected |
Field weights apply to filters, sorts and projections, with weighted selection
without replacement. Exclusions apply to all three (except explicit _id: 0).
Requested sort/projection counts may shrink when suitable fields are scarce.
Projection paths that collide with parent/child paths are not selected together.
Probabilities describe proposed pattern slots; rejection changes final frequencies.
For each slot, predicate count and option-presence flags stay fixed through retries.
Operator configuration retains the existing explicit per-type weights:
config = MongoGenerationConfig(
operator_config={
"types": {
"int": {"operators": ["eq", "gte", "in"], "weights": [1, 1, 0.5]},
"str": {"operators": ["eq", "ne"], "weights": [1, 0.2]},
},
"operators": {
"$exists": {"enabled": True, "chance": 0.3},
"$type": {"enabled": False, "chance": 0},
"$size": {"enabled": False, "chance": 0},
},
},
)
This replaces, rather than merges with, defaults. Omitted scalar types are
not proposed; omitted special operators are disabled. Ordinary operators are
eq, ne, lt, lte, gt, gte, in, nin. Zero operator weights disable
individual operators; at least one weight per configured type must be positive.
Default integer operators are comparisons plus equality (weights 1,1,1,1,0.5);
floats use four comparisons; strings use eq/ne/in/nin/gte/lte with weights
1/0.2/0.4/0.2/0.3/0.3. Booleans and ObjectIds use eq/ne (1/0.3), datetimes use
comparisons/equality equally, and null uses eq/ne (1/0.3). Int64 follows integers.
$in uses 2–10 distinct values, $nin 1–10, and $size 0–5. Special operators
conditionally replace an ordinary predicate, in exists/type/size order. Defaults
are enabled with chances 0.3/0.2/0.1: exists requires observed missingness, type
requires multiple observed types, and size requires an observed array.
These probabilities are conditional, not independent final proportions.