Skip to content

Latest commit

 

History

64 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Jone

A Go database query builder. Supports PostgreSQL and MySQL.

πŸ“¦ Installation

1. Install the CLI (one-time, adds jone command to your system):

go install github.com/Grandbusta/jone/cmd/jone@latest

2. Add library to your project (run in your project directory):

go get github.com/Grandbusta/jone

πŸš€ Quick Start

# Initialize jone in your project (defaults to postgres)
jone init

# Create a migration
jone migrate:make create_users

# Run all pending migrations
jone migrate:latest

# View migration status
jone migrate:list

πŸ’» CLI Commands

Command Description
jone init Initialize jone project. Creates jone/ folder and config.
jone migrate:make <name> Create a new migration file.
jone migrate:latest Run all pending migrations.
jone migrate:up [name] Run next pending migration (or specific one).
jone migrate:down [name] Rollback last migration (or specific one).
jone migrate:rollback Rollback last batch of migrations.
jone migrate:list List all migrations with status.
jone migrate:status Alias for migrate:list.

Flags

jone init

  • --db, -d β€” Database type: postgres, mysql, sqlite (default: postgres)

jone migrate:latest, migrate:up, migrate:down, migrate:rollback

  • --dry-run β€” Show SQL that would be executed without running it

jone migrate:rollback

  • --all, -a β€” Rollback all migrations (not just last batch)

βš™οΈ Configuration

After running jone init, edit jone/jonefile.go:

PostgreSQL:

package jone

import (
    "github.com/Grandbusta/jone"
    _ "github.com/jackc/pgx/v5/stdlib" // PostgreSQL driver
)

var Config = jone.Config{
    Client: "postgresql",
    Connection: jone.Connection{
        Host:     "localhost",
        Port:     "5432",
        User:     "postgres",
        Password: "password",
        Database: "my_db",
    },
    Migrations: jone.Migrations{
        TableName: "jone_migrations",
    },
}

var DB = jone.New(&Config)

MySQL:

package jone

import (
    "github.com/Grandbusta/jone"
    _ "github.com/go-sql-driver/mysql" // MySQL driver
)

var Config = jone.Config{
    Client: "mysql",
    Connection: jone.Connection{
        Host:     "localhost",
        Port:     "3306",
        User:     "root",
        Password: "password",
        Database: "my_db",
    },
    Migrations: jone.Migrations{
        TableName: "jone_migrations",
    },
}

var DB = jone.New(&Config)

πŸ”— Connection Pooling

Jone leverages Go's built-in database/sql connection pool. You can configure pool behavior by adding a Pool field to your config:

import (
    "time"

    "github.com/Grandbusta/jone"
    _ "github.com/jackc/pgx/v5/stdlib" // Driver
)

var Config = jone.Config{
    Client: "postgresql",
    Connection: jone.Connection{
        Host:     "localhost",
        Port:     "5432",
        User:     "postgres",
        Password: "password",
        Database: "my_db",
    },
    Pool: jone.Pool{
        MaxOpenConns:    10,              // Max open connections (0 = unlimited)
        MaxIdleConns:    5,               // Max idle connections (0 = default 2)
        ConnMaxLifetime: 30 * time.Minute, // Max connection reuse time (0 = no limit)
        ConnMaxIdleTime: 5 * time.Minute,  // Max idle time before close (0 = no limit)
    },
    Migrations: jone.Migrations{
        TableName: "jone_migrations",
    },
}

All fields are optional. Omitting Pool (or using zero values) preserves the database/sql defaults.

πŸ—οΈ Schema Builder

Creating Tables

func Up(s *jone.Schema) {
    s.CreateTable("users", func(t *jone.Table) {
        t.Increments("id")
        t.String("name").Length(100).NotNullable()
        t.String("email").Length(255).NotNullable().Unique()
        t.Text("bio").Nullable()
        t.Boolean("active").Default(true)
        t.Timestamps() // created_at, updated_at
    })
}

Column Types

Method SQL Type
Increments(name) SERIAL / AUTO_INCREMENT PRIMARY KEY
String(name) VARCHAR(255)
Text(name) TEXT
Int(name) INTEGER
BigInt(name) BIGINT
SmallInt(name) SMALLINT
Boolean(name) BOOLEAN
Float(name) REAL / FLOAT
Double(name) DOUBLE PRECISION
Decimal(name) DECIMAL
Date(name) DATE
Time(name) TIME
Timestamp(name) TIMESTAMP
UUID(name) UUID
JSON(name) JSON
JSONB(name) JSONB
Binary(name) BYTEA / BLOB

