Pop — Interfaces#

Interface catalog#

dialect#

  • Package: github.com/gobuffalo/pop/v6
  • File: dialect.go:28
  • Methods:
    // Composed from crudable, fizzable, quotable, plus:
    Name() string
    DefaultDriver() string
    URL() string
    MigrationURL() string
    Details() *ConnectionDetails
    TranslateSQL(string) string
    CreateDB() error
    DropDB() error
    DumpSchema(io.Writer) error
    LoadSchema(io.Reader) error
    Lock(func() error) error
    TruncateAll(*Connection) error
  • Purpose: Master portability seam for all database-specific behavior. A Connection holds one dialect and delegates all CRUD execution, schema management, identifier quoting, and Fizz DSL translation to it.
  • Implementations: postgresql (dialect_postgresql.go), mysql (dialect_mysql.go), mariadb (dialect_mariadb.go), sqlite (dialect_sqlite.go), cockroach (dialect_cockroach.go) — all registered via init() into the private newConnection registry.
  • Design quality: Well-segregated. dialect itself composes three smaller sub-interfaces (crudable, fizzable, quotable), each with a single, coherent responsibility. The composite is reasonably sized (12 methods on dialect proper, plus the 9 inherited) and cleanly follows the Interface Segregation Principle — callers that only need quoting accept quotable, not the entire dialect.

crudable#

  • Package: github.com/gobuffalo/pop/v6
  • File: dialect.go:10
  • Methods:
    SelectOne(*Connection, *Model, Query) error
    SelectMany(*Connection, *Model, Query) error
    Create(*Connection, *Model, columns.Columns) error
    Update(*Connection, *Model, columns.Columns) error
    UpdateQuery(*Connection, *Model, columns.Columns, Query) (int64, error)
    Destroy(*Connection, *Model) error
    Delete(*Connection, *Model, Query) error
  • Purpose: Isolates the database-specific CRUD SQL generation from the generic ORM logic. Connection passes its rich Model and Query values in; each dialect implementation compiles the correct SQL syntax (e.g., RETURNING id for PostgreSQL vs. LastInsertId() for MySQL) and executes it via the store.
  • Implementations: All five dialect structs satisfy this via embedding in dialect.
  • Design quality: Granular and well-focused. Seven methods map directly to the seven fundamental SQL operations. No method is redundant; each dialect must provide real implementations.

fizzable#

  • Package: github.com/gobuffalo/pop/v6
  • File: dialect.go:20
  • Methods:
    FizzTranslator() fizz.Translator
  • Purpose: Returns a gobuffalo/fizz Translator capable of converting Fizz DDL (a DB-agnostic schema DSL) into the native SQL for the dialect. Used exclusively by the Migrator.
  • Implementations: All five dialects.
  • Design quality: Exemplary single-method interface. Completely separable concern; the Migrator only needs fizzable, not the full dialect.

quotable#

  • Package: github.com/gobuffalo/pop/v6
  • File: dialect.go:24
  • Methods:
    Quote(key string) string
  • Purpose: Returns a properly-quoted SQL identifier (e.g., "users" for PostgreSQL, `users` for MySQL). Used by the columns sub-package and query builders wherever identifier quoting is needed.
  • Implementations: All five dialects.
  • Design quality: Textbook single-responsibility interface. The columns sub-package also defines a private quoter interface with the same signature (columns/columns.go:132), showing clean internal use of the same abstraction.

store#

  • Package: github.com/gobuffalo/pop/v6
  • File: store.go:12
  • Methods:
    Select(interface{}, string, ...interface{}) error
    Get(interface{}, string, ...interface{}) error
    NamedExec(string, interface{}) (sql.Result, error)
    NamedQuery(query string, arg interface{}) (*sqlx.Rows, error)
    Exec(string, ...interface{}) (sql.Result, error)
    PrepareNamed(string) (*sqlx.NamedStmt, error)
    Transaction() (*Tx, error)
    Rollback() error
    Commit() error
    Close() error
    SelectContext(context.Context, ...) error
    GetContext(context.Context, ...) error
    NamedExecContext(context.Context, ...) (sql.Result, error)
    NamedQueryContext(context.Context, ...) (*sqlx.Rows, error)
    ExecContext(context.Context, ...) (sql.Result, error)
    PrepareNamedContext(context.Context, ...) (*sqlx.NamedStmt, error)
    TransactionContext(context.Context) (*Tx, error)
    TransactionContextOptions(context.Context, *sql.TxOptions) (*Tx, error)
  • Purpose: Thin abstraction over *sqlx.DB / *sqlx.Tx. Enables the contextStore decorator (which wraps every non-context method to inject a stored context.Context) and the instrumented-driver overlay, all without changing the public Connection API.
  • Implementations: dB (wraps *sqlx.DB), contextStore (embedding store + ctx, overriding non-context methods), *Tx (wraps *sqlx.Tx).
  • Design quality: Broad but necessary. The duplication of every method in both context and non-context variants is the deliberate cost of backward compatibility — adding ctx to public methods would break existing callers. The contextStore decorator technique cleanly solves the problem. 18 methods is large; a stricter ISP application might split read/write/lifecycle concerns, but for an ORM abstraction this is acceptable.

