///|
fn checked_integer(value : Int64, path : String) -> Result[Value, Issue] {
if value < -2147483648L || value > 2147483647L {
Err(issue("arithmetic_overflow", path, "Derived integer exceeds Int32"))
} else {
Ok(Integer(value.to_int()))
}
}
///|
fn generate_value(
generator : Generator,
rng : Random,
index : Int,
row : Row,
tables : Map[String, Table],
indexes : Map[String, Map[String, Row]],
path : String,
reference_time : String,
remaining_text_units : Int,
) -> Result[Value, Issue] {
match generator {
Constant(value) => Ok(value)
Sequence(start, step) =>
checked_integer(
start.to_int64() + index.to_int64() * step.to_int64(),
path,
)
IntegerRange(min, max) => {
let width = (max.to_int64() - min.to_int64() + 1L).to_int()
Ok(Integer(min + rng.below(width).unwrap()))
}
BooleanChance(chance) => Ok(Boolean(rng.below(1000).unwrap() < chance))
DateOffset(min, max) => {
let delta = min + rng.below(max - min + 1).unwrap()
match reference_date(reference_time).unwrap().add_days(delta) {
Ok(date) => Ok(Text(date.to_string()))
Err(error) => Err(error)
}
}
Choice(values) => Ok(values[rng.below(values.length()).unwrap()])
WeightedChoice(values) => {
let mut total = 0
for (_, weight) in values {
total += weight
}
let mut chosen = rng.below(total).unwrap()
for (value, weight) in values {
if chosen < weight {
return Ok(value)
}
chosen -= weight
}
Err(issue("weight_total", path, "Weighted choice is empty"))
}
Pattern(alphabet, length) => {
if length > remaining_text_units {
return Err(
issue("text_limit", path, "Pattern exceeds remaining text budget"),
)
}
let chars = alphabet.to_array()
let result = StringBuilder()
for _ in 0.. {
let parent = tables.get(target).unwrap()
if parent.rows.is_empty() {
return Err(issue("empty_reference", path, "No parent rows"))
}
match parent.rows[rng.below(parent.rows.length()).unwrap()].get(key) {
Some(Null) | None =>
Err(
issue(
"invalid_reference_key", path, "Parent key is missing or null",
),
)
Some(value) => Ok(value)
}
}
Copy(name) =>
match row.get(name) {
Some(value) => Ok(value)
None =>
Err(
issue(
"missing_dependency",
path,
"Dependency is unavailable: " + name,
),
)
}
Add(a, b) | Multiply(a, b) =>
match (row.get(a), row.get(b)) {
(Some(Integer(left)), Some(Integer(right))) => {
let value = if generator is Add(_, _) {
left.to_int64() + right.to_int64()
} else {
left.to_int64() * right.to_int64()
}
checked_integer(value, path)
}
_ =>
Err(
issue(
"invalid_operand", path, "Arithmetic requires two non-null integers",
),
)
}
Concat(names, separator) => {
let parts : Array[String] = []
let mut length = (names.length() - 1).max(0).to_int64() *
separator.length().to_int64()
for name in names {
match row.get(name) {
Some(value) => {
let text = value.display()
length += text.length().to_int64()
if length > remaining_text_units.to_int64() {
return Err(
issue(
"text_limit", path, "Concatenation exceeds remaining text budget",
),
)
}
parts.push(text)
}
None =>
return Err(
issue("missing_dependency", path, "Unknown concat field: " + name),
)
}
}
Ok(Text(parts.join(separator)))
}
Lookup(local_field, target, key, value_field) => {
let selected = row.get(local_field)
if selected == None || selected == Some(Null) {
return Err(
issue("invalid_lookup", path, "Lookup key is missing or null"),
)
}
match
indexes.get(target + "." + key).unwrap().get(selected.unwrap().key()) {
Some(parent) =>
return match parent.get(value_field) {
Some(value) => Ok(value)
None =>
Err(issue("missing_lookup", path, "Lookup value is missing"))
}
None => ()
}
Err(
issue("unmatched_lookup", path, "No parent row matches the local key"),
)
}
}
}
///|
fn finite_capacity(generator : Generator) -> Int64? {
match generator {
Constant(Null) => None
Constant(_) => Some(1L)
BooleanChance(0 | 1000) => Some(1L)
BooleanChance(_) => Some(2L)
IntegerRange(min, max) | DateOffset(min, max) =>
Some(max.to_int64() - min.to_int64() + 1L)
Choice(values) => {
let keys : Map[String, Bool] = Map([])
for value in values {
keys[value.key()] = true
}
Some(keys.length().to_int64())
}
WeightedChoice(values) => {
let keys : Map[String, Bool] = Map([])
for (value, _) in values {
keys[value.key()] = true
}
Some(keys.length().to_int64())
}
Sequence(_, 0) => Some(1L)
Pattern(alphabet, length) => {
let chars : Map[Char, Bool] = Map([])
for c in alphabet.iter() {
chars[c] = true
}
let mut total = 1L
for _ in 0.. 2147483647L / chars.length().to_int64() {
return None
}
total *= chars.length().to_int64()
}
Some(total)
}
_ => None
}
}
///|
/// Construct a dataset. On failure no partial dataset is returned.
pub fn Plan::generate(self : Plan) -> Result[Dataset, Issue] {
let tables : Map[String, Table] = Map([])
let indexes : Map[String, Map[String, Row]] = Map([])
let result : Array[Table] = []
let mut text_units = 0L
for ei in self.entity_order {
let entity = self.model.entities[ei]
let sets : Array[Map[String, Bool]] = entity.fields.map(_ => Map([]))
let domains = entity.fields.map(field => {
if field.unique || field.primary {
unique_domain(field.generator, tables, self.context.reference_time)
} else {
None
}
})
for field in entity.fields {
if (field.unique || field.primary) && field.null_per_mille == 0 {
if finite_capacity(field.generator) is Some(capacity) &&
entity.count.to_int64() > capacity {
return Err(
issue(
"unsatisfiable_unique",
entity.name + "." + field.name,
"Requested rows exceed the finite unique domain",
),
)
}
}
}
let rows : Array[Row] = []
for index in 0.. 0 &&
rng.below(1000).unwrap() < field.null_per_mille {
Null
} else {
match domains[fi] {
Some(domain) =>
match domain.draw(rng) {
Some(value) => value
None =>
return Err(
issue(
"unsatisfiable_unique", path, "Unique domain has no remaining non-null values",
),
)
}
None =>
match
generate_value(
field.generator,
rng,
index,
row,
tables,
indexes,
path,
self.context.reference_time,
(self.context.limits.max_text_units.to_int64() - text_units).to_int(),
) {
Ok(value) => value
Err(error) => return Err(error)
}
}
}
if field.primary && value == Null {
return Err(
issue("null_primary", path, "Primary keys cannot be null"),
)
}
// Like SQL UNIQUE, multiple null values are permitted.
if value != Null && (field.unique || field.primary) {
let key = value.key()
if sets[fi].contains(key) {
continue
}
sets[fi][key] = true
}
accepted = Some(value)
break
}
match accepted {
None =>
return Err(
issue(
"attempt_budget", path, "Unique generation exhausted its attempt budget",
),
)
Some(value) => {
if value is Text(text) {
text_units += text.length().to_int64()
if text_units > self.context.limits.max_text_units.to_int64() {
return Err(
issue("text_limit", path, "Generated text exceeds the limit"),
)
}
}
cells.push({ name: field.name, value, })
}
}
}
// Output is in declaration order, regardless of evaluation order.
rows.push({
cells: entity.fields.map(f => {
name: f.name,
value: row.get(f.name).unwrap(),
}),
})
}
let table : Table = { name: entity.name, rows, }
for field in entity.fields {
if field.unique || field.primary {
let entries : Map[String, Row] = Map([])
for row in rows {
if row.get(field.name) is Some(value) && value != Null {
entries[value.key()] = row
}
}
indexes[entity.name + "." + field.name] = entries
}
}
tables[entity.name] = table
result.push(table)
}
Ok({
model: self.model.name,
seed: self.context.seed,
algorithm: algorithm_version(),
reference_time: self.context.reference_time,
tables: result,
})
}
///|
pub fn generate(
model : Model,
context? : Context = Context::new(1U),
) -> Result[Dataset, Array[Issue]] {
match compile(model, context~) {
Err(errors) => Err(errors)
Ok(plan) =>
match plan.generate() {
Ok(dataset) => Ok(dataset)
Err(error) => Err([error])
}
}
}