Help us shape the future of Go! Take the 2026 Go Developer Survey

migration

package
v0.26.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 3, 2026 License: MIT Imports: 16 Imported by: 0

Documentation

Overview

Package migration defines Ridu's database-agnostic, reviewable migration artifact vocabulary. Store adapters translate schema manifests into physical steps while Ridu retains ownership of content-aware semantic steps.

Index

Constants

View Source
const (
	RequiredScopeLocalized   = "localized"
	RequiredScopeUnlocalized = "unlocalized"
)

Required-value audit scopes of a block definition's field.

View Source
const ArtifactVersion uint32 = 1

ArtifactVersion is the migration artifact format emitted by this build.

View Source
const PhysicalContractVersion uint32 = 1

PhysicalContractVersion identifies the meaning of intermediate physical digests.

View Source
const RunnerContractVersion uint32 = 1

RunnerContractVersion is the execution contract implemented by this build.

Variables

View Source
var ErrReplacementNeedsConfirmation = errors.New("this database is exactly at its recorded " +
	"migration head, so an application binary built from the old history may be serving it; " +
	"after the replacement those binaries fail readiness until they are rebuilt from the new " +
	"history; stop every application process and writer first")

ErrReplacementNeedsConfirmation reports a baseline replacement that would rewrite the history of a database exactly at its recorded head. Repeat the replacement with AllowProduction once its application processes are stopped.

Functions

func DataTransformChecksum

func DataTransformChecksum(source []byte) string

DataTransformChecksum returns the lowercase SHA-256 identity of reviewed callback source bytes. Applications normally compute and commit this value when authoring the migration callback.

func DigestArtifactHistory

func DigestArtifactHistory(identities []ArtifactIdentity, headDigest string, manifest schema.Manifest) (string, error)

DigestArtifactHistory binds one complete ordered migration history to its recorded head manifest and the executable's storage schema. Settings outside the storage schema may differ from the head without changing this identity. Names must be unique and strictly increasing because committed filenames define execution order.

func DigestManifest

func DigestManifest(manifest schema.Manifest) (string, error)

DigestManifest returns the SHA-256 digest of a canonical manifest: its schema.Manifest.Bytes encoding. Readiness checks and every migration step digest whole manifests, so the indented form is streamed into the hash from the compact encoding instead of being materialized.

func MarshalStepPayload

func MarshalStepPayload(value any) (json.RawMessage, error)

MarshalStepPayload emits one compact, deterministic payload object.

func PhasePhysicalDigest

func PhasePhysicalDigest(before string, mode PhaseMode, steps []Step) (string, error)

PhasePhysicalDigest advances the reviewed physical execution contract by one complete phase. The database is additionally inspected against the embedded manifests at the artifact boundaries.

func PhysicalDigestSeed

func PhysicalDigestSeed(fromManifestDigest string) string

PhysicalDigestSeed returns the canonical starting identity for the artifact's reviewed physical execution contract.

func UnmanagedSchemaError added in v0.8.0

func UnmanagedSchemaError(engine string, objects []string) error

UnmanagedSchemaError reports database objects found where a migration runner expected an empty database. `ridu dev` leaves exactly this behind: it synchronizes a schema directly and never records migration history.

func VersionsEnabled added in v0.19.0

func VersionsEnabled(before, after schema.Snapshot, renames map[schema.StableID]schema.StableID) []schema.Collection

VersionsEnabled returns the resources of after, collections then globals, that keep versions while the same resource in before does not. renames maps a renamed collection's before stable ID to its after one. Any other resource new in after starts versioned with nothing stored.

Types

type Artifact

type Artifact struct {
	// Version identifies this Ridu artifact JSON format.
	Version uint32 `json:"version"`
	// Name is the lowercase author-supplied migration name.
	Name string `json:"name"`
	// Planner records the exact planner and version used at creation.
	Planner Planner `json:"planner"`
	// MinimumRunnerContract is the lowest execution contract that may run
	// every phase and executor in this artifact.
	MinimumRunnerContract uint32 `json:"minimumRunnerContract,omitempty"`
	// PreviousArtifactDigest binds this artifact to the exact preceding artifact,
	// including data-only transitions whose manifest digest does not change.
	// It is empty only for the initial artifact.
	PreviousArtifactDigest string `json:"previousArtifactDigest"`
	// FromDigest and ToDigest establish immutable manifest lineage.
	FromDigest string `json:"fromDigest"`
	ToDigest   string `json:"toDigest"`
	// Before is absent only for an initial migration from an empty schema.
	Before *schema.Snapshot `json:"before,omitempty"`
	// After is the complete desired manifest after every step succeeds.
	After schema.Snapshot `json:"after"`
	// Phases are the resumable execution plan.
	Phases []Phase `json:"phases"`
	// Risks retains machine-readable planner and linter findings for review.
	Risks []Risk `json:"risks"`
}

