Git Product home page Git Product logo

go-structured-query's Introduction

GoDoc-postgres GoDoc-mysql CI Go Report Card Coverage Status

Hey! Development has stopped on this for a while. Please see https://github.com/bokwoon95/sq for the spiritual successor to this library. I will still support this library, but new features will not be added. For the main changes and how to migrate from go-structured-query to sq, see https://bokwoon.neocities.org/sq#migrating-from-go-structured-query

sq (Structured Query)

๐ŸŽฏ๐Ÿ† sq is a code-generated, type safe query builder and struct mapper for Go. ๐Ÿ†๐ŸŽฏ
Documentation ย ย โ€ขย ย  Reference ย ย โ€ขย ย  Examples

This package provides type safe querying on top of Go's database/sql. It is essentially a glorified string builder, but automates things in all the right places to make working with SQL queries pleasant and boilerplate-free.

  • Avoid magic strings. SQL queries written in Go are full of magic strings: strings specified directly within application code that have an impact on the application's behavior. Specifically, you have to hardcode table or column names over and over into your queries (even ORMs are guilty of this!). Such magic strings are prone to typos and hard to change as your database schema changes. sq generates table structs from your database and ensures that whatever query you write is always reflective of what's actually in your database. more info

  • Better null handling. Handling NULLs is a bit of a pain in the ass in Go. You have to either use pointers (cannot be used in HTML templates) or sql.NullXXX structs (extra layer of indirection). sq scans NULLs as zero values, while still offering you the ability to check if the column was NULL. more info

  • The mapper function is the SELECT clause.

    • database/sql requires you to repeat the list of columns twice in the exact same order, once for SELECT-ing and once for scanning. If you mess the order up, that's an error.
    • Reflection-based mapping (struct tags) has you defining a set of possible column names to map, and then requires you repeat those columns names again in your query. If you mistype a column name in the struct tag, that's an error. If you SELECT a column that's not present in the struct, that's an error.
    • In sq whatever you SELECT is automatically mapped. This means you just have to write your query, execute it and if there were no errors, the data is already in your Go variables. No iterating rows, no specifying column scan order, no error checking three times. Write your query, run it, you're done.
    • more info

Features

Getting started

go get github.com/bokwoon95/go-structured-query

You will also need the dialect-specific code generator

# Postgres
go get github.com/bokwoon95/go-structured-query/cmd/sqgen-postgres

# MySQL
go get github.com/bokwoon95/go-structured-query/cmd/sqgen-mysql

Generate tables from your database

# for more options, check out --help

# Postgres
sqgen-postgres tables --database 'postgres://name:pass@localhost:5432/dbname?sslmode=disable' --overwrite

# MySQL
sqgen-mysql tables --database 'name:pass@tcp(127.0.0.1:3306)/dbname' --schemas dbname --overwrite

For an example of what the generated file looks like, check out postgres/devlab_tables_test.go.

Importing sq

Each SQL dialect has its own sq package. Import the sq package for the dialect you are using accordingly:

// Postgres
import (
    sq "github.com/bokwoon95/go-structured-query/postgres"
)

// MySQL
import (
    sq "github.com/bokwoon95/go-structured-query/mysql"
)

Examples

You just want to see code, right? Here's some.

SELECT

-- SQL
SELECT u.user_id, u.name, u.email, u.created_at
FROM public.users AS u
WHERE u.name = 'Bob';
// Go
u := tables.USERS().As("u") // table is code generated
var user User
var users []User
err := sq.
    From(u).
    Where(u.NAME.EqString("Bob")).
    Selectx(func(row *sq.Row) {
        user.UserID = row.Int(u.USER_ID)
        user.Name = row.String(u.NAME)
        user.Email = row.String(u.EMAIL)
        user.CreatedAt = row.Time(u.CREATED_AT)
    }, func() {
        users = append(users, user)
    }).
    Fetch(db)
if err != nil {
    // handle error
}

INSERT

-- SQL
INSERT INTO public.users (name, email)
VALUES ('Bob', '[email protected]'), ('Alice', '[email protected]'), ('Eve', '[email protected]');
// Go
u := tables.USERS().As("u") // table is code generated
users := []User{
    {Name: "Bob",   Email: "[email protected]"},
    {Name: "Alice", Email: "[email protected]"},
    {Name: "Eve  ", Email: "[email protected]"},
}
rowsAffected, err := sq.
    InsertInto(u).
    Valuesx(func(col *sq.Column) {
        for _, user := range users {
            col.SetString(u.NAME, user.Name)
            col.SetString(u.EMAIL, user.Email)
        }
    }).
    Exec(db, sq.ErowsAffected)
if err != nil {
    // handle error
}

UPDATE

