squibble

package module
v0.0.0-...-3ee6329 Latest Latest
Warning

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

Go to latest
Published: Apr 8, 2024 License: BSD-3-Clause Imports: 19 Imported by: 2

README

squibble

Package squibble provides a schema migration assistant for SQLite databases.

GoDoc CI

A Schema value manages the schema of a SQLite database that will be modified over time. The current database schema is stored in the Current field, and migrations from previous versions are captured as UpdateRules.

Example

//go:embed schema.sql
var dbSchema string

var schema = &squibble.Schema{
	Current: dbSchema,

	Updates: []squibble.UpdateRule{
		// Each update gives the digests of the source and target schemas,
		// and a function to modify the first into the second.
		// The digests act as a version marker.
		{"a948904f2f0f479b8f8197694b30184b0d2ed1c1cd2a1ec0fb85d299a192a447",
			"727e2659ac457a3c86da2203ebd2e7387767ffe9a93501def5a87034ee672750",
			squibble.Exec(`CREATE TABLE foo (bar TEXT)`),
		},
		// The last update must end with the current schema.
		// Note that multiple changes are permitted in a rule.
		{"727e2659ac457a3c86da2203ebd2e7387767ffe9a93501def5a87034ee672750",
			"f18496b875133e09906a26ba23ef0e5f4085c1507dc3efee9af619759cb0fafe",
			squibble.Exec(
				`ALTER TABLE foo ADD COLUMN baz INTEGER NOT NULL`,
				`DROP VIEW quux`,
			),
		},
	},
}

func main() {
   flag.Parse()

   // Open the database as usual.
   db, err := sql.Open("sqlite", "test.db")
   if err != nil {
      log.Fatalf("Open db: %v", err)
   }

   // Apply any schema migrations needed.
   if err := schema.Apply(context.Background(), db); err != nil {
      log.Fatalf("Apply schema: %v", err)
   }

   // ...how you do
}

Usage Outline

For the following, assume your schema is defined in a file schema.sql and the current database is data.db.

  1. Modify schema.sql to look like the schema you want the database to end up with.

  2. Run squibble diff data.db schema.sql. This will print out the difference between the database schema and the update, including the computed digests.

    db:  b9062f812474223063c121d058e23823bf750074d1eba26605bbebbc9fd20dbe
    sql: 76a0ed44d8ad976d1de83bcb67d549dee2ab5bfb5af7d597d2548119e7359455
    < human-readable-ish diff >
    
  3. Using these digests, a new rule to the end of the Upgrades list like:

    {
      Source: "b9062f812474223063c121d058e23823bf750074d1eba26605bbebbc9fd20dbe",  // from the db
      Target: "76a0ed44d8ad976d1de83bcb67d549dee2ab5bfb5af7d597d2548119e7359455",  // from the schema
      Apply: squibble.Exec(`
         ALTER TABLE foo ADD COLUMN bar TEXT UNIQUE NOT NULL DEFAULT 'xyzzy';
         DROP VIEW IF EXISTS fuzzypants;
         CREATE INDEX horse_index ON animal (species) WHERE (species = 'horse');
      `),
    }
    

    Use squibble diff --rule data.db schema.sql to generate a copyable Go source text in this format. For example:

    {
        Source: "8d4f9b3e29aeca09e891460bf5ed08f12b84f6887b46a61082c339d49d7e0be8",
        Target: "b196954e613b770a4a1c0a07b96f6e03cb86923a226c2b53bd523fb759fef3d6",
        Apply: func(ctx context.Context, db squibble.DBConn) error {
            /* Schema diff:
    
            >> Modify table "Templates"
             ! replace column "raw" BLOB
               with "raw" BLOB not null
             + add column "count" INTEGER not null default=0
    
            >> Add table "lard"
             + CREATE TABLE lard (z integer, s text unique)
    
            */
            panic("not implemented")
        },
    },
    

    You will still need to fill in the update rule implementation, but a human-readable summary of the changes will be included as a comment to make it easier to figure out what to write. As shown in the example above, the squibble.Exec function can be helpful for simple changes.

    You should delete the comment before merging the rule, for legibility.

Mixing Migration and In-Place Updates

Some schema changes can be done "in-place", simply by re-applying the schema without any other migration steps. Typical examples include the addition or removal of whole tables, views, indexes, or triggers, which can be applied conditionally with statements like:

CREATE TABLE IF NOT EXISTS Foo ( ... )

DROP VIEW IF EXISTS Bar;

I generally recommend you not combine this style of update with use of the schema migrator. It works fine to do so, but adds extra friction.