Artifact is the immutable source of truth for one migration. It embeds both manifest states so history does not depend on a mutable latest-snapshot file.

func DecodeArtifact

func DecodeArtifact(encoded []byte) (Artifact, error)

DecodeArtifact strictly decodes one frozen Ridu artifact format. Future versions and unknown fields are rejected before a runner can inspect a database.

func NewArtifact

func NewArtifact(name string, planner Planner, before *schema.Manifest, after schema.Manifest) (Artifact, error)

NewArtifact creates an unpublished planner result with exact manifest lineage. Callers add ordered steps and risks before structural validation. The history publisher binds a non-initial result to its exact predecessor before it can be serialized or digested.

func (Artifact) AfterManifest

func (artifact Artifact) AfterManifest() (schema.Manifest, error)

AfterManifest returns the validated immutable after snapshot.

func (Artifact) BeforeManifest

func (artifact Artifact) BeforeManifest() (schema.Manifest, error)

BeforeManifest returns the validated immutable before snapshot. Initial artifacts return an error because they intentionally have no parent state.

func (Artifact) Digest

func (artifact Artifact) Digest() (string, error)

Digest returns the SHA-256 digest of the canonical artifact JSON.

func (Artifact) MarshalJSON

func (artifact Artifact) MarshalJSON() ([]byte, error)

MarshalJSON emits the frozen artifact wire contract.

func (*Artifact) UnmarshalJSON

func (artifact *Artifact) UnmarshalJSON(encoded []byte) error

UnmarshalJSON uses the same fail-closed versioned decoder as artifact files.

func (Artifact) Validate

func (artifact Artifact) Validate() error

Validate checks the planner-owned structure and manifest lineage of an artifact plan. Publication binds a non-initial plan to its exact predecessor; serialization and digesting reject the plan until that binding exists.

type ArtifactIdentity

type ArtifactIdentity struct {
	Name   string `json:"name"`
	Digest string `json:"digest"`
}

ArtifactIdentity is one committed migration file's immutable runtime identity. Name is the ordered filename recorded in an adapter's ledger; Digest authenticates the canonical artifact contents.

type AssertSchemaPayload

type AssertSchemaPayload struct{}

AssertSchemaPayload is deliberately empty. The expected schema is the artifact's immutable after manifest.

type AuditRequiredValuesPayload added in v0.15.0

type AuditRequiredValuesPayload struct {
	Fields []RequiredFieldAddress `json:"fields"`
}

AuditRequiredValuesPayload lists the fields whose stored values the audit checks, in planner order. Each field is newly required by the artifact.

type BackfillReferencesPayload

type BackfillReferencesPayload struct {
	BatchSize uint32 `json:"batchSize"`
}

BackfillReferencesPayload configures the one supported keyset batch executor. BatchSize is an artifact property, not a runner-side guess.

type BaselineReplacementOptions added in v0.12.0

type BaselineReplacementOptions struct {
	PreviousDirectory string
	// AllowProduction permits rewriting the history of a database that is
	// exactly at its recorded head, where binaries built from that history may
	// still pass readiness. Stop every application process and writer first.
	AllowProduction bool
}

BaselineReplacementOptions controls an explicit rewrite of recorded history. PreviousDirectory must contain the immutable artifacts the database records; when empty, the replacement directory is also used as the previous history.

type ConcurrentIndexAction

type ConcurrentIndexAction string

ConcurrentIndexAction selects typed concurrent index creation or removal.

const (
	ConcurrentIndexCreate ConcurrentIndexAction = "create"
	ConcurrentIndexDrop   ConcurrentIndexAction = "drop"
)

type ConcurrentIndexPayload