Column Modifiers

t.String("name").Length(100)         // Set length
t.String("email").NotNullable()      // NOT NULL
t.String("status").Default("new")    // Default value
t.String("code").Unique()            // Unique constraint
t.String("notes").Nullable()         // Explicitly nullable
t.String("title").Comment("...")     // Column comment
t.BigInt("user_id").Unsigned()       // Unsigned (MySQL)
t.BigInt("id").Primary()             // Primary key
t.Decimal("price").Precision(10).Scale(2) // DECIMAL(10,2)

Indexes

// Create index
t.Index("email")
t.Index("first_name", "last_name").Name("idx_full_name")
t.Index("data").Using("gin")  // Index method (btree, hash, gin, gist)

// Unique index
t.Unique("email")
t.Unique("org_id", "slug").Name("uq_org_slug")

Foreign Keys

t.BigInt("user_id").NotNullable()
t.Foreign("user_id").References("users", "id").OnDelete("CASCADE")
t.Foreign("org_id").References("orgs", "id").OnDelete("SET NULL").OnUpdate("CASCADE")
t.Foreign("custom").References("table", "col").Name("fk_custom_name")

Timestamps

t.Timestamps() // Adds created_at and updated_at

Altering Tables

func Up(s *jone.Schema) {
    s.Table("users", func(t *jone.Table) {
        t.String("phone").Nullable()        // Add column
        t.DropColumn("legacy_field")        // Drop column
        t.RenameColumn("name", "full_name") // Rename column
        t.SetNullable("bio")                // Make nullable
        t.DropNullable("email")             // Make not nullable
        t.SetDefault("active", true)        // Set default
        t.DropDefault("active")             // Drop default
        t.DropIndex("email")                // Drop index
        t.DropForeign("user_id")            // Drop foreign key
    })
}

Dropping Tables

func Down(s *jone.Schema) {
    s.DropTable("users")
    // or
    s.DropTableIfExists("users")
}

Other Operations

s.RenameTable("old_name", "new_name")
s.HasTable("users")           // Check if table exists
s.HasColumn("users", "email") // Check if column exists

Raw SQL

For custom statements the schema builder doesn't support:

// DDL statements
s.Raw("CREATE EXTENSION IF NOT EXISTS \"uuid-ossp\"")
s.Raw("CREATE INDEX CONCURRENTLY idx_users_email ON users(email)")

// Data migrations with parameters
s.Raw("INSERT INTO settings (key, value) VALUES ($1, $2)", "version", "1.0")
s.Raw("UPDATE users SET status = $1 WHERE created_at < $2", "legacy", "2020-01-01")

πŸ” Query Builder

Jone includes a fluent query builder. The database connection is lazy β€” it connects automatically on first query.

import jonecfg "myapp/jone"

// Single row insert with map
result, err := jonecfg.DB.Insert(map[string]any{
    "name":  "John",
    "email": "john@example.com",
}).Into("users").Exec()

// Multi-row insert
result, err := jonecfg.DB.Insert([]map[string]any{
    {"name": "John", "email": "john@example.com"},
    {"name": "Jane", "email": "jane@example.com"},
}).Into("users").Exec()

// Struct insert (uses db tags or snake_case field names)
type User struct {
    Name  string `db:"name"`
    Email string `db:"email"`
}
result, err := jonecfg.DB.Insert(User{Name: "John", Email: "john@example.com"}).Into("users").Exec()

// Raw SQL expressions
result, err := jonecfg.DB.Insert(map[string]any{
    "name":       "John",
    "created_at": jone.Fn.Now(), // CURRENT_TIMESTAMP
}).Into("users").Exec()

// Skip conflicting rows (ON CONFLICT DO NOTHING)
result, err := jonecfg.DB.Insert(map[string]any{
    "email": "john@example.com",
    "name":  "John",
}).OnConflict().Ignore().Into("users").Exec()

// Get generated values back (PostgreSQL RETURNING)
rows, err := jonecfg.DB.Insert(map[string]any{
    "name": "John",
}).Into("users").ExecReturning("id")
id := rows[0]["id"]