-- SQL
UPDATE public.users
SET name = 'Bob', password = 'qwertyuiop'
WHERE email = '[email protected]';
// Go
u := tables.USERS().As("u") // table is code generated
user := User{
    Name:     "Bob",
    Email:    "[email protected]",
    Password: "qwertyuiop",
}
rowsAffected, err := sq.
    Update(u).
    Setx(func(col *sq.Column) {
        col.SetString(u.NAME, user.Name)
        col.SetString(u.PASSWORD, user.Password)
    }).
    Where(u.EMAIL.EqString(user.Email)).
    Exec(db, sq.ErowsAffected)
if err != nil {
    // handle error
}

DELETE

-- SQL
DELETE FROM public.users AS u
USING public.user_roles AS ur
JOIN public.user_roles_students AS urs ON urs.user_role_id = ur.user_role_id
WHERE u.user_id = ur.user_id AND urs.team_id = 15;
// Go
u   := tables.USERS().As("u")                 // tables are code generated
ur  := tables.USER_ROLES().As("ur")           // tables are code generated
urs := tables.USER_ROLES_STUDENTS().As("urs") // tables are code generated
rowsAffected, err := sq.
    DeleteFrom(u).
    Using(ur).
    Join(urs, urs.USER_ROLE_ID.Eq(ur.USER_ROLE_ID)).
    Where(
        u.USER_ID.Eq(ur.USER_ID),
        urs.TEAM_ID.EqInt(15),
    ).
    Exec(db, sq.ErowsAffected)
if err != nil {
    // handle error
}

For more information, check out the Basics.

For a list of example queries, check out Query Building.

Project Status

The external API is considered stable. Any changes will only be add to the API (like support for custom loggers and structured logging). If you have any feature requests or if you find bugs do open a new issue.

Contributing

See CONTRIBUTING.md

Appendix

Why this exists

I wrote this because I needed a more convenient way to scan database rows into nested structs, some of which exist twice in the same struct due to self joined tables. That made sqlx's StructScan unsuitable (e.g. cannot handle type Child struct { Father Person; Mother Person; }). database/sql's way of scanning is really verbose especially since I had about ~25 fields to scan into, some of which could be null. That's a lot of sql Null structs needed! Because I had opted to -not- pollute my domain structs with sql.NullInt64/ sql.NullString etc, I had to create a ton of intermediate Null structs just to contain the possible null fields, then transfer their zero value back into the domain struct. There had to be a better way. I just wanted their zero values, since everything in Go accomodates the zero value.

sq is therefore a data mapper first, and query builder second. I try my best to make the query builder as faithful to SQL as possible, but the main reason for its existence was always the struct mapping.

The case for ALL_CAPS

Here are the reasons why ALL_CAPS is used for table and column names over the idiomatic MixedCaps:

  1. jOOQ does it.
  2. It's SQL. It's fine if it doesn't follow Go convention, because it isn't Go.
    • Go requires exported fields by capitalized.
    • SQL, being case insensitive, generally uses underscores as word delimiters.
    • ALL_CAPS is a blend that satisfies both Go's export rules and SQL's naming conventions.
    • In my opinion, it is also easier to read because table and column names visually stand out from application code.
  3. Avoids clashing with interface methods. For a struct to fit the Table interface, it has to possess the methods GetAlias() and GetName(). This means that no columns can be called 'GetAlias' or 'GetName' because it would clash with the interface methods. This is sidestepped by following an entirely different naming scheme for columns i.e. ALL_CAPS.

On SQL Type Safety

sq makes no effort to check the semantics of your SQL queries at runtime. Any type checking is entirely enforced by what methods that you can call and argument types that you can pass to these methods. For example, You can call Asc()/Desc() and NullsFirst()/NullsLast() on any selected field and it would pass the type checker, because Asc()/Desc()/NullsFirst()/NullsLast() still return a Field interface:

u := tables.USERS().As("u")
sq.Select(u.USER_ID, u.USERNAME.Asc().NullsLast()).From(u)

which would translate to

SELECT u.user_id, u.username ASC NULLS LAST FROM users AS u
-- obviously wrong, you can't use ASC NULLS LAST inside the SELECT clause

The above example passes the Go type checker, so sq will happily build the query string -- even if that SQL query is sematically wrong. In practice, as long as you aren't trying to actively do the wrong thing (like in the above example), the limited type safety will prevent you from making the most common types of errors.

It also means the query builder will never fail: there's no boilerplate error checking required. Any semantic errors will be deferred to the database to point it out to you.

Dialect agnostic query builder?

sq is not dialect agnostic. This means I can add your favorite dialect specific SQL features without the headache of cross-dialect compatibility. It also makes contributions easier, as you just have to focus on your own SQL dialect and not care about the others.

go-structured-query's People

Contributors

bokwoon95 avatar gimlet2 avatar kblomster avatar quentin-fox avatar surajbarkale avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

go-structured-query's Issues

How to specify select query as one of the select fields?

With following two tables:

users (id, name)
likes (id, count, user_id)