type ConcurrentIndexPayload struct {
	Action    ConcurrentIndexAction `json:"action"`
	Name      string                `json:"name"`
	Table     string                `json:"table,omitempty"`
	Unique    bool                  `json:"unique,omitempty"`
	Method    string                `json:"method,omitempty"`
	Parts     []string              `json:"parts,omitempty"`
	Predicate string                `json:"predicate,omitempty"`
}

ConcurrentIndexPayload is the closed concurrent-index vocabulary. Parts are PostgreSQL expressions emitted by the frozen planner; the runner quotes identifiers and never accepts a free-form no-transaction statement.

type DataTransaction

type DataTransaction interface {
	// Collection returns the collection with this slug as the running
	// migration leaves it, or as it was before when the migration removes it.
	// Every request must use a shape from the migration itself, so a
	// transform keeps replaying after later migrations change the collection.
	Collection(slug string) (schema.Collection, error)
	// Global returns the global with this slug, resolved like Collection.
	Global(slug string) (schema.Collection, error)
	Create(context.Context, store.CreateRequest) (store.Document, error)
	Find(context.Context, store.Request) (store.Document, error)
	List(context.Context, store.Request) (store.Page, error)
	Update(context.Context, UpdateRequest) (store.Document, error)
	Trash(context.Context, store.Request) (store.Document, error)
	Restore(context.Context, store.Request) (store.Document, error)
	Delete(context.Context, store.Request) (store.Document, error)
}

DataTransaction is the transaction-bound semantic surface available to a migration callback. It intentionally omits Commit, Rollback, and generic SQL so the adapter retains ownership of the enclosing schema/data transaction. Update locks and reads the stored document itself; a callback never supplies it. Delete also removes the framework state the document owns, such as its versions, sessions, and preferences.

type DataTransform

DataTransform binds executable up/down behavior to the immutable descriptor stored in an artifact. Both directions are required for lifecycle parity.

func (DataTransform) Validate

func (transform DataTransform) Validate() error

Validate checks the immutable and executable portions of a registration.

type DataTransformCallback

type DataTransformCallback func(context.Context, DataTransaction) error

DataTransformCallback performs one direction of a reviewed migration.

type DataTransformDescriptor

type DataTransformDescriptor struct {
	Name     string `json:"name"`
	Checksum string `json:"checksum"`
}

DataTransformDescriptor is the immutable artifact identity of one compiled migration callback. Checksum is author-supplied SHA-256 source identity; changing callback behavior requires a new checksum before artifact creation.

func (DataTransformDescriptor) Validate

func (descriptor DataTransformDescriptor) Validate() error

Validate checks one artifact-safe callback descriptor.

type DataTransformPayload

type DataTransformPayload struct {
	Transform DataTransformDescriptor `json:"transform"`
}

DataTransformPayload identifies one application-compiled callback without serializing executable code into the immutable artifact.

type EnableVersionsPayload added in v0.19.0

type EnableVersionsPayload struct {
	ResourceID schema.StableID   `json:"resourceId"`
	Existing   ExistingDocuments `json:"existing"`
}

EnableVersionsPayload names one resource that starts keeping versions and what becomes of the documents it already stores.

type ExistingDocuments added in v0.19.0

type ExistingDocuments string

ExistingDocuments is what becomes of the documents a collection or global already stores when it starts keeping versions.

const (
	// ExistingPublished publishes each document with a matching draft, so every
	// reader keeps seeing what they saw before.
	ExistingPublished ExistingDocuments = "published"
	// ExistingDraft keeps each document as an unpublished draft, which no
	// public reader sees until it is published. It needs drafts.
	ExistingDraft ExistingDocuments = "draft"
	// ExistingRequireEmpty stops the migration while the resource stores a
	// document, trashed documents included.
	ExistingRequireEmpty ExistingDocuments = "require-empty"
)

func ParseExistingDocuments added in v0.19.0

func ParseExistingDocuments(value string) (ExistingDocuments, error)

ParseExistingDocuments reads one ExistingDocuments choice.

type FieldRename

type FieldRename struct {
	// Before is the canonical field path in the before manifest.
	Before string `json:"before"`
	// After is the canonical field path in the after manifest.
	After string `json:"after"`
}

FieldRename preserves one field address across an authored rename.

type MongoDBCreateIndexPayload

