///|
/// One independent input. No transaction or setting state flows between inputs.
pub(all) struct Migration {
path : String
source : String
execution : Execution
initial_transaction : Bool
require_lock_timeout : Bool
max_lock_timeout_ms : Int32?
}
///|
pub fn Migration::new(
path : String,
source : String,
execution~ : Execution,
initial_transaction~ : Bool = false,
require_lock_timeout~ : Bool = false,
max_lock_timeout_ms? : Int32,
) -> Migration {
{
path,
source,
execution,
initial_transaction,
require_lock_timeout,
max_lock_timeout_ms,
}
}
///|
pub struct FileReport {
migration : Migration
report : Report
}
///|
/// Input order and duplicate path labels are retained. Each file has its own run.
pub struct BatchReport {
files : Array[FileReport]
initial_lock_timeout_ms : Int32?
}
///|
/// Review every input even if earlier inputs are incomplete or have findings.
/// An optional initial_lock_timeout_ms starts every independent review with the
/// same caller-assured session value; it never flows from an earlier input.
pub fn review_migrations(
inputs : ArrayView[Migration],
initial_lock_timeout_ms? : Int32,
) -> BatchReport {
let files = []
for input in inputs {
files.push({
migration: input,
report: review_sql(
input.source,
execution=input.execution,
initial_transaction=input.initial_transaction,
require_lock_timeout=input.require_lock_timeout,
max_lock_timeout_ms?=input.max_lock_timeout_ms,
initial_lock_timeout_ms?,
),
})
}
{ files, initial_lock_timeout_ms, }
}
///|
/// Highest per-file exit code: incomplete (2) wins over findings (1) and clean (0).
pub fn BatchReport::exit_code(self : BatchReport) -> Int32 {
if self.initial_lock_timeout_ms is Some(value) && value < 0 {
return 2
}
let mut code = 0
for file in self.files {
if file.report.exit_code() > code {
code = file.report.exit_code()
}
}
code
}
///|
pub fn BatchReport::to_json(self : BatchReport) -> Json {
let files : Array[Json] = []
for file in self.files {
files.push({
"path": file.migration.path,
"context": migration_context(
file.migration,
initial_lock_timeout_ms?=self.initial_lock_timeout_ms,
),
"report": file.report.to_json(),
})
}
let data : Json = {
"format_version": 1,
"exit_code": self.exit_code(),
"files": Json(files),
}
if self.initial_lock_timeout_ms is Some(value) && data is Object(fields) {
fields["initial_lock_timeout_ms"] = Json(value)
if value < 0 {
fields["configuration_error"] = Json(
"initial_lock_timeout_ms must be nonnegative",
)
}
}
data
}
///|
pub fn BatchReport::to_text(self : BatchReport) -> String {
let out = StringBuilder()
out.write_string(
"MigrationLens batch: \{self.files.length()} files; exit=\{self.exit_code()}\n",
)
if self.initial_lock_timeout_ms is Some(value) && value < 0 {
out.write_string("initial_lock_timeout_ms must be nonnegative\n")
}
for file in self.files {
out.write_string("== \{file.migration.path} ==\n")
out.write_string(file.report.to_text())
}
out.to_string()
}
///|
/// Each input is a separate SARIF run, retaining its source and completeness.
/// One invalid file report/path rejects the export; no partial log is returned.
pub fn BatchReport::to_sarif(self : BatchReport) -> Result[Json, String] {
if self.initial_lock_timeout_ms is Some(value) && value < 0 {
return Err("initial_lock_timeout_ms must be nonnegative")
}
let runs : Array[Json] = []
for i, file in self.files {
match file.report.to_sarif(file.migration.path) {
Err(e) => return Err("File \{i} (\{file.migration.path}): \{e}")
Ok(log) =>
match log {
{ "runs": [run], .. } => {
guard run is Object(fields) &&
fields.get("properties") is Some(Object(properties)) else {
return Err("Unexpected SARIF run properties")
}
properties["migrationLensContext"] = migration_context(
file.migration,
initial_lock_timeout_ms?=self.initial_lock_timeout_ms,
)
runs.push(run)
}
_ => return Err("Unexpected single-report SARIF envelope")
}
}
}
let data : Json = {
"$schema": "https://docs.oasis-open.org/sarif/sarif/v2.1.0/errata01/os/schemas/sarif-schema-2.1.0.json",
"version": "2.1.0",
"runs": Json(runs),
}
if self.initial_lock_timeout_ms is Some(value) && data is Object(fields) {
fields["properties"] = { "migrationLensInitialLockTimeoutMs": value }
}
Ok(data)
}
///|
fn migration_context(
input : Migration,
initial_lock_timeout_ms? : Int32,
) -> Json {
let context : Json = {
"path": input.path,
"execution": Json(input.execution),
"initial_transaction": input.initial_transaction,
"require_lock_timeout": input.require_lock_timeout,
"max_lock_timeout_ms": Json(input.max_lock_timeout_ms),
}
if initial_lock_timeout_ms is Some(value) && context is Object(fields) {
fields["initial_lock_timeout_ms"] = Json(value)
}
context
}