///|
/// One migration that has been generated, in the order it must run.
pub(all) struct JournalEntry {
idx : Int
/// File stem shared by the migration and nothing else, e.g. `0001_add_plan`.
tag : String
/// Milliseconds since the epoch, as reported by the runtime that generated
/// it. Ordering comes from `idx`; this is here for humans reading the file.
when_ : Int64
/// Whether the `.sql` file is split with statement breakpoints.
breakpoints : Bool
} derive(Debug, Eq)
///|
/// The index of the migrations directory.
///
/// The directory listing cannot be the index: file names sort lexically, tell
/// you nothing about which dialect they were written for, and say nothing when
/// one is deleted by hand.
pub(all) struct Journal {
version : String
dialect : @aya.Dialect
entries : Array[JournalEntry]
} derive(Debug, Eq)
///|
pub const JOURNAL_VERSION = "1"
///|
pub fn Journal::new(dialect : @aya.Dialect) -> Journal {
{ version: JOURNAL_VERSION, dialect, entries: [] }
}
///|
/// The last migration generated, if any.
pub fn Journal::last(self : Journal) -> JournalEntry? {
if self.entries.is_empty() {
None
} else {
Some(self.entries[self.entries.length() - 1])
}
}
///|
/// The index the next migration will take.
pub fn Journal::next_index(self : Journal) -> Int {
match self.last() {
None => 0
Some(e) => e.idx + 1
}
}
///|
pub fn Journal::to_json_string(self : Journal) -> String {
let buf = StringBuilder()
buf.write_string("{\n")
buf.write_string(" \"version\": \{quote(self.version)},\n")
buf.write_string(
" \"dialect\": \{quote(@ddl.dialect_name(self.dialect))},\n",
)
buf.write_string(" \"entries\": [")
for i, e in self.entries {
buf.write_string(if i == 0 { "\n" } else { ",\n" })
buf.write_string(
" { \"idx\": \{e.idx}, \"tag\": \{quote(e.tag)}, \"when\": \{e.when_}, \"breakpoints\": \{e.breakpoints} }",
)
}
buf.write_string(
if self.entries.is_empty() {
"]\n}\n"
} else {
"\n ]\n}\n"
},
)
buf.to_string()
}
///|
pub fn Journal::from_json_string(text : String) -> Journal raise KitError {
let json = @json.parse(text) catch {
e => raise BadJournal(reason="not valid JSON: \{e}")
}
guard json is Object(obj) else {
raise BadJournal(reason="expected a JSON object")
}
let version = match obj.get("version") {
Some(String(s)) => s
_ => raise BadJournal(reason="missing `version`")
}
guard version == JOURNAL_VERSION else {
raise BadJournal(
reason="journal format version \{version}, this aya writes \{JOURNAL_VERSION}",
)
}
let dialect = match obj.get("dialect") {
Some(String(s)) =>
@ddl.dialect_of_name(s) catch {
e => raise BadJournal(reason=e.to_string())
}
_ => raise BadJournal(reason="missing `dialect`")
}
let entries = []
guard obj.get("entries") is Some(Array(items)) else {
raise BadJournal(reason="missing `entries`")
}
for item in items {
guard item is Object(e) else {
raise BadJournal(reason="every entry must be an object")
}
let idx = match e.get("idx") {
Some(Number(n, ..)) => n.to_int()
_ => raise BadJournal(reason="an entry is missing `idx`")
}
let tag = match e.get("tag") {
Some(String(s)) => s
_ => raise BadJournal(reason="entry \{idx} is missing `tag`")
}
entries.push({
idx,
tag,
when_: match e.get("when") {
Some(Number(n, ..)) => n.to_int64()
_ => 0
},
breakpoints: !(e.get("breakpoints") is Some(False)),
})
}
entries.sort_by_key(e => e.idx)
{ version, dialect, entries }
}