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

softdelete

package
v0.3.0 Latest Latest
Warning

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

Go to latest
Published: Nov 1, 2026 License: MIT Imports: 2 Imported by: 0

README

Soft Delete Plugin

Status: Proof of Concept — This plugin demonstrates the transformer architecture but is not production-ready. Use it as a reference implementation when building your own plugins.

For general plugin usage, see the Plugins guide. For plugin development, see the Plugin System README.

The soft delete plugin automatically injects IS NULL conditions into queries, filtering out soft-deleted rows. It operates as a Transformer that appends WHERE "table"."deleted_at" IS NULL for every table referenced in the FROM and JOIN clauses.

How It Works

The plugin inspects the AST at SQL generation time and calls plugins.CollectTables() to find all referenced tables (FROM and JOINs). For each table that passes the filter, it appends an IS NULL condition on the configured column. The condition is AND'd with any existing WHERE clauses.

By default it applies to every table using deleted_at as the column name. Both the column name and the set of tables can be customised via options.

The plugin supports SELECT, UPDATE, and DELETE operations. INSERT is left untouched (no soft-delete filtering on insertions).

Configuration Options
Option Description
WithColumn(name) Set the soft-delete column name (default: deleted_at)
WithTables(names...) Restrict the plugin to only the named tables
WithTableColumn(table, column) Set a per-table column override (also adds the table to the whitelist)
Table Aliases

When a table is aliased (e.g. "users" AS "u"), the plugin matches by the underlying table name (users) but qualifies the column with the alias ("u"."deleted_at" IS NULL).

Examples

Using the Plugin in Code
package main

import (
    "fmt"

    "github.com/bawdo/gosbee/managers"
    "github.com/bawdo/gosbee/nodes"
    "github.com/bawdo/gosbee/plugins/softdelete"
)

func main() {
    // --- Default: deleted_at IS NULL on all tables ---

    users := nodes.NewTable("users")
    query := managers.NewSelectManager(users)
    query.Use(softdelete.New())

    fmt.Println(query.ToSQL())
    // SELECT * FROM "users" WHERE "users"."deleted_at" IS NULL

    // --- Custom column name ---

    query = managers.NewSelectManager(users)
    query.Use(softdelete.New(softdelete.WithColumn("removed_at")))

    fmt.Println(query.ToSQL())
    // SELECT * FROM "users" WHERE "users"."removed_at" IS NULL

    // --- Restrict to specific tables ---

    posts := nodes.NewTable("posts")
    query = managers.NewSelectManager(users)
    query.InnerJoin(posts).On(users.Col("id").Eq(posts.Col("user_id")))
    query.Use(softdelete.New(softdelete.WithTables("users")))

    fmt.Println(query.ToSQL())
    // SELECT * FROM "users"
    //   INNER JOIN "posts" ON "users"."id" = "posts"."user_id"
    //   WHERE "users"."deleted_at" IS NULL
    // (posts is not filtered)

    // --- Per-table column overrides ---

    query = managers.NewSelectManager(users)
    query.InnerJoin(posts).On(users.Col("id").Eq(posts.Col("user_id")))
    query.Use(softdelete.New(
        softdelete.WithTableColumn("users", "deleted_at"),
        softdelete.WithTableColumn("posts", "removed_at"),
    ))

    fmt.Println(query.ToSQL())
    // SELECT * FROM "users"
    //   INNER JOIN "posts" ON "users"."id" = "posts"."user_id"
    //   WHERE "users"."deleted_at" IS NULL AND "posts"."removed_at" IS NULL
}
Using the REPL

The REPL provides a plugin command to enable and configure soft delete interactively.

gosbee> table users
  Registered table "users"
gosbee> from users
  Query FROM "users"
gosbee> sql
  SELECT * FROM "users";

gosbee> plugin softdelete
  Soft-delete enabled (column: deleted_at)
gosbee> sql
  SELECT * FROM "users" WHERE "users"."deleted_at" IS NULL;

Custom column name:

