fournf

package module
v0.0.0-...-f4059b2 Latest Latest
Warning

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

Go to latest
Published: Apr 23, 2026 License: MIT Imports: 4 Imported by: 0

README

fournf

An Ent extension that enforces fourth normal form (4NF): all relationships must go through explicit join table schemas. No foreign key columns on entity tables.

Install

go get github.com/blobmasterbrian/fournf

Usage

1. Wire the extension into entc.go
//go:build ignore

package main

import (
    "log"

    "entgo.io/ent/entc"
    "entgo.io/ent/entc/gen"
    "github.com/blobmasterbrian/fournf"
)

func main() {
    ext, err := fournf.NewExtension()
    if err != nil {
        log.Fatalf("creating fournf extension: %v", err)
    }
    err = entc.Generate("./schema",
        &gen.Config{},
        entc.Extensions(ext),
    )
    if err != nil {
        log.Fatalf("running ent codegen: %v", err)
    }
}

go generate will now fail if any schema violates 4NF.

2. Define entity schemas and join table schemas

Entity schemas define your domain models. Join table schemas wire relationships between them and are annotated with fournf.JoinTable().

// schema/species.go
package schema

type Species struct { ent.Schema }

func (Species) Fields() []ent.Field {
    return []ent.Field{
        field.String("name"),
    }
}

func (Species) Edges() []ent.Edge {
    return []ent.Edge{
        edge.From("habitats", Habitat.Type).
            Ref("species"),
    }
}
// schema/habitat.go
package schema

type Habitat struct { ent.Schema }

func (Habitat) Fields() []ent.Field {
    return []ent.Field{
        field.String("name"), // e.g. "Rainforest", "Tundra"
    }
}

func (Habitat) Edges() []ent.Edge {
    return []ent.Edge{
        edge.From("species", Species.Type).
            Ref("habitats"),
    }
}
// schema/species_habitat.go — join table
package schema

import "github.com/blobmasterbrian/fournf"

type SpeciesHabitat struct { ent.Schema }

func (SpeciesHabitat) Annotations() []schema.Annotation {
    return []schema.Annotation{
        fournf.JoinTable(),
    }
}

func (SpeciesHabitat) Fields() []ent.Field {
    return []ent.Field{
        field.Int("species_id"),
        field.Int("habitat_id"),
    }
}

func (SpeciesHabitat) Edges() []ent.Edge {
    return []ent.Edge{
        edge.To("species", Species.Type).
            Unique().
            Required().
            Field("species_id"),
        edge.To("habitat", Habitat.Type).
            Unique().
            Required().
            Field("habitat_id"),
    }
}
What gets flagged

Foreign keys on entity tables. If an entity schema places a foreign key directly on its own table via .Field(), code generation will fail:

// schema/species.go — BAD: foreign key on an entity table
package schema

type Species struct { ent.Schema }

func (Species) Fields() []ent.Field {
    return []ent.Field{
        field.String("name"),
        field.Int("habitat_id"), // foreign key lives on the entity table
    }
}

func (Species) Edges() []ent.Edge {
    return []ent.Edge{
        edge.To("habitat", Habitat.Type).
            Unique().
            Field("habitat_id"), // this triggers the violation
    }
}
4NF violation: entity "Species" has edge "habitat" with .Field("habitat_id");
move this foreign key to a join table schema annotated with fournf.JoinTable()

Non-foreign-key fields on join tables. Join tables may only contain foreign key fields. Adding extra columns defeats the purpose of the join table:

// schema/species_habitat.go — BAD: extra field on a join table
package schema

type SpeciesHabitat struct { ent.Schema }

func (SpeciesHabitat) Annotations() []schema.Annotation {
    return []schema.Annotation{fournf.JoinTable()}
}

func (SpeciesHabitat) Fields() []ent.Field {
    return []ent.Field{
        field.Int("species_id"),
        field.Int("habitat_id"),
        field.String("notes"), // not a foreign key
    }
}
4NF violation: join table "SpeciesHabitat" has non-foreign-key field "notes";
join tables may only contain foreign key fields

Join tables with fewer than two foreign key edges. A join table must link at least two entities:

// schema/species_habitat.go — BAD: only one foreign key edge
package schema

type SpeciesHabitat struct { ent.Schema }

func (SpeciesHabitat) Annotations() []schema.Annotation {
    return []schema.Annotation{fournf.JoinTable()}
}

func (SpeciesHabitat) Fields() []ent.Field {
    return []ent.Field{
        field.Int("species_id"),
    }
}

func (SpeciesHabitat) Edges() []ent.Edge {
    return []ent.Edge{
        edge.To("species", Species.Type).
            Unique().
            Required().
            Field("species_id"),
    }
}
4NF violation: join table "SpeciesHabitat" has 1 foreign key edge(s), need at least 2
3. CI test (optional)

For a safety net independent of code generation:

func TestFourNF(t *testing.T) {
    fournftest.ValidateGraph(t, "./schema", "mymodule/ent")
}

How it works

In Ent, calling .Field() on an edge places a foreign key column on the schema's table. 4NF requires that multi-valued dependencies are factored into separate tables. This extension enforces that rule at two levels:

  1. Entity schemas must not have edges with .Field(). All foreign keys must live in dedicated join table schemas.
  2. Join table schemas (annotated with fournf.JoinTable()) must have at least two foreign key edges, and every field must be a foreign key. This prevents misuse of the annotation to bypass the entity restriction.

License

MIT

Documentation

Overview

Package fournf provides an entc extension that enforces 4NF.

Entity schemas must not have edges that use .Field() (which would place a foreign key column on the entity table). Only schemas annotated with JoinTable may do so. Wire it into your entc.go:

ext, err := fournf.NewExtension()
if err != nil {
    log.Fatalf("creating fournf extension: %v", err)
}
entc.Extensions(ext)

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func ValidateGraph

func ValidateGraph(schemaDir, pkg string) ([]string, error)

ValidateGraph loads the schema graph from schemaDir and checks that no entity schema has edges with .Field(). Returns a list of violations (empty = pass).

Types

type Annotation

type Annotation struct {
	// IsJoinTable marks the schema as a join table. Join tables must have at
	// least two foreign key edges and every field must be a foreign key.
	IsJoinTable bool `json:"is_join_table,omitempty"`
}

Annotation holds 4NF metadata for an Ent schema.

func JoinTable

func JoinTable() Annotation

JoinTable returns an Annotation that marks the schema as a join table.

func (Annotation) Merge

func (a Annotation) Merge(other schema.Annotation) schema.Annotation

Merge implements the ent schema.Annotation interface.

func (Annotation) Name

func (Annotation) Name() string

Name implements the ent schema.Annotation interface.

type Extension

type Extension struct {
	entc.DefaultExtension
}

Extension is an entc.Extension that fails code generation when an entity schema (one NOT annotated with JoinTable) contains an edge with .Field().

func NewExtension

func NewExtension() (*Extension, error)

NewExtension returns a new 4NF extension.

func (*Extension) Hooks

func (*Extension) Hooks() []gen.Hook

Hooks returns the generation hooks that perform the 4NF validation.

Directories

Path Synopsis
Package fournftest provides test helpers for 4NF validation.
Package fournftest provides test helpers for 4NF validation.

Jump to

Keyboard shortcuts

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