///|
/// Includes row index, RNG, unique sets, table data and the schema contract.
pub fn Session::checkpoint(self : Session) -> Json {
  let seen : Map[String, Json] = Map([])
  for path, entries in self.seen {
    let keys = []
    for key, _ in entries {
      keys.push(key)
    }
    keys.sort_by((a, b) => a.lexical_compare(b))
    seen[path] = keys.to_json()
  }
  let result : Map[String, Json] = Map([
    ("version", (1).to_json()),
    ("schema", ToJson::to_json(self.spec)),
    ("rng", self.generator.snapshot().to_json()),
    ("index", self.index.to_json()),
    ("seen", seen.to_json()),
    ("tables", self.tables.to_json()),
  ])
  // Deep copy table values and schema arrays before handing them to the caller.
  @json.parse(result.to_json().stringify()) catch {
    _ => abort("checkpoint encoding")
  }
}

///|
/// Restore is atomic and requires the same schema. Checkpoints are not credentials.
pub fn Session::restore(
  self : Session,
  checkpoint : Json,
) -> Unit raise FakeError {
  let fields = object(checkpoint)
  reject_unknown(fields, ["version", "schema", "rng", "index", "seen", "tables"])
  if int_option(fields, "version", 0) != 1 ||
    required(fields, "schema") != ToJson::to_json(self.spec) {
    raise Invalid("checkpoint version/schema mismatch")
  }
  let state : UInt = @json.from_json(required(fields, "rng")) catch {
    _ => raise Invalid("invalid checkpoint RNG")
  }
  let index = int_option(fields, "index", -1)
  if state == 0U || index < 0 || index > 1000000000 {
    raise Invalid("checkpoint state bounds")
  }
  let seen : Map[String, Map[String, Bool]] = Map([])
  let mut count = 0
  let mut size = 0
  for path, value in object(required(fields, "seen")) {
    if !self.unique_paths.contains(path) {
      raise Invalid("unknown checkpoint unique path")
    }
    let keys : Array[String] = @json.from_json(value) catch {
      _ => raise Invalid("checkpoint unique array")
    }
    let entries : Map[String, Bool] = Map([])
    for key in keys {
      count += 1
      size += key.length()
      if count > 200000 || size > 8000000 || entries.contains(key) {
        raise Invalid("checkpoint unique budget/duplicate")
      }
      let decoded = @json.parse(key) catch {
        _ => raise Invalid("checkpoint unique encoding")
      }
      if scalar(decoded).stringify() != key {
        raise Invalid("checkpoint unique canonical value")
      }
      entries[key] = true
    }
    seen[path] = entries
  }
  let candidate = Session::new(state, self.spec)
  for name, value in object(required(fields, "tables")) {
    guard value is Array(rows) else { raise Invalid("checkpoint table array") }
    candidate.bind_table(name, rows)
  }
  self.generator.restore(state)
  self.index = index
  self.seen = seen
  self.tables = candidate.tables
  self.unique_count = count
  self.unique_text = size
}