{"id":19816114,"url":"https://github.com/nofeaturesonlybugs/sqlh","last_synced_at":"2025-05-01T10:32:13.901Z","repository":{"id":57568205,"uuid":"342407337","full_name":"nofeaturesonlybugs/sqlh","owner":"nofeaturesonlybugs","description":"Powerful struct scanning for Go's database/sql and other compatible interfaces.","archived":false,"fork":false,"pushed_at":"2022-06-12T18:41:07.000Z","size":157,"stargazers_count":46,"open_issues_count":0,"forks_count":1,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-04-06T12:45:59.551Z","etag":null,"topics":["database","go","golang","postgresql","sql","sqlite3"],"latest_commit_sha":null,"homepage":"","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/nofeaturesonlybugs.png","metadata":{"files":{"readme":"README.md","changelog":"changelog.txt","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2021-02-25T23:30:08.000Z","updated_at":"2024-12-03T21:55:14.000Z","dependencies_parsed_at":"2022-08-28T10:01:57.501Z","dependency_job_id":null,"html_url":"https://github.com/nofeaturesonlybugs/sqlh","commit_stats":null,"previous_names":[],"tags_count":8,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nofeaturesonlybugs%2Fsqlh","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nofeaturesonlybugs%2Fsqlh/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nofeaturesonlybugs%2Fsqlh/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nofeaturesonlybugs%2Fsqlh/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/nofeaturesonlybugs","download_url":"https://codeload.github.com/nofeaturesonlybugs/sqlh/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":251860082,"owners_count":21655670,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["database","go","golang","postgresql","sql","sqlite3"],"created_at":"2024-11-12T10:08:24.897Z","updated_at":"2025-05-01T10:32:13.600Z","avatar_url":"https://github.com/nofeaturesonlybugs.png","language":"Go","readme":"[![Go Reference](https://pkg.go.dev/badge/github.com/nofeaturesonlybugs/sqlh.svg)](https://pkg.go.dev/github.com/nofeaturesonlybugs/sqlh)\n[![Go Report Card](https://goreportcard.com/badge/github.com/nofeaturesonlybugs/sqlh)](https://goreportcard.com/report/github.com/nofeaturesonlybugs/sqlh)\n[![Build Status](https://app.travis-ci.com/nofeaturesonlybugs/sqlh.svg?branch=master)](https://app.travis-ci.com/nofeaturesonlybugs/sqlh)\n[![codecov](https://codecov.io/gh/nofeaturesonlybugs/sqlh/branch/master/graph/badge.svg)](https://codecov.io/gh/nofeaturesonlybugs/sqlh)\n[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)\n\n`sqlh` aka `SQL Helper`.\n\n## `sqlh.Scanner`\n\n`sqlh.Scanner` is a powerful database result set scanner.\n\n-   Similar to `jmoiron/sqlx` but supports nested Go `structs`.\n-   _Should work with any `database/sql` compatible driver._\n\n## `model.Models`\n\n`model.Models` supports `INSERT|UPDATE` on Go `structs` registered as database _models_, where a _model_ is a language type mapped to a database table.\n\n-   Supports Postgres.\n-   Supports grammars that use `?` for parameters **and** have a `RETURNING` clause.\n    -   Benchmarked with Sqlite 3.35 -- your mileage may vary.\n\n## `sqlh` Design Philosphy\n\n```\nHand Crafted  |                                         |  Can I Have\n   Artisinal  | ======================================= |  My Database\n         SQL  |     ^                                   |  Back, Please?\n                    |\n                    +-- sqlh is here.\n```\n\n`sqlh` is easy to use because it lives very close to `database/sql`. The primary goal of `sqlh` is to work with and facilitate using `database/sql` without replacing or hijacking it. When using `sqlh` you manage your `*sql.DB` or create `*sql.Tx` as you normally would and pass those as arguments to functions in `sqlh` when scanning or persisting models; `sqlh` then works within the confines of what you gave it.\n\nWhen accepting arguments that work directly with the database (`*sql.DB` or `*sql.Tx`) `sqlh` accepts them as interfaces. This means `sqlh` may work with other database packages that define their own types as long as they kept a method set similar to `database/sql`.\n\nThe implementation for `sqlh` is fairly straight forward. Primarily this is because all the heavy `reflect` work is offloaded to `set`, which is another of my packages @ https://www.github.com/nofeaturesonlybugs/set\n\n`set` exports a flexible `set.Mapper` for mapping Go `structs` to string names such as database columns. A lot of the power and flexibility exposed by `sqlh` is really derived from `set`. I think this gives `sqlh` an advantage over similar database packages because it's very configurable, performs well, and alleviates `sqlh` from getting bogged down in the complexities of `reflect`.\n\nHere are some `sqlh.Scanner` examples:\n\n```go\ntype MyStruct struct {\n    Message string\n    Number  int\n}\n//\ndb, err := examples.Connect(examples.ExSimpleMapper)\nif err != nil {\n    fmt.Println(err.Error())\n}\n//\nscanner := \u0026sqlh.Scanner{\n    // Mapper is pure defaults.  Uses exported struct names as column names.\n    Mapper: \u0026set.Mapper{},\n}\nvar rv []MyStruct // []*MyStruct also acceptable\nerr = scanner.Select(db, \u0026rv, \"select * from mytable\")\nif err != nil {\n    fmt.Println(err.Error())\n}\n```\n\n```go\ntype Common struct {\n    Id       int       `json:\"id\"`\n    Created  time.Time `json:\"created\"`\n    Modified time.Time `json:\"modified\"`\n}\ntype Person struct {\n    Common\n    First string `json:\"first\"`\n    Last  string `json:\"last\"`\n}\n// Note here the natural mapping of SQL columns to nested structs.\ntype Sale struct {\n    Common\n    // customer_first and customer_last map to Customer.\n    Customer Person `json:\"customer\"`\n    // contact_first and contact_last map to Contact.\n    Contact Person `json:\"contact\"`\n}\ndb, err := examples.Connect(examples.ExNestedTwice)\nif err != nil {\n    fmt.Println(err.Error())\n}\n//\nscanner := \u0026sqlh.Scanner{\n    Mapper: \u0026set.Mapper{\n      // Mapper elevates Common to same level as other fields.\n      Elevated: set.NewTypeList(Common{}),\n      // Nested struct fields joined with _\n      Join:     \"_\",\n      // Mapper uses struct tag db or json, db higher priority.\n      Tags:     []string{\"db\", \"json\"},\n    },\n}\nvar rv []Sale // []*Sale also acceptable\nquery := `\n        select\n            s.id, s.created, s.modified,\n            s.customer_id, c.first as customer_first, c.last as customer_last,\n            s.vendor_id as contact_id, v.first as contact_first, v.last as contact_last\n        from sales s\n        inner join customers c on s.customer_id = c.id\n        inner join vendors v on s.vendor_id = v.id\n    `\nerr = scanner.Select(db, \u0026rv, query)\nif err != nil {\n    fmt.Println(err.Error())\n}\n```\n\n## Roadmap\n\nThe development of `sqlh` is essentially following my specific pain points when using `database/sql`:\n\n-   ✓ Row scanning provided by sqlh.Scanner\n-   ✓ High level Save() method provided by model.Models\n-   ✓ Specific Insert(), Update(), and Upsert() logic provided by model.Models\n    -   Upsert() currently supports conflict from primary key; conflicts on arbitrary unique indexes not supported.\n-   ⭴ `DELETE` CRUD statements : to be covered by `model.Models`.\n-   ⭴ `UPSERT` type operations using index information : to be covered by `model.Models`.\n-   ⭴ `Find()` or `Filter()` for advanced `WHERE` clauses and model selection.\n-   ⭴ Performance enhancements if possible.\n-   ⭴ Relationship management -- maybe.\n\nPersonally I find `SELECT|INSERT|UPDATE` to be the most painful and tedious with large queries or tables so those are the features I've addressed first.\n\n## `set.Mapper` Tips\n\nWhen you want `set.Mapper` to treat a nested struct as a single field rather than a struct itself add it to the `TreatAsScalar` member:\n\n-   `TreatAsScalar : set.NewTypeList( sql.NullBool{}, sql.NullString{} )`\n\nWhen you use a common nested struct to represent fields present in many of your types consider using the `Elevated` member:\n\n```go\ntype CommonDB struct {\n    Id int\n    CreatedAt time.Time\n    ModifiedAt time.Time\n}\ntype Something struct {\n    CommonDB\n    Name string\n}\n```\n\nWithout `Elevated` the `set.Mapper` will generate names like:\n\n```\nCommonDBId\nCommonDBCreatedAt\nCommonDBModifiedAt\nName\n```\n\nTo prevent `CommonDB` from being part of the name add `CommonDB{}` to the `Elevated` member of the mapper, which elevates the nested fields as if they were defined directly in the parent struct:\n\n```go\nElevated : set.NewTypeList( CommonDB{} )\n```\n\nThen the generated names will be:\n\n```\nId\nCreatedAt\nModifiedAt\nName\n```\n\nYou can further customize generated names with struct tags:\n\n```go\ntype CommonDB struct {\n    Id int `json:\"id\"`\n    CreatedAt time.Time `json:\"created\"`\n    ModifiedAt time.Time `json:\"modified\"`\n}\ntype Something struct {\n    CommonDB // No tag necessary since this field is Elevated.\n    Name string `json:\"name\"`\n}\n```\n\nSpecify the tag name to use in the `Tags` member, which is a `[]string`:\n\n```go\nTags : []string{\"json\"}\n```\n\nNow generated names will be:\n\n```\nid\ncreated\nmodified\nname\n```\n\nIf you want to use different names for some fields in your database versus your JSON encoding you can specify multiple `Tags`, with tags listed first taking higher priority:\n\n```go\nTags : []string{\"db\", \"json\"} // Uses either db or json, db has higher priority.\n```\n\nWith the above `Tags`, if `CommonDB` is defined as the following:\n\n```go\ntype CommonDB struct {\n    Id int `json:\"id\" db:\"pk\"`\n    CreatedAt time.Time `json:\"created\" db:\"created_tmz\"`\n    ModifiedAt time.Time `json:\"modified\" db:\"modified_tmz\"`\n}\n```\n\nThen the mapped names are:\n\n```\npk\ncreated_tmz\nmodified_tmz\nname\n```\n\n## Benchmarks\n\nSee my sibling package `sqlhbenchmarks` for my methodology, goals, and interpretation of results.\n\n## API Consistency and Breaking Changes\n\nI am making a very concerted effort to break the API as little as possible while adding features or fixing bugs. However this software is currently in a pre-1.0.0 version and breaking changes _are_ allowed under standard semver. As the API approaches a stable 1.0.0 release I will list any such breaking changes here and they will always be signaled by a bump in _minor_ version.\n\n-   0.4.0 ⭢ 0.5.0\n    -   `model.Models` methods allow `[]T` or `[]*T` when performing `INSERT|UPDATE|UPSERT` on slices of models.\n    -   `model.QueryBinding` is no longer an interface.\n    -   `model.Model` pruned:\n        -   Removed fields `V`, `VSlice` and `BoundMapping`\n        -   Removed methods `NewInstance` and `NewSlice`\n        -   `BindQuery()` signature changed to require a `*set.Mapper`\n    -   Upgrade `set` dependency to v0.5.1 for performance enhancements.\n-   0.3.0 ⭢ 0.4.0\n    -   `Transact(fn)` was correctly rolling the transaction back if `fn` returned `err != nil`; however\n        the error from `fn` and any potential error from the rollback were not returned from `Transact()`.\n        This is fixed in `0.4.0` and while technically a bug fix it _also_ changes the behavior of `Transact()`\n        to (correctly) return errors as it should have been doing. As this is a potentially breaking change\n        in behavior I have bumped the minor version for this patch.\n-   0.2.0 ⭢ 0.3.0\n    -   `grammar.Default` renamed to `grammar.Sqlite` -- generated SQL is same as previous version.\n    -   `grammar.Grammar` is now an interface where methods now return `(*statements.Query, error)`\n        where previously only `(*statements.Query)` was returned.\n    -   Package grammar no longer has any panics; errors are returned instead (see previous note).\n    -   Prior to this release `model.Models` only ran queries that had followup targets\n        for Scan() and panicked when such targets did not exist. This release allows for queries\n        that do not have any Scan() targets and will switch to calling Exec() instead of Query() or\n        QueryRow() when necessary. An implication of this change is that `Models.Insert()` and\n        `Models.Update()` no longer panic in the absence of Scan() targets.\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnofeaturesonlybugs%2Fsqlh","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fnofeaturesonlybugs%2Fsqlh","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnofeaturesonlybugs%2Fsqlh/lists"}