type MongoDBCreateIndexPayload struct {
	Collection string `json:"collection"`
	Index      string `json:"index"`
}

MongoDBCreateIndexPayload identifies one planner-owned physical index without serializing BSON, commands, or a portable schema language into the artifact.

type MongoDBDropIndexPayload

type MongoDBDropIndexPayload struct {
	CollectionID schema.StableID `json:"collectionId"`
	Version      bool            `json:"version,omitempty"`
	Published    bool            `json:"published,omitempty"`
	Index        string          `json:"index"`
}

MongoDBDropIndexPayload identifies one adapter-owned index by the resource stable identity and deterministic index name.

type MongoDBDropResourcesPayload

type MongoDBDropResourcesPayload struct {
	ResourceIDs []schema.StableID `json:"resourceIds"`
}

MongoDBDropResourcesPayload binds physical namespace removal to the same reviewed resource identities as the typed semantic retirement step.

type MongoDBRenameResourcePayload

type MongoDBRenameResourcePayload struct {
	BeforeID schema.StableID `json:"beforeId"`
	AfterID  schema.StableID `json:"afterId"`
}

MongoDBRenameResourcePayload binds one explicit collection identity rename.

type Operation

type Operation struct {
	// Kind selects the runner behavior.
	Kind StepKind `json:"kind"`
	// Name is a stable, review-friendly operation label.
	Name string `json:"name"`
	// SQL is present only for StepSQL.
	SQL string `json:"sql,omitempty"`
	// Rename is present only for StepRenameContent.
	Rename *Rename `json:"rename,omitempty"`
	// ResourceIDs is present only while planning a StepRetireResources step.
	ResourceIDs []schema.StableID `json:"resourceIds,omitempty"`
	// PurgeVersionOwnerIDs identifies surviving resources whose complete
	// historical version rows could otherwise restore references to a retired
	// resource. It is present only while planning StepRetireResources.
	PurgeVersionOwnerIDs []schema.StableID `json:"purgeVersionOwnerIds,omitempty"`
	// RequiredFields is present only for StepAuditRequiredValues.
	RequiredFields []RequiredFieldAddress `json:"requiredFields,omitempty"`
	// EnableVersions is present only for StepEnableVersions.
	EnableVersions *EnableVersionsPayload `json:"enableVersions,omitempty"`
}

Operation is one ordered physical or semantic planner operation. It is converted into a checkpointed artifact step before an artifact is written.

type Phase

type Phase struct {
	ID                      string    `json:"id"`
	Mode                    PhaseMode `json:"mode"`
	PhysicalContractVersion uint32    `json:"physicalContractVersion"`
	BeforePhysicalDigest    string    `json:"beforePhysicalDigest"`
	AfterPhysicalDigest     string    `json:"afterPhysicalDigest"`
	Steps                   []Step    `json:"steps"`
}

Phase is one resumable execution boundary in an immutable artifact.

type PhaseMode

type PhaseMode string

PhaseMode determines the transaction boundary used to execute one phase.

const (
	// PhaseTransaction executes every phase step and its ledger completion in
	// one database transaction.
	PhaseTransaction PhaseMode = "transaction"
	// PhaseBatch executes one typed keyset executor in bounded transactions.
	PhaseBatch PhaseMode = "batch"
	// PhaseNoTransaction executes one typed physical operation which cannot run
	// inside a database transaction. Each operation is its own resumable phase.
	PhaseNoTransaction PhaseMode = "no_transaction"
)

type Planner

type Planner struct {
	Name    string `json:"name"`
	Version string `json:"version"`
}

Planner records the immutable provenance of an artifact.

type ProjectAction

type ProjectAction string

ProjectAction is one lifecycle operation delegated from the portable CLI to an application's compiled migration driver when artifacts contain callbacks.

const (
	ProjectApply   ProjectAction = "up"
	ProjectDown    ProjectAction = "down"
	ProjectReset   ProjectAction = "reset"
	ProjectRefresh ProjectAction = "refresh"
	ProjectFresh   ProjectAction = "fresh"
	ProjectVerify  ProjectAction = "verify"
)

type ProjectDriver