Association#

  • Package: github.com/gobuffalo/pop/v6/associations
  • File: associations/association.go:13
  • Methods:
    Kind() reflect.Kind
    Interface() interface{}
    Constraint() (string, []interface{})
    InnerAssociations() InnerAssociations
    Skipped() bool
  • Purpose: Defines the contract for all relationship types (BelongsTo, HasMany, HasOne, ManyToMany). The eager-loading path in the root package queries a slice of Association values and dispatches based on capability interfaces (AssociationBeforeCreatable, AssociationAfterCreatable, etc.).
  • Implementations: belongsToAssociation, hasManyAssociation, hasOneAssociation, manyToManyAssociation — each embeds associationSkipable and associationComposite helper structs to satisfy Skipped() and InnerAssociations() without code repetition.
  • Design quality: Well-designed base interface. It is intentionally minimal; richer capabilities are expressed through extending interfaces rather than a fat Association. The use of helper embedding structs to share default implementations is idiomatic.

AssociationBeforeCreatable / AssociationAfterCreatable / AssociationCreatableStatement#

  • Package: github.com/gobuffalo/pop/v6/associations
  • File: associations/association.go:60,68,77
  • Methods (BeforeCreatable):
    BeforeInterface() interface{}
    BeforeSetup() error
    Association  // embedded
  • Methods (AfterCreatable):
    AfterInterface() interface{}
    AfterSetup() error
    AfterProcess() AssociationStatement
    Association  // embedded
  • Methods (CreatableStatement):
    Statements() []AssociationStatement
    Association  // embedded
  • Purpose: Capability segregation for the create lifecycle. BelongsTo associations implement BeforeCreatable (must be persisted before the parent record), HasMany/HasOne implement AfterCreatable, and ManyToMany implements CreatableStatement (produces raw join-table INSERTs). Each path is dispatched via type assertion in the create executor.
  • Implementations: belongsToAssociationBeforeCreatable; hasManyAssociation, hasOneAssociationAfterCreatable; manyToManyAssociationCreatableStatement.
  • Design quality: Excellent application of ISP. Instead of a monolithic Association with nullable lifecycle methods, the design adds capability via optional interfaces tested with type assertions. This is a canonical Go pattern for extensible type systems.

TableNameAble / TableNameAbleWithContext#

  • Package: github.com/gobuffalo/pop/v6
  • File: model.go:92,99
  • Methods:
    // TableNameAble
    TableName() string
    
    // TableNameAbleWithContext
    TableName(ctx context.Context) string
  • Purpose: Allows user model structs to override the default pluralized table name. Pop first checks for TableNameAbleWithContext (higher priority), then TableNameAble, then falls back to gobuffalo/flect pluralization. These are the primary extension points for end users.
  • Implementations: User-provided model types; no library-internal implementations.
  • Design quality: Clean, minimal consumer-defined interfaces. The context variant is a forward-compatible addition that doesn’t break the original interface. The priority chain (WithContext > plain > default) is a standard Go optional interface dispatch.

Lifecycle callback interfaces (callbacks.go)#

  • Package: github.com/gobuffalo/pop/v6
  • File: callbacks.go
  • Interfaces:
    AfterFindable        { AfterFind(*Connection) error }
    AfterEagerFindable   { AfterEagerFind(*Connection) error }
    BeforeSaveable       { BeforeSave(*Connection) error }
    BeforeCreateable     { BeforeCreate(*Connection) error }
    BeforeUpdateable     { BeforeUpdate(*Connection) error }
    BeforeDestroyable    { BeforeDestroy(*Connection) error }
    BeforeValidateable   { BeforeValidate(*Connection) error }
    AfterDestroyable     { AfterDestroy(*Connection) error }
    AfterUpdateable      { AfterUpdate(*Connection) error }
    AfterCreateable      { AfterCreate(*Connection) error }
    AfterSaveable        { AfterSave(*Connection) error }
  • Purpose: ActiveRecord-style lifecycle hooks. Model structs opt into specific hooks by implementing the corresponding interface. Pop checks for each interface via type assertion in the model dispatch methods (beforeCreate, afterSave, etc.) and calls them at the appropriate points in the CRUD pipeline.
  • Implementations: User-provided model types.
  • Design quality: Maximally segregated — eleven single-method interfaces, each independently optional. This is ideal ISP: a model can implement only BeforeCreateable without carrying the weight of all other hooks. The naming convention (-able suffix) is consistent and self-documenting.