If you do want to manage schema changes this way, you should apply the updated schema before calling the Apply method of the squibble.Schema. If the new schema has changes that are not compatible with the known migration state, the Apply method will report an error, and you can add an appropriate migration step.

For example, suppose you have this schema:

-- Schema 1
CREATE TABLE IF NOT EXISTS Foo (
  id INTEGER PRIMARY KEY,
  name TEXT NOT NULL
);

After executing Schema 1, the migrator will be satisfied: The schema before migration already looks like Schema 1, so there is nothing to do.

Now say you add a new column:

-- Schema 2
CREATE TABLE IF NOT EXISTS Foo (
  id INTEGER PRIMARY KEY,
  name TEXT NOT NULL,
  important BOOL   -- new column
);

When executing Schema 2, the database does not change: Table Foo already exists, so SQLite does not do anything. But the migrator sees that the schema has changed and it doesn't have a migration rule, so you will have to add one:

Updates: []squibble.UpdateRule{{
   Source: "7e4799f89f03e9913d309f50c4cc70963fc5607fb335aa318f9c246fdd336488",
   Target: "dee76ad0f980b8a5b419c4269559576d8413666adfe4a882e77f17b5792cca01",
   Apply:  squibble.Exec(`ALTER TABLE Foo ADD COLUMN important BOOL`),
}}

and the migrator will be happy. Now say you add a new table:

-- Schema 3
CREATE TABLE IF NOT EXISTS Foo (
  id INTEGER PRIMARY KEY,
  name TEXT NOT NULL,
  important BOOL   -- added in schema 2
);

CREATE TABLE IF NOT EXISTS Bar (comment TEXT NOT NULL);

This executes just fine, but now the state of the database seen by the migrator is different from the last state it has an update for: It has no migration rule to go from dee76ad0f980b8a5b419c4269559576d8413666adfe4a882e77f17b5792cca01 to 30233f4462f18d591795b1f8b455a5daf3b19c8786e90ec94daf8d3825de0320, which is the state of the database after Schema 3 was applied.

The migrator needs a rule for this, but the rule can be a no-op:

Updates: []squibble.UpdateRule{{
   Source: "7e4799f89f03e9913d309f50c4cc70963fc5607fb335aa318f9c246fdd336488",
   Target: "dee76ad0f980b8a5b419c4269559576d8413666adfe4a882e77f17b5792cca01",
   Apply:  squibble.Exec(`ALTER TABLE Foo ADD COLUMN important BOOL`),
}, {
   // This rule tells the migrator how to get to the current state, but
   // the change was already handled by the schema directly.
   Source: "dee76ad0f980b8a5b419c4269559576d8413666adfe4a882e77f17b5792cca01",
   Target: "30233f4462f18d591795b1f8b455a5daf3b19c8786e90ec94daf8d3825de0320",
   Apply:  squibble.NoAction, // does nothing, just marks an update
}}

Documentation

Overview

Package squibble provides a schema migration assistant for SQLite databases.

Overview

A Schema value manages the schema of a SQLite database that will be modified over time. The current database schema is stored in the Current field, and migrations from previous versions are captured as UpdateRules.

When the program starts up, it should pass the open database to the Apply method of the Schema. This verifies that the Schema is valid, then checks whether the database is up-to-date. If not, it applies any relevant update rules to bring it to the current state. If Apply fails, the database is rolled back.

The Schema tracks schema versions by hashing the schema with SHA256, and it stores a record of upgrades in a _schema_history table that it maintains. Apply creates this table if it does not already exist, and updates it as update rules are applied.

Update Rules

The Updates field of the Schema must contain an ordered list of update rules for all the versions of the schema prior to the Current one, from oldest to newest. Each rule has the hash of a previous schema version and a function that can be applied to the database to upgrade it to the next version in sequence.

When revising the schema, you must add a new rule mapping the old (existing) schema to the new one. These rules are intended to be a permanent record of changes, and should be committed into source control as part of the program. As a consistency check, each rule must also declare the hash of the target schema it upgrades to.

When Apply runs, it looks for the most recent version of the schema recorded in the _schema_history table. If there is none, and the database is otherwise empty, the current schema is assumed to be the initial version, and it is applied directly. Otherwise, Apply compares the hash of the most recent update to the current version: If they differ, it finds the most recent update hash in the Updates list, and applies all the updates from that point forward. If this succeeds, the current schema is recorded as the latest version in _schema_history.

Validation

