{"id":37093022,"url":"https://github.com/sourcesoft/ssql","last_synced_at":"2026-01-14T11:18:48.952Z","repository":{"id":65408325,"uuid":"589433269","full_name":"sourcesoft/ssql","owner":"sourcesoft","description":"Tiny opinionated 'database/sql' wrapper focused on simplicity with built-in support for offset/cursor pagination and GraphQL (Relay Connections)","archived":false,"fork":false,"pushed_at":"2023-12-28T18:58:34.000Z","size":112,"stargazers_count":12,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2024-06-21T15:35:25.233Z","etag":null,"topics":["cursor","database","go","golang","graphql","pagination","relay","sql"],"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/sourcesoft.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2023-01-16T05:08:12.000Z","updated_at":"2024-01-11T03:27:55.000Z","dependencies_parsed_at":"2024-06-21T14:16:35.146Z","dependency_job_id":"66b6da8b-9196-4a66-aba2-9943efc103fc","html_url":"https://github.com/sourcesoft/ssql","commit_stats":null,"previous_names":[],"tags_count":3,"template":false,"template_full_name":null,"purl":"pkg:github/sourcesoft/ssql","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sourcesoft%2Fssql","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sourcesoft%2Fssql/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sourcesoft%2Fssql/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sourcesoft%2Fssql/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/sourcesoft","download_url":"https://codeload.github.com/sourcesoft/ssql/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sourcesoft%2Fssql/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28418141,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-14T10:47:48.104Z","status":"ssl_error","status_checked_at":"2026-01-14T10:46:19.031Z","response_time":107,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.6:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"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":["cursor","database","go","golang","graphql","pagination","relay","sql"],"created_at":"2026-01-14T11:18:48.391Z","updated_at":"2026-01-14T11:18:48.933Z","avatar_url":"https://github.com/sourcesoft.png","language":"Go","readme":"[![Godoc](http://img.shields.io/badge/godoc-reference-blue.svg?style=flat)](https://godoc.org/github.com/sourcesoft/ssql) [![license](http://img.shields.io/badge/license-MIT-red.svg?style=flat)](https://raw.githubusercontent.com/sourcesoft/ssql/main/LICENSE)\n\n**This library is still in development and the API may change, see the [roadmap](#roadmap) for more information.**\n\n### Table of Contents\n\n- [What it is and what it's not](#what-it-is-and-what-its-not)\n  * [Goal \u0026 Features](#goal--features)\n  * [Limitations](#limitations)\n- [Getting Started](#getting-started)\n- [APIs](#apis)\n  * [Insert](#insert)\n  * [UpdateOne](#updateone)\n  * [Update](#update)\n  * [DeleteOne](#deleteone)\n  * [Delete](#delete)\n  * [FindOne](#findone)\n  * [Find](#find)\n    + [Find: Simple](#find-simple)\n    + [Find: SuperScan 💪](#find-superscan)\n    + [Find: Offset Pagination](#find-offset-pagination)\n    + [Find: Cursor Pagination](#find-cursor-pagination)\n    + [Find: Sorting](#find-sorting)\n    + [Find: Conditions](#find-conditions)\n    + [Find: GraphQL](#find-graphql)\n  * [Raw](#raw)\n  * [Helpers working with structs](#helpers-working-with-structs)\n  * [Helpers scanning query results](#helpers-scanning-query-results)\n- [Roadmap](#roadmap)\n- [Credits](#credits)\n\n## What it is and what it's not\n\nThis is not an ORM. The client is just a tiny simple wrapper around `database/sql`\nthat provides support for simple querying pattern. It supports and provides\nextra utilities that can be used with that makes it actually useful.\n\nIf you need anything more than what the API provides, you can use the `Raw` method.\n\n[API Reference](https://pkg.go.dev/github.com/sourcesoft/ssql)\n\n[Examples](https://github.com/sourcesoft/ssql/tree/main/_examples)\n\n### Goal \u0026 Features\n\n- No unnecessary extra abstraction, should be compatible with standard `database/sql`\n- Opt-in for features that make common complex query patterns simple\n- Be opinionated and enforce some usage patterns best practices\n- Minimum use of `reflect`\n- Some common utilities for everyday usage like `sqlx` scan while still being compatible with standard `sql` lib\n- GraphQL (+Relay Connection) cursor pagination\n- Limit and offset pagination built in and enforced\n\n### Limitations\n\nIf your DML is not a simple query that is not supported, **just use the `Raw` method instead**. \nWe'll keep it intentionally simple:\n\n- No JOINS\n- No ORM features\n- No transaction support to build complex queries\n- Honesty most non-trival query patterns are not added \n\nIf you think you need more patterns/utilities/methods/helpers, and it's actually useful that is hard to do\nwithout a wrapper, feel free to open a PR.\n\n[↩](#table-of-contents)\n\n## Getting Started\n\nGet the library with:\n\n```bash\ngo get \"github.com/sourcesoft/ssql\"\n```\n\n[API Reference](https://pkg.go.dev/github.com/sourcesoft/ssql)\n\n[Examples](https://github.com/sourcesoft/ssql/tree/main/_examples)\n\nFirst create the client by connecting to a database of your choice.\n\n```golang\npackage main\n\nimport (\n  \"context\"\n  \"database/sql\"\n\n  _ \"github.com/lib/pq\"\n  \"github.com/sourcesoft/ssql\"\n)\n\nfunc main() {\n  ...\n  ctx := context.Background()\n  psqlInfo := fmt.Sprintf(\"host=%s port=%d user=%s \"+\n    \"password=%s dbname=%s sslmode=disable\", ...)\n  dbCon, err := sql.Open(\"postgres\", psqlInfo)\n  if err != nil {\n    panic(err)\n  }\n  // You can also pass nil as options.\n  options := ssql.Options{\n    Tag:      \"sql\", // Struct tag used for SQL field name (defaults to 'sql').\n    LogLevel: ssql.LevelDebug, // By default loggin is disabled.\n    MainSortField:     \"created_at\",\n    MainSortDirection: ssql.DirectionDesc,\n  }\n  client, err := ssql.NewClient(ctx, dbCon, \u0026options)\n  if err != nil {\n    panic(err)\n  }\n  ...\n```\n\nNote that we have passed `MainSortField` and `MainSortDirection` options which is the default\nfield and sorting direction used for pagination. SSQL library enforces these fields to be specified in either\nin the client Options or you can pass them as part of the query options to `Find` method to override the default.\nOnly `Find` method requires these two options, without them it will return early with an error.\n\nSee queries like [FindOne](#findone) or other APIs to see how to use the client to execute queries.\n\n[↩](#table-of-contents)\n\n## APIs\n\n### Insert\n\nWhen using insert, you will be passing the variable of a struct, which `ssql` uses `reflect` package to\nextract the `sql` tag by default (you can customize it in the client options).\n\n```golang\ntype User struct {\n  ID            *string `json:\"id,omitempty\" sql:\"id\" graph:\"id\" rel:\"pk\"`\n  Username      *string `json:\"username,omitempty\" sql:\"username\" graph:\"username\"`\n  Email         *string `json:\"email,omitempty\" sql:\"email\" graph:\"email\"`\n  EmailVerified *bool   `json:\"emailVerified,omitempty\" sql:\"email_verified\" graph:\"emailVerified\"`\n  Active        *bool   `json:\"active,omitempty\" sql:\"active\" graph:\"active\"`\n  UpdatedAt     *int    `json:\"updatedAt,omitempty\" sql:\"updated_at\" graph:\"updatedAt\"`\n  CreatedAt     *int    `json:\"createdAt,omitempty\" sql:\"created_at\" graph:\"createdAt\"`\n  DeletedAt     *int    `json:\"deletedAt,omitempty\" sql:\"deleted_at\" graph:\"deletedAt\"`\n}\n// Sample record.\nfID := \"7f8d1637-ca82-4b1b-91dc-0828c98ebb34\"\nfUsername := \"test\"\nfEmail := \"test@domain.com\"\nts := 1673899847\n\n// Insert a new row.\nnewUser := User{\n  ID:        \u0026fID,\n  Username:  \u0026fUsername,\n  Email:     \u0026fEmail,\n  UpdatedAt: \u0026ts,\n  CreatedAt: \u0026ts,\n}\n// You can pass any struct as is.\n_, err = client.Insert(ctx, \"user\", newUser)\nif err != nil {\n  panic(err)\n}\n\n```\n\n[↩](#table-of-contents)\n\n### UpdateOne\n\nHaving the ID of the record you can simply update it by passing the struct variable.\n\n```golang\n// Update row by ID.\nfEmail = \"new@test.com\"\nnewUser.Email = \u0026fEmail\nres, err := client.UpdateOne(ctx, \"user\", \"id\", fID, newUser)\nif err != nil {\n  log.Error().Err(err).Msg(\"Postgres update user error\")\n  panic(err)\n}\nif count, err := (*res).RowsAffected(); count \u003c 1 {\n  log.Error().Err(err).Msg(\"Postgres update user error, or not found\")\n  panic(err)\n}\n```\n\n[↩](#table-of-contents)\n\n### Update\n\nYou can also create a condition array to update all the matching fields.\n\n```golang\n...\n// Add some custom conditions.\nconds := []*ssql.ConditionPair{{\n  Field: \"active\",\n  Value: true,\n  Op:    ssql.OPEqual, // '=' operator.\n}}\nfFalse = false\nnewUser.Active = \u0026fFalse\nres, err := client.Update(ctx, \"user\", conds, updatedUser)\n...\n```\n\n[↩](#table-of-contents)\n\n### DeleteOne\n\n```golang\nres, err = client.DeleteOne(ctx, \"user\", \"id\", fID)\nif err != nil {\n  log.Error().Err(err).Msg(\"Cannot delete user by ID from Postgres\")\n  panic(err)\n}\nif count, err := (*res).RowsAffected(); count \u003c 1 {\n  log.Error().Err(err).Msg(\"User not found\")\n  panic(err)\n}\n```\n\n[↩](#table-of-contents)\n\n### Delete\n\nYou can also create a condition array to delete all the matching fields.\n\n```golang\n...\n// Add some custom conditions.\nconds := []*ssql.ConditionPair{{\n  Field: \"active\",\n  Value: false,\n  Op:    ssql.OPEqual, // '=' operator.\n}}\nres, err = client.Delete(ctx, \"user\", conds)\nif err != nil {\n  log.Error().Err(err).Msg(\"Cannot delete user\")\n  panic(err)\n}\n...\n```\n\n[↩](#table-of-contents)\n\n### FindOne\n\nHaving a primary key and finding your record using that is a common use case.\nYou can also pass the key (`id` in the following example) to look up.\n\n```golang\nrows, err := client.FindOne(ctx, \"user\", \"id\", \"7f8d1637-ca82-4b1b-91dc-0828c98ebb34\")\nif err != nil {\n\tpanic(err)\n}\n// You can scan all the fields to the struct directly.\nvar resp User\nif err := ssql.ScanOne(\u0026resp, rows); err != nil {\n\tpanic(err)\n}\nlogger.Print(\"user %+v\", resp)\n```\n\nCheck the [examples](https://github.com/sourcesoft/ssql/tree/main/_examples) folder to see more.\n\n\n[↩](#table-of-contents)\n\n### Find\n\n#### Find: Simple\n\nLet's see how a minimal simple `Find` query looks like.\n\nFirst build the query options\n\n```golang\n// setting up pagination.\nlimit := 10\nparams := ssql.Params{\n  OffsetParams: \u0026ssql.OffsetParams{\n    Limit: \u0026limit,\n    // There's also offset available.\n  },\n  // There's also 'order' available.\n  // There's also 'cursor' pagination available.\n}\nopts := ssql.SQLQueryOptions{\n  Table:          \"user\",\n  WithTotalCount: shouldReturnTotalCount,\n  Params:         \u0026params,\n  MainSortField:     \"created_at\", // Used for cursor/offset pagination\n  MainSortDirection: ssql.DirectionDesc,\n}\n```\n\n**Note SSQL library enforces `MainSortField` and `MainSortDirection` fields to be specified in either\nthe client Options (as default) or you can pass them as part of the query options to `Find` method here to override the default.\nOnly `Find` method requires these two options, without them it will return early with an error.**\n\nIf you are unsure what field to use for `MainSortField`, you can choose the auto-increment ID or (if the PK is sth like GUID) you can\nchoose a field that has epoch timestamp on it, eg: `created_at` or `updated_at`.\n\nCurrent `MainSortField` only supports integer values, support for timestamp SQL types will be added soon.\n\nFor the rest of the documentation we will not mention these two options, assuming you have specified them at the top level\nin client options which is used as a fallback default config.\n\nRun the query by using the `Find` method.\n\n```golang\n// Executing the query.\nresult, err := client.Find(ctx, \u0026opts)\nif err != nil {\n  log.Error().Err(err).Msg(\"Cannot find users\")\n  panic(err)\n}\n```\n\nScan the rows into your arrays of custom structs.\n\n```golang\n// Reading through the results.\nvar users []User\nfor result.Rows.Next() {\n  var user User\n  if err := ssql.ScanRow(\u0026user, result.Rows); err != nil {\n    log.Error().Err(err).Msg(\"Cannot scan users\")\n  }\n  users = append(users, user)\n}\n```\n\n[↩](#table-of-contents)\n\n#### Find: SuperScan\n\n`ssql` also provides a powerful `SuperScan` function that takes care of Relay Connection and cursor\npagination complexity. It also does the Scan itself for us and then spits out a `PageInfo` object:\n\n```golang\ntype PageInfo struct {\n  HasPreviousPage *bool   `json:\"hasPreviousPage,omitempty\"`\n  HasNextPage     *bool   `json:\"hasNextPage,omitempty\"`\n  StartCursor     *string `json:\"startCursor,omitempty\"`\n  EndCursor       *string `json:\"endCursor,omitempty\"`\n  TotalCount      *int    `json:\"-\"`\n}\n```\n\nAs you see, it's very similar to Relay connection type, in fact you can just use it as is in your\nGraphQL response.\n\nMost of the code is same up to running the `Find` method.\n\n```golang\n// setting up pagination.\nlimit := 10\nparams := ssql.Params{\n\tCursorParams: \u0026ssql.CursorParams{\n\t\tFirst: \u0026limit, // Get first 10 rows only.\n\t},\n}\nopts := ssql.SQLQueryOptions{\n  Table:          \"user\",\n  WithTotalCount: shouldReturnTotalCount,\n  Params:         \u0026params,\n}\n// Executing the query.\nresult, err := client.Find(ctx, \u0026opts)\nif err != nil {\n  log.Error().Err(err).Msg(\"Cannot find users\")\n  panic(err)\n}\n```\n\nNote that we used `CursorParams` setting `First` option instead of offset. It's\nrecommended to use `CursorParams` instead of `OffsetParams` as options, this allows\nthe return `PageInfo` of `SuperScan` to return a more correct and complete result.\n\n\nUse the returned `result` and create a variable to store your list in. Pass\nboth to `SuperScan` method and that's it.\n\n```golang\nvar users []User\npageInfo, err := ssql.SuperScan(\u0026users, result)\nif err != nil {\n  log.Error().Err(err).Msg(\"SuperScan failed\")\n  panic(err)\n}\n```\n\n[↩](#table-of-contents)\n\n#### Find: Offset Pagination\n\nYou can use offset and limit pagination.\n\n```golang\nlimit := 10\noffset := 3\nparams := ssql.Params{\n  OffsetParams: \u0026ssql.OffsetParams{\n    Limit: \u0026limit,\n    Offset: \u0026offset\n  },\n}\n// Same as before use the params in query options argument.\nopts := ssql.SQLQueryOptions{\n  Table:          \"user\",\n  Params:         \u0026params,\n}\nresult, err := client.Find(ctx, \u0026opts)\n```\n\n[↩](#table-of-contents)\n\n#### Find: Cursor Pagination\n\nIf you use `SuperScan` in your queries, you can then use the `PageInfo` object that has\nthe `StartCursor` and `EndCursor`.\n\n```golang\n// From previous query\nvar users []User\npageInfo, err := ssql.SuperScan(\u0026users, result)\nif err != nil {\n  log.Error().Err(err).Msg(\"SuperScan failed\")\n  panic(err)\n}\n// Now that we have pageInfo object, we can use the next cursor.\nparams := ssql.Params{\n  CursorParams: \u0026ssql.CursorParams{\n    After:  pageInfo.EndCursor, // If you have the previous cursor, you can pass it here to continue the pagination.\n    First:  10, // Get first 10 results (works like LIMIT).\n    Last:   nil, // Work same as first (LIMIT) but reverses the order of querying.\n  },\n}\n// Same as before use the params in query options argument.\nopts := ssql.SQLQueryOptions{\n  Table:          \"user\",\n  Fields:         dbFields,\n  WithTotalCount: shouldReturnTotalCount,\n  Params:         \u0026params,\n  Conditions:     conds,\n}\nresult, err := client.Find(ctx, \u0026opts)\n```\n\n[↩](#table-of-contents)\n\n#### Find: Sorting\n\nYou can have one or many sorting configs. The order matters.\n\n```golang\nparams := ssql.Params{\n  SortParams = []*ssql.SortParams{{\n    Direction: \"asc\",\n    Field:     \"hits\",\n  }}\n}\n// Same as before use the params in query options argument.\nopts := ssql.SQLQueryOptions{\n  Table:          \"user\",\n  Fields:         dbFields,\n  WithTotalCount: shouldReturnTotalCount,\n  Params:         \u0026params,\n  Conditions:     conds,\n}\nresult, err := client.Find(ctx, \u0026opts)\n  \n```\n\n[↩](#table-of-contents)\n\n#### Find: Conditions\n\nYou can set one or many conditions which will translate to WHERE clause in the final query.\n\n```golang\n...\nuserIDs := []string{...} // some list of user IDs\n// Add some custom conditions.\nconds := []*ssql.ConditionPair{\n  {\n    Field: \"active\",\n    Value: true,\n    Op:    ssql.OPEqual, // '=' operator.\n  },\n  {\n    Field: \"user_id\",\n    Value: userIDs,\n    Op:    ssql.OPLogicalIn, // Example of \"IN\" operator.\n  },\n}\n// Same as before use the params in query options argument.\nopts := ssql.SQLQueryOptions{\n  Table:          \"user\",\n  Fields:         dbFields,\n  WithTotalCount: shouldReturnTotalCount,\n  Params:         \u0026params,\n  Conditions:     conds,\n}\nresult, err := client.Find(ctx, \u0026opts)\n\n```\n\n[↩](#table-of-contents)\n\n#### Find: GraphQL\n\nAs mentioned in `SuperScan` section, you can use it to return a `pageInfo` object for GraphQL Relay Connections:\n\n```golang\ntype PageInfo struct {\n  HasPreviousPage *bool   `json:\"hasPreviousPage,omitempty\"`\n  HasNextPage     *bool   `json:\"hasNextPage,omitempty\"`\n  StartCursor     *string `json:\"startCursor,omitempty\"`\n  EndCursor       *string `json:\"endCursor,omitempty\"`\n  TotalCount      *int    `json:\"-\"`\n}\n```\nUsing the result, call the `SuperScan` to get you the `PageInfo` object.\n\n```golang\n// First let's get a list of rows.\nresult, err := rp.Client.Find(ctx, \u0026opts)\nif err != nil {\n  log.Logger(ctx).Error().Err(err).Msg(\"Cannot find users\")\n  panic(err)\n}\nvar users []User\npageInfo, err := ssql.SuperScan(\u0026users, result)\nif err != nil {\n  log.Error().Err(err).Msg(\"SuperScan failed\")\n  panic(err)\n}\n```\n\n[↩](#table-of-contents)\n\n### Raw\n\nIf the provided API doesn't satisfy the usage you need, feel free to just run a raw custom query.\n\n```golang\n...\n// Add some custom conditions.\nvalues := []interface{}{true}\nraw := \"SELECT * FROM \\\"user\\\" WHERE active = $1\"\nres, err = client.Raw(ctx, raw, values)\nif err != nil {\n  log.Error().Err(err).Msg(\"Cannot execute raw query\")\n  panic(err)\n}\n...\n```\n\n[↩](#table-of-contents)\n\n### Helpers working with structs\n\nMuch of the headache working with SQL in golang is to have a mapping between your struct fields and SQL columns.\n\nImagine we have type in Go that describes our user object.\n\n```golang\ntype User struct {\n  ID            *string `json:\"id,omitempty\" sql:\"id\"`\n  Username      *string `json:\"username,omitempty\" sql:\"username\"`\n  Email         *string `json:\"email,omitempty\" sql:\"email\"`\n  EmailVerified *bool   `json:\"emailVerified,omitempty\" sql:\"email_verified\"`\n  Active        *bool   `json:\"active,omitempty\" sql:\"active\"`\n  UpdatedAt     *int    `json:\"updatedAt,omitempty\" sql:\"updated_at\"`\n  CreatedAt     *int    `json:\"createdAt,omitempty\" sql:\"created_at\"`\n  DeletedAt     *int    `json:\"deletedAt,omitempty\" sql:\"deleted_at\"`\n}\n\nuser := User{\n  ID: \"...\",\n  Username: \"...\",\n  ...\n}\n```\n\nWe want to `insert` the above user record to our PostgreSQL database.\n\n```golang\n// You can pass any struct as is.\n_, err = client.Insert(ctx, \"user\", newUser)\nif err != nil {\n  panic(err)\n}\n```\n\nThis is because `ssql` internally uses `reflect` package in this case during runtime to get the tags.\n\nHowever for find records (using `Find` method) with query options, you will need to pass the SQL fields themselves as a argument to the `find` \nmethod. This is because `Find` internally avoids using `reflect` and expects the plain text fields to be determined as query options.\n\nTo do this you can use `ExtractStructMappings(tags []string, s interface{})` helper function that simply returns type of:\n\n```golang\ntype TagMappings map[string]map[string]string\n```\n\nIn the following example the first-level map is the tag name requested, and the second-level is the either by tags or by field.\n\n```golang\ntype User struct {\n  ...\n  CreatedAt     *int    `json:\"createdAt,omitempty\" sql:\"created_at\"`\n  ...\n}\n// Request tags for mappings for `sql` and `json`.\nvar userMappingsByTags, userMappingsByFields = ssql.ExtractStructMappings([]string{\"sql\", \"json\"}, model.User{})\n// Get field name by tag name we know.\nuserMappingsByTags[\"sql\"][\"created_at\"] // will return `CreatedAt` (struct field name)\nuserMappingsByTags[\"json\"][\"createdAt\"] // will return `CreatedAt` (struct field name)\n// Get json/sql tag by field name\nuserMappingsByField[\"sql\"][\"CreatedAt\"] // will return `created_at` (sql tag value)\nuserMappingsByField[\"json\"][\"CreatedAt\"] // will return `createdAt` (json tag value)\n```\n\nKnowing this we can use these helper functions to run the expensive extraction of tags/fields only one time during startup\ninstead of for each find since it uses `reflect` internally.\n\nTo do this call `ExtractStructMappings` outside of your insert function/method in the same file for your type, then use it to\npopulate the fields array.\n\n**Note that even though we recommend calling this helper function outside of frequently called functions/methods,\nbut `ExtractStructMappings` still internally caches the result of heavy reflects operations so technically each type in your\ncode base will only use reflect once.**\n\n```golang\n\n// Calling this one time only outside of our function.\nvar _, userMappingsByFields = ssql.ExtractStructMappings([]string{\"rel\", \"sql\"}, model.User{})\n\n...\n\nfunc MyInsertRowFunction(user *User) {\n  dbUserFields := map[string]bool{}\n  // Let's convert our struct type to a map of string that keys are the SQL column names.\n  for fieldName := range user {\n    dbUserFields[userMappingsByFields[\"sql\"][fieldName]] = true\n  }\n  ...\n  opts := ssql.SQLQueryOptions{\n    Table:          \"user\",\n    Fields:         dbFields, // Fields expect a map[string]bool which we now have.\n    ...\n  }\n  // Executing the query.\n  result, err := client.Find(ctx, \u0026opts)\n  if err != nil {\n    log.Error().Err(err).Msg(\"Cannot find users\")\n    panic(err)\n  }\n}\n```\n\n[↩](#table-of-contents)\n\n### Helpers scanning query results\n\nUse `ScanOne` if you are selecting/expecting one result. You can pass your struct pointer as is to scan it\nwithout mapping individual fields like the standard `database/sql` library forces you to. \n\n```golang\nrows, err := client.FindOne(ctx, \"user\", \"id\", \"7f8d1637-ca82-4b1b-91dc-0828c98ebb34\")\nif err != nil {\n  log.Error().Err(err).Msg(\"Cannot select by ID\")\n  panic(err)\n}\nvar resp User\nif err := ssql.ScanOne(\u0026resp, rows); err != nil {\n  log.Error().Err(err).Msg(\"Cannot get resp by ID from Postgres\")\n  panic(err)\n}\n```\n\nUse `ScanRow` inside the `rows.Next()` loop to populate your users array.\n\n```golang\nresult, err := client.Find(ctx, \u0026opts)\nif err != nil {\n  log.Error().Err(err).Msg(\"Cannot find users\")\n  panic(err)\n}\n// Reading through the results.\nvar users []User\nfor result.Rows.Next() {\n  var user User\n  if err := ssql.ScanRow(\u0026user, result.Rows); err != nil {\n    log.Error().Err(err).Msg(\"Cannot scan users\")\n  }\n  users = append(users, user)\n}\n```\n\n[↩](#table-of-contents)\n\n## Roadmap\n\n- [ ] Add support for OR operator.\n- [ ] Add tests.\n- [ ] Add support for LIKE and NOT logical operators.\n- [ ] Add support for timestamp types to be used as cursor fields (not just epoch).\n- [ ] Add full example of GraphQL usage.\n- [ ] Add mock package.\n- [ ] Add benchmarks.\n\n[↩](#table-of-contents)\n\n## Credits\n\n- Thanks to [scany](github.com/georgysavva/scany), `ScanRow` and `ScanOne` are\nactually just wrappers around scany library.\n- Thanks to [this comment](https://github.com/graphql/graphql-relay-js/issues/94#issuecomment-232410564).\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsourcesoft%2Fssql","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsourcesoft%2Fssql","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsourcesoft%2Fssql/lists"}