// SELECT with parameterized WHERE
rows, err := jonecfg.DB.Select("id", "name").From("users").
    Where("age", ">", 18).              // (column, operator, value)
    Where("active", true).              // (column, value) β†’ implied =
    WhereIn("status", []string{"active", "pending"}).
    WhereNull("deleted_at").
    Where(func(g *jone.WhereGroup) {    // parenthesized group
        g.Where("role", "admin").OrWhere("verified", true)
    }).
    OrderBy("created_at", "desc").
    Limit(10).
    Exec()

// First row as a map (sql.ErrNoRows if none)
row, err := jonecfg.DB.Select("*").From("users").Where("id", 1).First()

// All rows as []map[string]any (empty when none match)
users, err := jonecfg.DB.Select("*").From("users").Where("active", true).All()

// Joins β€” qualified columns are quoted ("users.id" β†’ "users"."id"),
// tables support "as" aliasing
rows, err := jonecfg.DB.Select("users.name", "o.total").From("users").
    Join("orders as o", "users.id", "o.user_id").
    Where("o.total", ">", 100).
    Exec()

// Derived tables β€” name a subquery with As() and select from it
sub := jone.Select("user_id").From("orders").GroupBy("user_id").As("t")
rows, err := jonecfg.DB.Select("*").From(sub).Exec()

// CTEs, unions, and row locking
rows, err = jonecfg.DB.Select("*").From("totals").
    With("totals", jone.Select("user_id").From("orders").GroupBy("user_id")).
    Exec()
rows, err = jonecfg.DB.Select("name").From("users").
    Union(jone.Select("name").From("archived_users")).Exec()
row, err = jonecfg.DB.Select("*").From("jobs").
    Where("status", "queued").ForUpdate().SkipLocked().First()

// UPDATE β€” the data is passed directly to Update; SET and WHERE are both
// parameterized
result, err := jonecfg.DB.Update("users", map[string]any{
    "name":       "Alice",
    "updated_at": jone.Fn.Now(),
}).Where("id", 1).Exec()

// Structs work too, using `db` tags with snake_case fallback (same as Insert),
// and there's a single-pair shorthand
result, err = jonecfg.DB.Update("users", user).Where("id", 1).Exec()
result, err = jonecfg.DB.Update("books", "title", "Slaughterhouse Five").Where("id", 42).Exec()

// DELETE
result, err := jonecfg.DB.Delete("users").Where("active", false).Exec()

// TRUNCATE β€” remove all rows
result, err = jonecfg.DB.Truncate("sessions").Exec()

// Raw SQL β€” ? placeholders are rebound per dialect ($n on PostgreSQL)
rows, err := jonecfg.DB.RawQuery("SELECT * FROM users WHERE age > ?", 21).All()
result, err = jonecfg.DB.RawQuery("UPDATE users SET status = ? WHERE id = ?", "legacy", 7).Exec()

πŸ“ Migration Example

// jone/migrations/20260123000000_create_users/migration.go
package m20260123000000

import "github.com/Grandbusta/jone"

func Up(s *jone.Schema) {
    s.CreateTable("users", func(t *jone.Table) {
        t.Increments("id")
        t.String("email").NotNullable().Unique()
        t.String("password_hash").NotNullable()
        t.String("name").Length(100)
        t.Boolean("verified").Default(false)
        t.Timestamps()
    })

    s.CreateTable("posts", func(t *jone.Table) {
        t.Increments("id")
        t.BigInt("user_id").NotNullable()
        t.String("title").NotNullable()
        t.Text("content")
        t.Timestamps()

        t.Foreign("user_id").References("users", "id").OnDelete("CASCADE")
        t.Index("user_id")
    })
}

func Down(s *jone.Schema) {
    s.DropTableIfExists("posts")
    s.DropTableIfExists("users")
}

πŸ—„οΈ Supported Databases

Database Driver Package Status
PostgreSQL github.com/jackc/pgx/v5/stdlib βœ… Supported
MySQL github.com/go-sql-driver/mysql βœ… Supported
SQLite github.com/mattn/go-sqlite3 🚧 Planned

🀝 Contributing

Contributions are welcome! Please see our Contributing Guide for more details.

πŸ“„ License

MIT

About

A @knex inspired SQL migration and schema builder for Go. Supports PostgreSQL today, Others coming soon. Designed to be flexible, and fun to use.

Topics

Resources

Contributing

Stars

30 stars

Watchers

2 watching

Forks

Releases

Packages

Contributors

Languages