gosbee> plugin softdelete removed_at
  Soft-delete enabled (column: removed_at)
gosbee> sql
  SELECT * FROM "users" WHERE "users"."removed_at" IS NULL;

Restrict to specific tables when joining:

gosbee> plugin softdelete removed_at on users posts
  Soft-delete enabled (column: removed_at, tables: users, posts)

Per-table column overrides:

gosbee> plugin softdelete users.deleted_at, posts.removed_at
  Soft-delete enabled (per-table columns)

Check active plugins:

gosbee> plugins
  Available plugins:
    softdelete    on   (column: deleted_at)
    opa           off

Disable the plugin:

gosbee> plugin off softdelete
  Soft-delete disabled
REPL Commands
Command Description
plugin softdelete Enable with default column (deleted_at) on all tables
plugin softdelete <col> Enable with a custom column name on all tables
plugin softdelete <col> on <tables...> Enable with a custom column on specific tables
plugin softdelete <t.col, ...> Enable with per-table column overrides
plugin off softdelete Disable the soft delete plugin
plugin off Disable all plugins
plugins List available plugins and their status

Documentation

Overview

Package softdelete provides a Transformer that automatically injects "column IS NULL" conditions into SELECT queries, filtering out soft-deleted rows.

By default it appends WHERE "deleted_at" IS NULL for every table referenced in the FROM and JOIN clauses. Both the column name and the set of tables can be customised via options.

Basic usage

sd := softdelete.New()
query := managers.NewSelectManager(table)
query.Use(sd)
// SELECT * FROM "users" WHERE "users"."deleted_at" IS NULL

Custom column

sd := softdelete.New(softdelete.WithColumn("removed_at"))
// ... WHERE "users"."removed_at" IS NULL

Restrict to specific tables

When a query joins multiple tables but only some use soft-delete:

sd := softdelete.New(softdelete.WithTables("users"))
// Only "users" gets the IS NULL condition; other joined tables are unchanged.

Per-table columns

Different tables may use different column names for soft-delete:

sd := softdelete.New(
    softdelete.WithTableColumn("users", "deleted_at"),
    softdelete.WithTableColumn("posts", "removed_at"),
)
// users gets "deleted_at" IS NULL; posts gets "removed_at" IS NULL

Nested queries and raw SQL

Via a manager, the plugin applies inside subqueries, CTE bodies and set-operation branches. A name that refers to a CTE is not a table and is skipped. Queries containing raw SQL (SqlLiteral, StringJoin) are refused with plugins.ErrUnpoliceable unless WithAllowRawSQL is given.

REPL usage

gosbee> plugin softdelete
gosbee> plugin softdelete removed_at
gosbee> plugin softdelete removed_at on users posts
gosbee> plugin softdelete users.deleted_at, posts.removed_at
gosbee> plugin off softdelete
gosbee> plugins

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Option

type Option func(*SoftDelete)

Option configures a SoftDelete transformer.

func WithAllowRawSQL added in v0.2.0

func WithAllowRawSQL() Option

WithAllowRawSQL lets queries containing raw SQL fragments (SqlLiteral, StringJoin) through. By default the plugin refuses them with an error wrapping plugins.ErrUnpoliceable, because it cannot tell whether a raw fragment reads from a table it should be filtering. Only enable this when every raw fragment is trusted not to bypass soft-delete.

func WithColumn

func WithColumn(name string) Option

WithColumn sets the soft-delete column name. Default is "deleted_at".

func WithTableColumn

func WithTableColumn(table, column string) Option

WithTableColumn sets a per-table column override. The table is automatically added to the whitelist, restricting the plugin's scope.

Example

Tables that name their soft-delete column differently get a column each. A table with no entry is left alone.

package main

import (
	"fmt"

	"github.com/bawdo/gosbee/managers"
	"github.com/bawdo/gosbee/nodes"
	"github.com/bawdo/gosbee/plugins/softdelete"
	"github.com/bawdo/gosbee/visitors"
)