type ProjectDriver interface {
	// Validate checks the complete executable registration before a project
	// command resolves config or changes database state.
	Validate() error
	DataTransforms() []DataTransformDescriptor
	// RunProjectMigration receives the exact manifest resolved by the process
	// that will execute the request. The driver must validate it against the
	// selected artifact history before opening or changing the database.
	RunProjectMigration(context.Context, ProjectRequest, schema.Manifest) error
}

ProjectDriver keeps application callbacks compiled into the project while letting the selected adapter own the enclosing database transaction.

type ProjectRequest

type ProjectRequest struct {
	Action                 ProjectAction
	DatabasePath           string
	DatabaseURL            string
	Directory              string
	AllowInsecureDatabase  bool
	AllowMaintenance       bool
	AllowUnbounded         bool
	LockWait               time.Duration
	OperationTimeout       time.Duration
	LockTimeout            time.Duration
	StatementTimeout       time.Duration
	BatchTimeout           time.Duration
	IdleTransactionTimeout time.Duration
	StopAfterPhase         string
	StopAfterStep          string
}

ProjectRequest contains only the database selection and structural paths selected by the invoking CLI. It carries no executable code and is private to the local project process. Credential-bearing URLs must be transported to that process outside its command-line arguments.

func (ProjectRequest) Validate

func (request ProjectRequest) Validate() error

Validate rejects incomplete or unknown project migration requests.

type Rename

type Rename struct {
	// CollectionBefore and CollectionAfter are current public slugs. Both are
	// empty for a block field rename.
	CollectionBefore schema.CollectionSlug `json:"collectionBefore,omitempty"`
	CollectionAfter  schema.CollectionSlug `json:"collectionAfter,omitempty"`
	// Block names the block definition whose field is renamed. The definition
	// is shared, so the rename moves the field's content in every stored block
	// of it, in every resource and version snapshot.
	Block string `json:"block,omitempty"`
	// FieldBefore and FieldAfter are canonical field paths, relative to Block
	// for a block field rename. Empty paths mean the rename applies to the
	// collection itself.
	FieldBefore string `json:"fieldBefore,omitempty"`
	FieldAfter  string `json:"fieldAfter,omitempty"`
	// Fields records every nested or top-level field path whose identity changed
	// as part of a collection rename.
	Fields []FieldRename `json:"fields,omitempty"`
}

Rename records explicit, committed content identity intent. Inference is allowed only while proposing this value; runners execute only persisted intent.

type RenamePayload

type RenamePayload struct {
	Rename Rename `json:"rename"`
}

RenamePayload contains one frozen content-identity rewrite.

type RequiredFieldAddress added in v0.15.0

type RequiredFieldAddress struct {
	ResourceID schema.StableID `json:"resourceId,omitempty"`
	Path       string          `json:"path"`
	Scope      string          `json:"scope,omitempty"`
}

RequiredFieldAddress identifies a field of the artifact's after manifest. With a resource stable ID, Path is its canonical path in that resource: a path through a blocks field names the block type, as in layout.hero.heading. Without one, Path starts with a "**" segment and the field belongs to a block definition, checked in every stored block of it: **.hero.heading. A "**" segment finds the block it precedes at any depth beneath the preceding segment, so layout.**.hero.heading reaches every hero block stored in layout and **.card.items.**.hero.heading every hero block stored in a card's items. Scope, when set, limits the first block found to placements beneath a localized ancestor ("localized") or not ("unlocalized").

type RetireResourcesPayload

type RetireResourcesPayload struct {
	ResourceIDs          []schema.StableID `json:"resourceIds"`
	PurgeVersionOwnerIDs []schema.StableID `json:"purgeVersionOwnerIds,omitempty"`
}

RetireResourcesPayload identifies removed collection or global resources by their immutable stable IDs. IDs are strictly sorted so the artifact and its physical contract remain deterministic.

type Risk

type Risk struct {
	// Code is a stable identifier suitable for CI policy.
	Code string `json:"code"`
	// Level classifies whether the finding is informational, risky, or destructive.
	Level RiskLevel `json:"level"`
	// Message explains the concrete impact and required review.
	Message string `json:"message"`
}

Risk is one machine-readable finding produced while planning a migration.

type RiskLevel

type RiskLevel string

RiskLevel classifies the operational impact of a migration step.