Validation interfaces (validations.go — private)#

  • Package: github.com/gobuffalo/pop/v6
  • File: validations.go
  • Interfaces (unexported):
    validateable        { Validate(*Connection) (*validate.Errors, error) }
    validateCreateable  { ValidateCreate(*Connection) (*validate.Errors, error) }
    validateSaveable    { ValidateSave(*Connection) (*validate.Errors, error) }
    validateUpdateable  { ValidateUpdate(*Connection) (*validate.Errors, error) }
    beforeValidatable   { BeforeValidations(*Connection) error }
  • Purpose: Optional validation hooks that integrate with gobuffalo/validate. Pop calls these during ValidateAndCreate, ValidateAndSave, and ValidateAndUpdate. User model structs implement whichever subset they need.
  • Implementations: User-provided model types.
  • Design quality: Same ISP philosophy as callbacks. Keeping them unexported is slightly unusual — it means the Go doc won’t show them — but the public ValidateAndSave/ValidateAndCreate functions document which methods to implement in their own doc comments.

PaginationParams#

  • Package: github.com/gobuffalo/pop/v6
  • File: paginator.go:68
  • Methods:
    Get(key string) string
  • Purpose: Abstracts the query-string source for pagination parameters. NewPaginatorFromParams accepts this interface, which url.Values satisfies natively. Allows the paginator to be constructed from any key-value provider (HTTP query string, form values, custom map).
  • Implementations: url.Values (stdlib satisfies it directly), any custom map wrapper.
  • Design quality: Single-method interface that unlocks composition with stdlib. A clean example of defining an interface against what the consumer needs, not what providers already have.

Interface patterns#

  • Size distribution: The vast majority of interfaces are single-method (ISP applied aggressively). The two genuinely multi-method interfaces are dialect (~21 methods via composition) and store (18 methods) — both justified by their role as adapters over third-party drivers. The Association interface (5 methods) is the mid-range case. Average across the full set is approximately 2–3 methods.

  • Embedding: dialect embeds crudable, fizzable, and quotable — the canonical Go pattern for composing larger interfaces from focused ones. AssociationBeforeCreatable, AssociationAfterCreatable, and AssociationCreatableStatement all embed Association, building a capability hierarchy. contextStore embeds the store interface as a field to override only non-context methods while delegating the rest.

  • Implicit satisfaction: All user-facing interfaces (callbacks, validation hooks, TableNameAble) are designed to be satisfied implicitly by user model structs — Pop tests for them with type assertions at runtime, never requiring explicit var _ InterfaceName = (*MyModel)(nil) declarations. Internal interfaces (dialect, store, crudable) are satisfied only by library-internal types, never exposed as constraints users must meet.

  • stdlib interfaces used: io.Writer and io.Reader appear in dialect.DumpSchema / LoadSchema; sql.Result is returned from store methods; context.Context is threaded through the context variants of store. url.Values satisfies PaginationParams. No fmt.Stringer, io.Closer, or sort.Interface implementations are prominent, though Paginator.String() is provided as a convenience.


Key abstractions#

  1. dialect — The central portability interface. Its three-layer composition (crudable + fizzable + quotable) is the architectural decision that makes multi-database support maintainable. Everything that differs between databases lives behind this seam; everything generic (query building, model mapping, migration bookkeeping) lives in front of it.

  2. store — The connection-level abstraction that enables context propagation without API breakage. The contextStore decorator pattern built on this interface is a sophisticated, non-obvious technique for threading context.Context through a library that predates widespread context adoption.

  3. Association — The base of a capability-extension hierarchy. By keeping the base interface small and adding capabilities via AssociationBeforeCreatable / AfterCreatable / CreatableStatement, Pop achieves a flexible, ordered create lifecycle across four association types without a single switch statement or nil-check.

  4. Lifecycle callback interfaces (BeforeCreateable, AfterSaveable, etc.) — The ORM’s primary extension point for end users. Eleven single-method interfaces, each independently optional, implement the ActiveRecord callback pattern in idiomatic Go. This is arguably the best example in the codebase of ISP applied at the consumer-facing API level.

  5. TableNameAble / TableNameAbleWithContext — Simple but foundational. These two interfaces govern the single most common customization users need (override the table name). Their priority chain (context-aware > context-free > default) demonstrates backward-compatible interface evolution in Go.


Interface-driven extensibility#

Pop uses interfaces for extensibility at three distinct levels:

  • Driver-level (dialect + store): New databases are added by implementing dialect and registering a factory via init(). This is the framework’s deepest extension point and is used internally (five dialects ship with pop). External contributors can add dialects by satisfying the full dialect interface.

  • Association-level: New association types can be added by implementing Association and optionally the lifecycle sub-interfaces (BeforeCreatable, AfterCreatable, CreatableStatement). The associationBuilder function type and the builder registry in associations/ make this straightforward.

  • Model-level (user-facing): End users extend model behavior by implementing any combination of:

    • TableNameAble / TableNameAbleWithContext — custom table naming
    • BeforeCreateable, AfterSaveable, etc. — lifecycle callbacks (11 hooks)
    • validateable, validateCreateable, etc. — validation hooks (5 hooks)

    This “implement what you need” model is the dominant extensibility pattern in Pop and requires no registration, no configuration, and no code generation — just interface satisfaction.