You use the Validate function to check that the current schema in the special sqlite_schema table maintained by SQLite matches a schema written as SQL text. If not, it reports a diff describing the differences between what the text wants and what the real schema has.

Limitations

Currently this package only handles the main database, not attachments.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func DBDigest

func DBDigest(ctx context.Context, db DBConn) (string, error)

DBDigest computes a hex-encoded SHA256 digest of the SQLite schema encoded in the specified database.

func Exec

func Exec(stmts ...string) func(context.Context, DBConn) error

Exec returns an UpdateRule apply function that executes the specified statements sequentially.

func Logf

func Logf(ctx context.Context, msg string, args ...any)

Logf sends a log message to the logger attached to ctx, or to log.Printf if ctx does not have a logger attached. The context passed to the apply function of an UpdateRule will have this set to the logger for the Schema.

func NoAction

func NoAction(context.Context, DBConn) error

NoAction is a no-op update action.

func SQLDigest

func SQLDigest(text string) (string, error)

SQLDigest computes a hex-encoded SHA256 digest of the SQLite schema encoded by the specified string.

func Validate

func Validate(ctx context.Context, db DBConn, schema string) error

Validate checks whether the current schema of db appears to match the specified schema, and reports an error if there are discrepancies. An error reported by Validate has concrete type ValidationError if the schemas differ.

Types

type DBConn

type DBConn interface {
	QueryContext(context.Context, string, ...any) (*sql.Rows, error)
	ExecContext(context.Context, string, ...any) (sql.Result, error)
}

DBConn is the subset of the sql.DB interface needed by the functions defined in this package.

type HistoryRow

type HistoryRow struct {
	Timestamp time.Time `json:"timestamp"`     // In UTC
	Digest    string    `json:"digest"`        // The digest of the schema at this update
	Schema    string    `json:"sql,omitempty"` // The SQL of the schema at this update
}

HistoryRow is a row in the schema history maintained by the Schema type.

func History

func History(ctx context.Context, db DBConn) ([]HistoryRow, error)

History reports the history of schema upgrades recorded by db in chronological order.

type Schema

type Schema struct {
	// Current is the SQL definition of the most current version of the schema.
	// It must not be empty.
	Current string

	// Updates is a sequence of schema update rules. The slice must contain an
	// entry for each schema version prior to the newest.
	Updates []UpdateRule

	// Logf is where logs should be sent; the default is log.Printf.
	Logf func(string, ...any)
}

Schema defines a family of SQLite schema versions over time, expressed as a SQL definition of the current version of the schema, plus an ordered collection of upgrade rules that define how to update each version to the next.

func (*Schema) Apply

func (s *Schema) Apply(ctx context.Context, db *sql.DB) error

Apply applies any pending schema migrations to the given database. It reports an error immediately if s is not consistent (per Check); otherwise it creates a new transaction and attempts to apply all applicable upgrades to db within it. If this succeeds and the transaction commits successfully, then Apply succeeds. Otherwise, the transaction is rolled back and Apply reports the reason wny.

When applying a schema to an existing unmanaged database, Apply reports an error if the current schema is not compatible with the existing schema; otherwise it applies the current schema and updates the history.

func (*Schema) Check

func (s *Schema) Check() error

Check reports an error if there are consistency problems with the schema definition that prevent it from being applied.

A Schema is consistent if it has a non-empty Current schema text, all the update rules are correctly stitched (prev.Target == next.Source), and the last update rule in the sequence has the current schema as its target.

type UpdateRule

type UpdateRule struct {
	// Source is the hex-encoded SHA256 digest of the schema at which this
	// update applies. It must not be empty.
	Source string

	// Target is the hex-encoded SHA256 digest of the schema reached by applying
	// this update.  It must not be empty.
	Target string

	// Apply applies the necessary changes to update the schema to the next
	// version in sequence. It must not be nil.
	//
	// An apply function can use squibble.Logf(ctx, ...) to write log messages
	// to the logger defined by the associated Schema.
	Apply func(ctx context.Context, db DBConn) error
}

An UpdateRule defines a schema upgrade.

type ValidationError

type ValidationError struct {
	// Diff is a human readable summary of the difference between what was in
	// the database (-lhs) and the expected schema (+rhs).
	Diff string
}

ValidationError is the concrete type of errors reported by the Validate function.

func (ValidationError) Error

func (v ValidationError) Error() string

Directories

Path Synopsis
cmd
squibble
Program squibble is a command-line utility for managing SQLite schemas.
Program squibble is a command-line utility for managing SQLite schemas.

Jump to

Keyboard shortcuts

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