Following SQL query can be created:

select *, (select count from likes where user_id = users.id) as like_count from users;

The reason for putting subquery in the field is that it can be selectively added to func(*sq.Row) function passed on to Selectx. In my code I have a very complex subquery like this. I am using the following go code:

type User struct {
  ID uuid.UUID
  Name string
  Likes *int
}

func (u *User) ScanRow(t tables.USERS, fetchLikes bool) func(*sq.Row) {
  return func(row *sq.Row) {
    u.ID = row.UUID(t.ID)
    u.Name = row.String(t.NAME)
    if fetchLikes {
      row.ScanInto(&u.Likes, sq.Fieldf("(select count from likes where user_id = ?)", t.ID).As("like_count"))
    }
  }
}

Can you please help me in removing the hard-coded SQL?

Q/A Where field equal subquery?

Hello, i can't find in the docs, how i can make query like this:
select name from categories where uuid = (select parent_uuid from categories where uuid = 'd45f1df9-169b-11e8-ba44-d8cb8abf61a1')
i find desicion by use WhereIn but question made is not the same.
Sorry. i try WhereIn and final query equal with what i wanted

Multiple tables in a query

Hi - I have a query that would be achieved in SQL like so

SELECT foo.A bar.B FROM A, B WHERE some predicate

I don't want to do a join (if I do it's a multi table join), and the second table's field is only used for an inner query

I cannot see a way to do that with this tool, have I missed something?

I've found that I can pass nil as a predicate - no idea what effect this will have on the query (yet, am testing)

data mapping for pointer fields will overwrite every item in the slice

sq's multi-row struct mapping works by repeatedly binding data to a single variable, then appending that variable to a slice. This works if the variable is a value type, since it will be copied by value every append: subsequent modifications to that variable will not affect what has already been appended into the slice. However if there were any fields that are pointer types, subsequent modifications to those fields will overwrite them for everything that has already been appended to the slice (since they were copied by reference).

There are workarounds for this (manually copying data before append), but it's not very user friendly as it requires understanding the inner workings of the library in order to fully grok.

I'm honestly not sure if there's a way around it, since the current way is the only way to have a fully 'generic' mapper function that works for any data type. By encapsulating stateful modifications in a closure function (or a pointer receiver method), the function for all mapper functions can have the same type of func (*sq.Row).

Even Go generics may not be sufficient here as they do not permit type parameters on methods. The following code below is ideal, but will not be achievable with Go's generics:

func (q SelectQuery) Get[Item any](db *DB, mapper func(*Row) Item) (Item, error)
func (q SelectQuery) GetSlice[Item any](db *DB, mapper func(*Row) Item) ([]Item, error)
func (q SelectQuery) GetAccumulate[Item, Items any](db *DB, initial Items, mapper func(*Row) Item, accumulator func(Item, Items) Items) (Items, error)

The document states that 'a suitably parameterized top-level function' will have to be written. Perhaps that may be the workaround.

func Get[Item any](db *DB, q Query, mapper func(*Row) Item) (Item, error)
func GetSlice[Item any](db *DB, q Query, mapper func(*Row) Item) ([]Item, error)

However the invocation will be so clunky compared to regular Selectx that I am not so sure that it is a direct upgrade:

// using Selectx
var user User
var user []User
err := sq.
    From(u).
    Where(u.Name.EqString("bob")).
    Selectx(user.RowMapper, func() { users = append(users, user) }).
    Fetch(db)
if err != nil {
}
fmt.Println(users)

// using GetSlice
users, err := sq.GetSlice(db,
    sq.From(u).
        Where(u.NAME.EqString("bob")),
    User{}.RowMapper,
if err != nil {
}
fmt.Println(users)
)

The only value Get brings over Selectx is that the mapper function can return a new value everytime rather than doing some side effect on a pointer receiver. My main issue with this is that it solves the problem of overwriting pointers fields but with an entirely different way of doing things (converting the query from a method to a top level function).

Allow the `Subquery` struct to satisfy the `Field` interface

It would be useful if the Subquery type could satisfy the Field interface, so that it could be used to embed subqueries in a select clause, without needing to add it to the from clause.

e.g. it would make it possible to write the following query (pulled from a project I'm working on)

SELECT
	matches.id,
	matches.date_created,
	matches.num_players,
	(select jsonb_agg(
		jsonb_build_object(
			'ID', match_users.id,
			'MatchID', match_users.match_id,
			'UserID', match_users.user_id,
			'Order', match_users.turn_order,
			'IsCreator', match_users.is_creator,
			'IsWinner', match_users.is_winner
		))
		from public.match_users where match_users.match_id = matches.id
	) as "match_users"
	FROM public.matches
        WHERE matches.id = $1;

Is there another way of doing this that I'm not aware of with this library? I don't mind reverting to plain SQL for cases like these, it would just be a nice addition to the query builder.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.