///|
/// High-level database facts used by the CLI's text and JSON renderers.
pub(all) struct DatabaseInspection {
header : DatabaseHeader
physical_page_count : UInt64
file_length : Int
page_one : BtreePageHeader
page_one_layout : PageLayout
schema_tree : BtreeTraversal
freelist : FreelistReport
diagnostics : Array[Diagnostic]
}
///|
pub fn inspect_database(data : Bytes) -> DatabaseInspection raise ParseError {
let database = DatabaseImage::open(data)
let page_one = parse_btree_page_header(database, 1UL)
let page_one_layout = analyze_page_layout(database, page_one)
let schema_tree = traverse_btree(database, 1UL)
let freelist = analyze_freelist(database)
let diagnostics = validate_database_header(database.header, data.length())
append_diagnostics(diagnostics, validate_page_layout(database, page_one))
append_diagnostics(diagnostics, schema_tree.diagnostics)
append_diagnostics(diagnostics, freelist.diagnostics)
{
header: database.header,
physical_page_count: database.page_count,
file_length: data.length(),
page_one,
page_one_layout,
schema_tree,
freelist,
diagnostics,
}
}
///|
fn optional_int_text(value : Int?) -> String {
match value {
Some(number) => number.to_string()
None => "-"
}
}
///|
fn diagnostic_text(diagnostic : Diagnostic) -> String {
let mut location = ""
if diagnostic.page_number is Some(page) {
location = location + " page=" + page.to_string()
}
if diagnostic.offset is Some(offset) {
location = location + " offset=" + offset.to_string()
}
"[" +
diagnostic.severity.label() +
"] " +
diagnostic.code +
location +
": " +
diagnostic.message
}
///|
fn diagnostics_text(diagnostics : Array[Diagnostic]) -> String {
if diagnostics.length() == 0 {
return "Diagnostics: none"
}
let lines : Array[String] = ["Diagnostics:"]
for diagnostic in diagnostics {
lines.push(" " + diagnostic_text(diagnostic))
}
lines.join("\n")
}
///|
fn json_string(value : String) -> String {
value.escape(quote=true)
}
///|
fn optional_int_json(value : Int?) -> String {
match value {
Some(number) => number.to_string()
None => "null"
}
}
///|
fn optional_page_json(value : UInt64?) -> String {
match value {
Some(number) => number.to_string()
None => "null"
}
}
///|
fn diagnostic_json(diagnostic : Diagnostic) -> String {
"{" +
"\"severity\":" +
json_string(diagnostic.severity.label()) +
",\"code\":" +
json_string(diagnostic.code) +
",\"message\":" +
json_string(diagnostic.message) +
",\"offset\":" +
optional_int_json(diagnostic.offset) +
",\"page_number\":" +
optional_page_json(diagnostic.page_number) +
"}"
}
///|
fn diagnostics_json(diagnostics : Array[Diagnostic]) -> String {
let values : Array[String] = []
for diagnostic in diagnostics {
values.push(diagnostic_json(diagnostic))
}
"[" + values.join(",") + "]"
}
///|
pub fn render_database_text(inspection : DatabaseInspection) -> String {
let header = inspection.header
let tree = summarize_btree(inspection.schema_tree)
let lines : Array[String] = [
"PageLens database inspection",
"File length: " + inspection.file_length.to_string() + " bytes",
"Physical pages: " + inspection.physical_page_count.to_string(),
"Page size: " + header.page_size.to_string(),
"Usable page size: " + header.usable_page_size().to_string(),
"Write version: " + header.write_version.to_string(),
"Read version: " + header.read_version.to_string(),
"Reserved bytes: " + header.reserved_bytes.to_string(),
"Payload fractions: " +
header.max_embedded_payload_fraction.to_string() +
"/" +
header.min_embedded_payload_fraction.to_string() +
"/" +
header.leaf_payload_fraction.to_string(),
"File change counter: " + header.file_change_counter.to_string(),
"Header database pages: " + header.database_size_pages.to_string(),
"First freelist trunk: " + header.first_freelist_trunk_page.to_string(),
"Total freelist pages: " + header.total_freelist_pages.to_string(),
"Schema cookie: " + header.schema_cookie.to_string(),
"Schema format: " + header.schema_format.to_string(),
"Default cache size: " + header.default_page_cache_size.to_string(),
"Largest root B-tree page: " + header.largest_root_btree_page.to_string(),
"Text encoding: " + header.text_encoding.label(),
"User version: " + header.user_version.to_string(),
"Incremental vacuum: " + header.incremental_vacuum.to_string(),
"Application ID: " + header.application_id.to_string(),
"Version valid for: " + header.version_valid_for.to_string(),
"SQLite version number: " + header.sqlite_version_number.to_string(),
"",
"Page 1 type: " + inspection.page_one.page_type.label(),
"Page 1 cells: " + inspection.page_one.cell_count.to_string(),
"Page 1 free bytes: " +
inspection.page_one_layout.total_free_bytes().to_string(),
"Schema B-tree pages: " + inspection.schema_tree.visits.length().to_string(),
"Schema B-tree interior pages: " + tree.interior_pages.to_string(),
"Schema B-tree leaf pages: " + tree.leaf_pages.to_string(),
"Schema B-tree cells: " + tree.total_cells.to_string(),
"Schema B-tree maximum depth: " + tree.maximum_depth.to_string(),
"Observed freelist pages: " +
inspection.freelist.observed_page_count().to_string(),
"",
diagnostics_text(inspection.diagnostics),
]
lines.join("\n")
}
///|
pub fn render_database_json(inspection : DatabaseInspection) -> String {
let header = inspection.header
let tree = summarize_btree(inspection.schema_tree)
"{" +
"\"kind\":\"database\"," +
"\"file_length\":" +
inspection.file_length.to_string() +
",\"physical_page_count\":" +
inspection.physical_page_count.to_string() +
",\"header\":{" +
"\"page_size\":" +
header.page_size.to_string() +
",\"usable_page_size\":" +
header.usable_page_size().to_string() +
",\"file_format_write_version\":" +
header.write_version.to_string() +
",\"file_format_read_version\":" +
header.read_version.to_string() +
",\"reserved_bytes\":" +
header.reserved_bytes.to_string() +
",\"maximum_embedded_payload_fraction\":" +
header.max_embedded_payload_fraction.to_string() +
",\"minimum_embedded_payload_fraction\":" +
header.min_embedded_payload_fraction.to_string() +
",\"leaf_payload_fraction\":" +
header.leaf_payload_fraction.to_string() +
",\"file_change_counter\":" +
header.file_change_counter.to_string() +
",\"database_size_pages\":" +
header.database_size_pages.to_string() +
",\"first_freelist_trunk_page\":" +
header.first_freelist_trunk_page.to_string() +
",\"total_freelist_pages\":" +
header.total_freelist_pages.to_string() +
",\"schema_cookie\":" +
header.schema_cookie.to_string() +
",\"schema_format\":" +
header.schema_format.to_string() +
",\"default_page_cache_size\":" +
header.default_page_cache_size.to_string() +
",\"largest_root_btree_page\":" +
header.largest_root_btree_page.to_string() +
",\"text_encoding\":" +
json_string(header.text_encoding.label()) +
",\"user_version\":" +
header.user_version.to_string() +
",\"incremental_vacuum\":" +
header.incremental_vacuum.to_string() +
",\"application_id\":" +
header.application_id.to_string() +
",\"version_valid_for\":" +
header.version_valid_for.to_string() +
",\"sqlite_version_number\":" +
header.sqlite_version_number.to_string() +
"},\"page_one\":{" +
"\"type\":" +
json_string(inspection.page_one.page_type.label()) +
",\"cell_count\":" +
inspection.page_one.cell_count.to_string() +
",\"free_bytes\":" +
inspection.page_one_layout.total_free_bytes().to_string() +
"},\"schema_btree_pages\":" +
inspection.schema_tree.visits.length().to_string() +
",\"schema_btree\":{\"interior_pages\":" +
tree.interior_pages.to_string() +
",\"leaf_pages\":" +
tree.leaf_pages.to_string() +
",\"total_cells\":" +
tree.total_cells.to_string() +
",\"maximum_depth\":" +
tree.maximum_depth.to_string() +
"}" +
",\"observed_freelist_pages\":" +
inspection.freelist.observed_page_count().to_string() +
",\"diagnostics\":" +
diagnostics_json(inspection.diagnostics) +
"}"
}
///|
pub fn render_wal_text(wal : WalFile) -> String {
let histories = wal.page_histories()
let lines : Array[String] = [
"PageLens WAL inspection",
"Magic: " + wal.header.magic.to_string(),
"Format version: " + wal.header.format_version.to_string(),
"Page size: " + wal.header.page_size.to_string(),
"Checkpoint sequence: " + wal.header.checkpoint_sequence.to_string(),
"Header checksum valid: " + wal.header.checksum_valid.to_string(),
"Frames: " + wal.frames.length().to_string(),
"Valid frames: " + wal.valid_frame_count().to_string(),
"Transactions: " + wal.transactions.length().to_string(),
"Committed transactions: " + wal.committed_transaction_count().to_string(),
"Distinct modified pages: " + histories.length().to_string(),
"Uncommitted valid frames: " + wal.uncommitted_frame_count().to_string(),
"Last commit frame: " + optional_int_text(wal.last_commit_frame),
"Trailing bytes: " + wal.trailing_bytes.to_string(),
"",
"Frames:",
]
for frame in wal.frames {
lines.push(
" #" +
frame.index.to_string() +
" page=" +
frame.page_number.to_string() +
" commit_size=" +
frame.database_size_after_commit.to_string() +
" salt=" +
frame.salt_valid.to_string() +
" checksum=" +
frame.checksum_valid.to_string(),
)
}
lines.push("")
lines.push(diagnostics_text(wal.diagnostics))
lines.join("\n")
}
///|
pub fn render_wal_json(wal : WalFile) -> String {
let frames : Array[String] = []
let histories = wal.page_histories()
for frame in wal.frames {
frames.push(
"{\"index\":" +
frame.index.to_string() +
",\"page_number\":" +
frame.page_number.to_string() +
",\"database_size_after_commit\":" +
frame.database_size_after_commit.to_string() +
",\"salt_valid\":" +
frame.salt_valid.to_string() +
",\"checksum_valid\":" +
frame.checksum_valid.to_string() +
"}",
)
}
"{" +
"\"kind\":\"wal\"," +
"\"magic\":" +
wal.header.magic.to_string() +
",\"format_version\":" +
wal.header.format_version.to_string() +
",\"page_size\":" +
wal.header.page_size.to_string() +
",\"checkpoint_sequence\":" +
wal.header.checkpoint_sequence.to_string() +
",\"header_checksum_valid\":" +
wal.header.checksum_valid.to_string() +
",\"last_commit_frame\":" +
optional_int_json(wal.last_commit_frame) +
",\"committed_transactions\":" +
wal.committed_transaction_count().to_string() +
",\"distinct_modified_pages\":" +
histories.length().to_string() +
",\"uncommitted_valid_frames\":" +
wal.uncommitted_frame_count().to_string() +
",\"trailing_bytes\":" +
wal.trailing_bytes.to_string() +
",\"frames\":[" +
frames.join(",") +
"],\"diagnostics\":" +
diagnostics_json(wal.diagnostics) +
"}"
}
///|
pub fn render_snapshot_text(snapshot : SnapshotView) -> String raise ParseError {
let differences = snapshot.differences()
let statistics = summarize_snapshot(snapshot)
let lines : Array[String] = [
"PageLens committed snapshot",
"Commit frame: " + snapshot.max_frame.to_string(),
"Logical database pages: " + snapshot.logical_page_count.to_string(),
"Changed pages: " + differences.length().to_string(),
"Changed bytes: " + statistics.changed_bytes.to_string(),
"Changed ranges: " + statistics.changed_ranges.to_string(),
"Pages added by WAL: " + statistics.pages_added_by_wal.to_string(),
"",
"Changes:",
]
for difference in differences {
lines.push(
" page=" +
difference.page_number.to_string() +
" frame=" +
difference.frame_index.to_string() +
" changed_bytes=" +
difference.changed_bytes.to_string() +
" first=" +
optional_int_text(difference.first_changed_offset) +
" last=" +
optional_int_text(difference.last_changed_offset),
)
}
lines.join("\n")
}
///|
pub fn render_snapshot_json(snapshot : SnapshotView) -> String raise ParseError {
let changes : Array[String] = []
let statistics = summarize_snapshot(snapshot)
for difference in snapshot.differences() {
changes.push(
"{\"page_number\":" +
difference.page_number.to_string() +
",\"frame_index\":" +
difference.frame_index.to_string() +
",\"changed_bytes\":" +
difference.changed_bytes.to_string() +
",\"first_changed_offset\":" +
optional_int_json(difference.first_changed_offset) +
",\"last_changed_offset\":" +
optional_int_json(difference.last_changed_offset) +
"}",
)
}
"{" +
"\"kind\":\"snapshot\"," +
"\"commit_frame\":" +
snapshot.max_frame.to_string() +
",\"logical_page_count\":" +
snapshot.logical_page_count.to_string() +
",\"changed_pages\":" +
statistics.changed_pages.to_string() +
",\"changed_bytes\":" +
statistics.changed_bytes.to_string() +
",\"changed_ranges\":" +
statistics.changed_ranges.to_string() +
",\"pages_added_by_wal\":" +
statistics.pages_added_by_wal.to_string() +
",\"largest_page_change\":" +
statistics.largest_page_change.to_string() +
",\"changes\":[" +
changes.join(",") +
"]}"
}
///|
pub fn render_integrity_text(report : IntegrityReport) -> String {
let lines : Array[String] = [
"PageLens consistency report",
"Result: " + (if report.is_ok() { "PASS" } else { "FAIL" }),
"Errors: " + report.error_count().to_string(),
"Warnings: " + report.warning_count().to_string(),
"Info: " + report.info_count().to_string(),
"Pages examined: " + report.pages_examined.to_string(),
"B-tree pages: " + report.btree_pages.to_string(),
"B-tree cells: " + report.btree_cells.to_string(),
"Records decoded: " + report.records_decoded.to_string(),
"Overflow pages: " + report.overflow_pages.to_string(),
"Freelist pages: " + report.freelist_pages.to_string(),
"WAL frames: " + report.wal_frames.to_string(),
"",
diagnostics_text(report.diagnostics),
]
lines.join("\n")
}
///|
pub fn render_integrity_json(report : IntegrityReport) -> String {
"{" +
"\"kind\":\"consistency\"," +
"\"ok\":" +
report.is_ok().to_string() +
",\"errors\":" +
report.error_count().to_string() +
",\"warnings\":" +
report.warning_count().to_string() +
",\"info\":" +
report.info_count().to_string() +
",\"pages_examined\":" +
report.pages_examined.to_string() +
",\"btree_pages\":" +
report.btree_pages.to_string() +
",\"btree_cells\":" +
report.btree_cells.to_string() +
",\"records_decoded\":" +
report.records_decoded.to_string() +
",\"overflow_pages\":" +
report.overflow_pages.to_string() +
",\"freelist_pages\":" +
report.freelist_pages.to_string() +
",\"wal_frames\":" +
report.wal_frames.to_string() +
",\"diagnostics\":" +
diagnostics_json(report.diagnostics) +
"}"
}