Artifact 11a7b2788f3377d9c88ccd4f02cc97b42b223c40dbe90d008689924ba85d0887:
- File
internal/fsltype/fsltype.go
— part of check-in
[2c2e5e74b6]
at
2026-10-09 15:35:38
on branch trunk
— Store check-in, link and tag rows as fossil does (#258) (#261)
* fix(manifest): store check-in, link and tag rows as fossil does (#258)
A rebuilt or locally written repository differed from fossil's in three derived values: event and plink times in their last digits, event.omtime left NULL, and a valueless tag stored as '' instead of NULL.
Fossil stores a tag time as SQLite's julianday() bound as a double, but writes event, plink, attachment and forumpost times into SQL text with its own printf("%.17g"), which emits 16 digits in long double arithmetic. TimeToJulian is now julianday() exactly (whole milliseconds over 86400000) and TimeToTextJulian ports fossil's printf conversion, so both match fossil bit for bit. The conversion follows builds where long double is a double (macOS, Windows); fossil itself differs across platforms in the last digit.
Check-in events record omtime, tag writes bind NULL for an empty value, the readers of event and plink times under the ncruces driver rebuild the text form, and the CLI's tag listing reads NULL values.
* test(db): sweep both stored julian forms; scan the text form from driver times (#258)
* test(verify): compare text times bit for bit only where fossil computes them in double (#258) (user: [email protected], size: 4803) [annotate] [blame] [check-ins using]
// Package fsltype defines shared types used across internal packages. // These are re-exported by the root libfossil package. package fsltype import ( "math" "time" ) // FslID is a row-id in the blob table (content-addressed artifacts). type FslID int64 // FslSize represents a blob size; negative values indicate phantom blobs. type FslSize int64 const ( // PhantomSize is the sentinel size for phantom (not-yet-received) blobs. PhantomSize FslSize = -1 // FossilApplicationID is the SQLite application_id for Fossil repositories. FossilApplicationID int32 = 252006673 ) // EventKind is the type discriminator on the event table's newest-first // timeline: one of the single-letter codes fossil assigns per row (see // src/schema.c:303-312). The zero value ("") means "all kinds" — the // canonical default a bare `fossil timeline` uses when no -t/--type is // given (src/timeline.c:timeline_cmd(), guarded by `zType && zType[0]!='a'`). type EventKind string const ( // EventKindCheckin is a check-in ('ci'). EventKindCheckin EventKind = "ci" // EventKindTechnote is a technote / event artifact ('e'). EventKindTechnote EventKind = "e" // EventKindForum is a forum post ('f'). EventKindForum EventKind = "f" // EventKindTag is a tag/control artifact ('g'). EventKindTag EventKind = "g" // EventKindTicket is a ticket change ('t'). EventKindTicket EventKind = "t" // EventKindWiki is a wiki page edit ('w'). EventKindWiki EventKind = "w" ) // Valid reports whether k is the zero value ("all kinds") or one of the // six recognized event.type codes. func (k EventKind) Valid() bool { switch k { case "", EventKindCheckin, EventKindTechnote, EventKindForum, EventKindTag, EventKindTicket, EventKindWiki: return true default: return false } } const julianEpoch = 2440587.5 // TimeToJulian converts a time.Time, at millisecond resolution, to SQLite's // julianday(): the value fossil stores in tagxref.mtime (see // JulianDayFromMillis). func TimeToJulian(t time.Time) float64 { return JulianDayFromMillis(t.UTC().UnixMilli()) } // TimeToTextJulian converts a time.Time, at millisecond resolution, to the // Julian day fossil stores in event, plink, attachment and forumpost times, // which it writes as SQL text (see TextJulianDayFromMillis). func TimeToTextJulian(t time.Time) float64 { return TextJulianDayFromMillis(t.UTC().UnixMilli()) } // JulianToTime converts a Fossil Julian day number to time.Time. // // Rounds to the nearest millisecond rather than truncating: a julian value // produced by TimeToJulian for an exact millisecond carries float64 error // on the order of 1e-10 days, which a plain int64() truncation can turn // into a full millisecond of drift when that error lands the value just // below the true integer. Rounding makes the pair a true round trip // (TimeToJulian(JulianToTime(m)) == m) for any m that itself came from // TimeToJulian — the property the Timeline enumeration's composite // (mtime, rid) pagination cursor depends on. func JulianToTime(j float64) time.Time { millis := int64(math.Round((j - julianEpoch) * 86400.0 * 1000.0)) return time.UnixMilli(millis).UTC() } // Cursor is an opaque pagination token for a Timeline enumeration. Obtain // one from a returned TimelineEntry's Cursor field and hand it back as the // next page's cursor to resume immediately after that entry, in the same // (mtime DESC, rid DESC) order Timeline itself produces. The zero Cursor // (Valid() == false) means "start from the newest event". // // The julian/rid pair is deliberately unexported. A cursor built from // parts — e.g. re-deriving mtime by round-tripping a TimelineEntry.Time // back through TimeToJulian — is not guaranteed to equal the row's actual // stored mtime bit-for-bit, which silently reintroduces skipped or // duplicated rows at a page boundary (the exact defect this type exists // to make impossible to construct). Only NewCursor, called by Timeline's // own row-scanning code with the float64 read directly off the row, may // produce a valid one. type Cursor struct { julian float64 rid FslID valid bool } // NewCursor builds a Cursor from a row's exact scanned mtime and rid. // Callers outside this package's Timeline implementation should not need // this — obtain a Cursor from a TimelineEntry instead. func NewCursor(julian float64, rid FslID) Cursor { return Cursor{julian: julian, rid: rid, valid: true} } // Julian returns the cursor's julian-day mtime component. func (c Cursor) Julian() float64 { return c.julian } // RID returns the cursor's rid component. func (c Cursor) RID() FslID { return c.rid } // Valid reports whether c was produced by NewCursor (as opposed to being // a zero-value Cursor{}, which means "no cursor"). func (c Cursor) Valid() bool { return c.valid }