func printSQL(sql string, params []any, err error) {
	if err != nil {
		fmt.Println("error:", err)
		return
	}
	fmt.Println(sql)
	fmt.Println(params)
}

func main() {
	users := nodes.NewTable("users")
	posts := nodes.NewTable("posts")
	tags := nodes.NewTable("tags")

	query := managers.NewSelectManager(users).
		Select(users.Col("name"), posts.Col("title"), tags.Col("label")).
		Join(posts).On(posts.Col("user_id").Eq(users.Col("id"))).
		Join(tags).On(tags.Col("post_id").Eq(posts.Col("id"))).
		Use(softdelete.New(
			softdelete.WithTableColumn("users", "deleted_at"),
			softdelete.WithTableColumn("posts", "removed_at"),
		))

	printSQL(query.ToSQL(visitors.NewPostgresVisitor()))
}
Output:
SELECT "users"."name", "posts"."title", "tags"."label" FROM "users" INNER JOIN "posts" ON "posts"."user_id" = "users"."id" INNER JOIN "tags" ON "tags"."post_id" = "posts"."id" WHERE "users"."deleted_at" IS NULL AND "posts"."removed_at" IS NULL
[]

func WithTables

func WithTables(names ...string) Option

WithTables restricts the plugin to only the named tables. By default, the plugin applies to every table in the query.

type SoftDelete

type SoftDelete struct {
	plugins.BaseTransformer
	Column  string
	Columns map[string]string // per-table column overrides (table name → column name)
	// contains filtered or unexported fields
}

SoftDelete is a Transformer that appends IS NULL conditions for a soft-delete column on every referenced table (or a configured subset).

func New

func New(opts ...Option) *SoftDelete

New creates a SoftDelete transformer with the given options.

Example

By default every table in FROM and the joins gets a deleted_at IS NULL condition.

package main

import (
	"fmt"

	"github.com/bawdo/gosbee/managers"
	"github.com/bawdo/gosbee/nodes"
	"github.com/bawdo/gosbee/plugins/softdelete"
	"github.com/bawdo/gosbee/visitors"
)

func printSQL(sql string, params []any, err error) {
	if err != nil {
		fmt.Println("error:", err)
		return
	}
	fmt.Println(sql)
	fmt.Println(params)
}

func main() {
	users := nodes.NewTable("users")
	posts := nodes.NewTable("posts")

	query := managers.NewSelectManager(users).
		Select(users.Col("name"), posts.Col("title")).
		Join(posts).On(posts.Col("user_id").Eq(users.Col("id"))).
		Use(softdelete.New())

	printSQL(query.ToSQL(visitors.NewPostgresVisitor()))
}
Output:
SELECT "users"."name", "posts"."title" FROM "users" INNER JOIN "posts" ON "posts"."user_id" = "users"."id" WHERE "users"."deleted_at" IS NULL AND "posts"."deleted_at" IS NULL
[]

func (*SoftDelete) RefusesRawSQL added in v0.2.0

func (sd *SoftDelete) RefusesRawSQL() bool

RefusesRawSQL reports whether the walker should refuse raw SQL fragments. It is true unless WithAllowRawSQL was given.

func (*SoftDelete) TransformSelect

func (sd *SoftDelete) TransformSelect(core *nodes.SelectCore) (*nodes.SelectCore, error)

TransformSelect appends "column IS NULL" to the WHERE clause for each matching table referenced in the query (FROM and JOINs), and in every query nested in it, via plugins.ApplySelect. A direct call outside a manager gets the same scope and raw SQL refusal as the plugin walker. The input is not modified.

func (*SoftDelete) TransformSelectScoped added in v0.2.0

func (sd *SoftDelete) TransformSelectScoped(core *nodes.SelectCore, scope plugins.Scope) (*nodes.SelectCore, error)

TransformSelectScoped is TransformSelect aware of the walker's scope: names that refer to a CTE, and derived-table aliases, are not treated as tables.

Jump to

Keyboard shortcuts

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