///|
/// Explain schema incompatibilities without attempting an unsafe automatic migration.
pub fn compare_schemas(
  expected : Array[Table],
  actual : Array[Table],
) -> Json raise CdcError {
  let issues : Array[Json] = []
  let wanted : Map[String, Table] = Map([])
  let observed : Map[String, Table] = Map([])
  for table in expected {
    table.validate()
    if wanted.contains(table.name) {
      raise Invalid("Duplicate expected schema")
    }
    wanted[table.name] = table
  }
  for table in actual {
    table.validate()
    if observed.contains(table.name) {
      raise Invalid("Duplicate actual schema")
    }
    observed[table.name] = table
  }
  for table in expected {
    match observed.get(table.name) {
      None => issues.push({ "table": table.name, "kind": "missing_table" })
      Some(other) => {
        let before : Map[String, Column] = Map([])
        let after : Map[String, Column] = Map([])
        for column in table.columns {
          before[column.name] = column
        }
        for column in other.columns {
          after[column.name] = column
        }
        for index, column in table.columns {
          match after.get(column.name) {
            None =>
              issues.push({
                "table": table.name,
                "column": column.name,
                "kind": "missing_column",
              })
            Some(changed) => {
              if column.declared_type != changed.declared_type {
                issues.push({
                  "table": table.name,
                  "column": column.name,
                  "kind": "declared_type",
                  "expected": column.declared_type,
                  "actual": changed.declared_type,
                })
              }
              if column.required != changed.required {
                issues.push({
                  "table": table.name,
                  "column": column.name,
                  "kind": "nullability",
                  "expected": column.required,
                  "actual": changed.required,
                })
              }
              if column.key_position != changed.key_position {
                issues.push({
                  "table": table.name,
                  "column": column.name,
                  "kind": "primary_key",
                  "expected": column.key_position,
                  "actual": changed.key_position,
                })
              }
              let position = other.column_index(column.name).unwrap()
              if position != index {
                issues.push({
                  "table": table.name,
                  "column": column.name,
                  "kind": "column_order",
                  "expected": index,
                  "actual": position,
                })
              }
            }
          }
        }
        for column in other.columns {
          if !before.contains(column.name) {
            issues.push({
              "table": table.name,
              "column": column.name,
              "kind": "extra_column",
            })
          }
        }
      }
    }
  }
  for table in actual {
    if !wanted.contains(table.name) {
      issues.push({ "table": table.name, "kind": "extra_table" })
    }
  }
  {
    "compatible": issues.is_empty(),
    "issues": Json::array(issues),
    "automatic_migration": false,
  }
}