feat(catalog): add Sequence, peekNext and SequenceDefinition
This commit is contained in:
1 parent
2665166513
commit
eac99ab3df
2 files changed
+102
No files matched your search
@@ -0,0 +1,76 @@
|
||||
package kestrel.catalog
|
||||
|
||||
import kestrel.KestrelException
|
||||
|
||||
/**
|
||||
* Mirrors Postgres' own internal sequence representation (`pg_sequences.last_value`/`is_called`)
|
||||
* rather than "the next value to hand out" directly, because the two ALTER SEQUENCE behaviors this
|
||||
* engine supports need genuinely different arithmetic: a plain `ALTER SEQUENCE ... INCREMENT BY n`
|
||||
* (no RESTART) must recompute the NEXT value from the LAST one actually returned using the NEW
|
||||
* increment (so incrementing by 100 right after NEXTVAL returned 1 gives 101, not "whatever was
|
||||
* precomputed under the old increment"), while `RESTART [WITH n]` must instead make the very next
|
||||
* NEXTVAL return `n` directly, bypassing that arithmetic entirely for one call - exactly the same
|
||||
* shape as a fresh sequence's first-ever NEXTVAL returning `START WITH` untouched. [called] `false`
|
||||
* captures both of those "return this value as-is, don't advance from it" cases uniformly.
|
||||
*
|
||||
* [value]/[called] live entirely in plain `var`s deliberately NOT covered by [Catalog]'s
|
||||
* snapshot/restore copying of its `sequences` map entries - mirrors Postgres: a ROLLBACK undoes
|
||||
* `CREATE`/`DROP`/`ALTER SEQUENCE` (the catalog entry itself, since [Catalog.restore] reinstalls
|
||||
* whichever [Sequence] *instance* a snapshot captured a reference to) but never undoes a NEXTVAL
|
||||
* advance, even one made by the very transaction being rolled back - the whole point of a sequence
|
||||
* is to hand out values that never repeat, even across an aborted transaction, so two concurrent
|
||||
* transactions can both call NEXTVAL without either blocking on the other's eventual
|
||||
* commit/rollback decision. [Catalog.alterSequence] relies on this same identity trick in reverse:
|
||||
* it always installs a brand new [Sequence] instance (see [altered]) rather than mutating this one
|
||||
* in place, so ALTER SEQUENCE (unlike NEXTVAL) rolls back cleanly too.
|
||||
*/
|
||||
class Sequence(val definition: SequenceDefinition, initialValue: Long = definition.startValue, initialCalled: Boolean = false) {
|
||||
private var value: Long = initialValue
|
||||
private var called: Boolean = initialCalled
|
||||
|
||||
/** What the next call to [next] would return, without consuming it - used by [kestrel.persistence.SqlWriter] to dump a sequence's current position so a reload resumes from there instead of restarting at [SequenceDefinition.startValue]. */
|
||||
fun peekNext(): Long = if (called) advance(value) else value
|
||||
|
||||
fun next(): Long = peekNext().also {
|
||||
value = it
|
||||
called = true
|
||||
}
|
||||
|
||||
/**
|
||||
* The raw `(value, called)` pair this instance currently holds, with no arithmetic and so no way
|
||||
* to throw - unlike [peekNext], which may run [advance] and hit an exhausted-without-CYCLE bound.
|
||||
* Used by [kestrel.store.sql.DiskCatalog] to persist a NEXTVAL advance (or an ALTER SEQUENCE's
|
||||
* resulting position) exactly as it stands, rather than one step further ahead: computing that
|
||||
* next step eagerly - right after a [next] call that legitimately landed exactly on
|
||||
* [kestrel.catalog.SequenceDefinition.maxValue]/[SequenceDefinition.minValue] - would spuriously
|
||||
* raise the "exhausted" exception a whole [next] call early, before anything ever actually asked
|
||||
* for a value beyond the bound.
|
||||
*/
|
||||
fun snapshotPosition(): Pair<Long, Boolean> = value to called
|
||||
|
||||
/**
|
||||
* Builds the [Sequence] `ALTER SEQUENCE` installs in place of this one: [newDefinition] takes
|
||||
* over immediately. With [restartAt] given (Postgres' `RESTART [WITH n]`, resolved to
|
||||
* [SequenceDefinition.startValue] by the caller for a bare `RESTART`), the next NEXTVAL returns
|
||||
* exactly that value; otherwise this sequence's own last-returned value and "has it ever been
|
||||
* called" flag both carry over unchanged, so the next NEXTVAL keeps advancing from where this
|
||||
* one left off - just using [newDefinition]'s (possibly different) increment/bounds.
|
||||
*/
|
||||
fun altered(newDefinition: SequenceDefinition, restartAt: Long? = null): Sequence =
|
||||
if (restartAt != null) Sequence(newDefinition, restartAt, initialCalled = false)
|
||||
else Sequence(newDefinition, value, called)
|
||||
|
||||
private fun advance(previous: Long): Long {
|
||||
val ascending = definition.incrementBy > 0
|
||||
val candidate = runCatching { Math.addExact(previous, definition.incrementBy) }.getOrNull()
|
||||
val exhausted = candidate == null ||
|
||||
(ascending && candidate > definition.maxValue) ||
|
||||
(!ascending && candidate < definition.minValue)
|
||||
if (!exhausted) return candidate!!
|
||||
if (!definition.cycle) {
|
||||
val bound = if (ascending) "maximum value (${definition.maxValue})" else "minimum value (${definition.minValue})"
|
||||
throw KestrelException("sequence '${definition.name}' has reached its $bound and does not CYCLE")
|
||||
}
|
||||
return if (ascending) definition.minValue else definition.maxValue
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package kestrel.catalog
|
||||
|
||||
/**
|
||||
* A sequence's static configuration - separate from [Sequence]'s mutable counter so that a
|
||||
* `CREATE SEQUENCE`/`ALTER SEQUENCE`/`DROP SEQUENCE` (all fully transactional, unlike a NEXTVAL
|
||||
* advance - see [Sequence]'s own doc) can be represented and rolled back like any other catalog
|
||||
* object, by simply installing or removing an immutable value rather than mutating shared state.
|
||||
*
|
||||
* [startValue] is never changed by `ALTER SEQUENCE` (Postgres has no way to do this either) - it is
|
||||
* only ever the value a bare `RESTART` (no `WITH n`) resets the counter back to, recorded once at
|
||||
* `CREATE SEQUENCE` time and carried forward unchanged by every later `ALTER SEQUENCE` (see
|
||||
* [Sequence.altered]).
|
||||
*/
|
||||
data class SequenceDefinition(
|
||||
val name: String,
|
||||
val incrementBy: Long,
|
||||
val minValue: Long,
|
||||
val maxValue: Long,
|
||||
val startValue: Long,
|
||||
val cycle: Boolean
|
||||
) {
|
||||
init {
|
||||
require(incrementBy != 0L) { "sequence '$name' INCREMENT BY cannot be zero" }
|
||||
require(minValue <= maxValue) { "sequence '$name' MINVALUE ($minValue) must not exceed MAXVALUE ($maxValue)" }
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user