const (
	// RiskNotice records operational context that does not require intervention.
	RiskNotice RiskLevel = "notice"
	// RiskWarning records a change that deserves explicit deployment review.
	RiskWarning RiskLevel = "warning"
	// RiskDestructive records a change that can permanently remove stored data.
	RiskDestructive RiskLevel = "destructive"
)

type SQLPayload

type SQLPayload struct {
	SQL string `json:"sql"`
}

SQLPayload contains SQL which is permitted only in a transaction phase.

type Step

type Step struct {
	ID              string          `json:"id"`
	Kind            StepKind        `json:"kind"`
	ExecutorVersion uint32          `json:"executorVersion"`
	Name            string          `json:"name"`
	Payload         json.RawMessage `json:"payload"`
}

Step is one stable, independently ledgered executor invocation.

type StepKind

type StepKind string

StepKind identifies how a migration runner executes one ordered step.

const (
	// StepSQL executes one physical schema statement.
	StepSQL StepKind = "sql"
	// StepRenameContent rewrites schema-addressed content after physical renames.
	StepRenameContent StepKind = "rename_content"
	// StepAssertSchema verifies the resulting physical schema fingerprint.
	StepAssertSchema StepKind = "assert_schema"
	// StepBackfillReferences derives the current-document reference index after
	// its physical table has been created. Version snapshots are not indexed.
	StepBackfillReferences StepKind = "backfill_references"
	// StepRetireResources removes framework-owned shared state for resources
	// that a reviewed destructive migration removes. It is a typed semantic
	// executor and must run before the corresponding physical tables are dropped.
	StepRetireResources StepKind = "retire_resources"
	// StepDataTransform invokes one application-compiled, checksum-bound
	// callback inside the adapter's migration transaction.
	StepDataTransform StepKind = "data_transform"
	// StepAuditRequiredValues fails the migration while a stored document has
	// no value for a field the artifact makes required. It runs after every
	// data transform in its transaction, so a transform can backfill values.
	StepAuditRequiredValues StepKind = "audit_required_values"
	// StepEnableVersions turns the documents a resource already stores into
	// versioned documents when it starts keeping versions, as its payload's
	// ExistingDocuments choice says.
	StepEnableVersions StepKind = "enable_versions"
)
const StepConcurrentIndex StepKind = "concurrent_index"

StepConcurrentIndex executes one structured PostgreSQL concurrent-index operation outside a database transaction.

const StepMongoDBAssertSchema StepKind = "mongodb_assert_schema"

StepMongoDBAssertSchema verifies the complete planner-owned MongoDB catalog outside a database transaction after every physical step has completed.

const StepMongoDBCreateIndex StepKind = "mongodb_create_index"

StepMongoDBCreateIndex creates one planner-owned MongoDB index outside a database transaction. The payload freezes physical identity only; the adapter must reconstruct and exactly match the definition from the embedded manifest and versioned planner before touching a database.

const StepMongoDBDropIndex StepKind = "mongodb_drop_index"

StepMongoDBDropIndex removes one planner-owned MongoDB index outside a database transaction. The adapter reconstructs the exact physical collection and index definition from the embedded manifests; the payload never carries an arbitrary command.

const StepMongoDBDropResources StepKind = "mongodb_drop_resources"

StepMongoDBDropResources removes the content and version namespaces for one exact, sorted set of reviewed retired stable identities. Framework-owned shared state is removed by the preceding transactional retirement step.

const StepMongoDBRebuildHeadReservations StepKind = "mongodb_rebuild_head_reservations"

StepMongoDBRebuildHeadReservations recalculates active cross-head unique reservations before newly declared unique indexes are built.

const StepMongoDBRenameResource StepKind = "mongodb_rename_resource"

StepMongoDBRenameResource moves one collection identity to another outside a database transaction. Only stable manifest identities are persisted; the MongoDB adapter derives the exact content and version namespaces.

type UpdateRequest added in v0.15.0

type UpdateRequest struct {
	store.Request
	Values        store.Values
	ReplaceValues bool
}

UpdateRequest changes one document from a data transform. Values patch the stored document unless ReplaceValues makes them its complete authored state. The transaction applies the patch to the stored row it locks, never to a document the callback read or edited.

Directories

Path Synopsis
Package payload imports a normalized Payload CMS export without coupling Ridu to Payload's database schema.
Package payload imports a normalized Payload CMS export without coupling Ridu to Payload's database schema.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL