{"id":37208166,"url":"https://github.com/kanjih/go-spnr","last_synced_at":"2026-01-14T23:55:47.671Z","repository":{"id":42993444,"uuid":"428842316","full_name":"kanjih/go-spnr","owner":"kanjih","description":"ORM for Cloud Spanner to boost your productivity","archived":false,"fork":false,"pushed_at":"2023-05-08T13:24:09.000Z","size":106,"stargazers_count":21,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"main","last_synced_at":"2024-06-20T05:11:06.640Z","etag":null,"topics":["cloud-spanner","gcp","go","golang","google-cloud","spanner"],"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/kanjih.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":"2021-11-16T23:11:22.000Z","updated_at":"2023-07-28T05:17:03.000Z","dependencies_parsed_at":"2024-06-19T04:11:07.502Z","dependency_job_id":"3331819d-ddb0-4577-9752-8f05bfbbc6eb","html_url":"https://github.com/kanjih/go-spnr","commit_stats":{"total_commits":61,"total_committers":2,"mean_commits":30.5,"dds":"0.47540983606557374","last_synced_commit":"53a2a44c1907c69278686306adba37bfa5f9cf65"},"previous_names":[],"tags_count":3,"template":false,"template_full_name":null,"purl":"pkg:github/kanjih/go-spnr","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kanjih%2Fgo-spnr","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kanjih%2Fgo-spnr/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kanjih%2Fgo-spnr/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kanjih%2Fgo-spnr/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/kanjih","download_url":"https://codeload.github.com/kanjih/go-spnr/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kanjih%2Fgo-spnr/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28439589,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-14T22:37:52.437Z","status":"ssl_error","status_checked_at":"2026-01-14T22:37:31.496Z","response_time":107,"last_error":"SSL_read: 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":["cloud-spanner","gcp","go","golang","google-cloud","spanner"],"created_at":"2026-01-14T23:55:47.014Z","updated_at":"2026-01-14T23:55:47.666Z","avatar_url":"https://github.com/kanjih.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cimg src=\"https://user-images.githubusercontent.com/21299899/142083327-cd45a119-2d2b-4cec-b0d3-4b57d843877f.png\" width=\"400px\"\u003e\n\nORM for Cloud Spanner to boost your productivity 🚀\n\n[![Go Reference](https://pkg.go.dev/badge/github.com/kanjih/go-spnr/v2.svg)](https://pkg.go.dev/github.com/kanjih/go-spnr/v2)\n[![Actions Status](https://github.com/kanjih/go-spnr/workflows/test/badge.svg?branch=main)](https://github.com/kanjih/go-spnr/actions)\n\n\n## Example 🔧\n```go\npackage main\n\nimport (\n\t\"cloud.google.com/go/spanner\"\n\t\"context\"\n\t\"github.com/kanjih/go-spnr\"\n)\n\ntype Singer struct {\n\t// spnr supports 2 types of tags.\n\t// - spanner: spanner column name\n\t// - pk: primary key order\n\tSingerID string `spanner:\"SingerId\" pk:\"1\"`\n\tName     string `spanner:\"Name\"`\n}\n\nfunc main() {\n\tctx := context.Background()\n\tclient, _ := spanner.NewClient(ctx, \"projects/{project_id}/instances/{instance_id}/databases/{database_id}\")\n\n\t// initialize\n\tsingerStore := spnr.New(\"Singers\") // specify table name\n\n\t// save record (spnr supports both Mutation API \u0026 DML!)\n\tsingerStore.ApplyInsertOrUpdate(ctx, client, \u0026Singer{SingerID: \"a\", Name: \"Alice\"})\n\n\t// fetch record\n\tvar singer Singer\n\tsingerStore.Reader(ctx, client.Single()).FindOne(spanner.Key{\"a\"}, \u0026singer)\n\n\t// fetch record using raw query\n\tvar singers []Singer\n\tquery := \"select * from Singers where SingerId=@singerId\"\n\tparams := map[string]any{\"singerId\": \"a\"}\n\tsingerStore.Reader(ctx, client.Single()).Query(query, params, \u0026singers)\n}\n```\n\n## Features\n- Supports both **Mutation API** \u0026 **DML**\n- Supports code generation to map records\n- Supports raw SQLs for complicated cases\n\nspnr is designed ...\n- 🙆‍♂️ for reducing boliderplate codes (i.e. mapping selected records to struct or write simple insert/update/delete operations)\n- 🙅‍♀️ not for hiding queries executed in background (spnr doesn't support abstractions for complicated operations)\n\n## Table of contents\n- [Installation](#installation)\n- spnr APIs\n  - [Read operations](#read-operations)\n  - [Mutation API](#mutation-api)\n  - [DML](#dml)\n- [Embedding](#embedding)\n- [Code generation](#code-generation)\n- [Helper functions](#helper-functions)\n\n## Installation\n```\ngo get github.com/kanjih/go-spnr/v2\n```\n\\* v2 requires go 1.18 or later. If you use previous go versions please use v1.\n\n## Read operations\nspnr provides the following types of read operations 💪\n1. Select records using primary keys\n2. Select one column using primary keys\n3. Select records using query\n4. Select one value using query\n\n### 1. Select records using primary keys\n```go\nvar singer Singer\nsingerStore.Reader(ctx, tx).FindOne(spanner.Key{\"a\"}, \u0026singer)\n\nvar singers []Singer\nkeys := spanner.KeySetFromKeys(spanner.Key{\"a\"}, spanner.Key{\"b\"})\nsingerStore.Reader(ctx, tx).FindAll(keys, \u0026singers)\n```\n\n#### 📝 Note\n`tx` is the transaction object. You can get it by calling `spanner.Client.ReadOnly(ReadWrite)Transaction`, or `spanner.Client.Single` method.\n\n### 2. Select one column using primary keys\n```go\nvar name string\nsingerStore.Reader(ctx, tx).GetColumn(spanner.Key{\"a\"}, \"Name\", \u0026name)\n\nvar names []string\nkeys := spanner.KeySetFromKeys(spanner.Key{\"a\"}, spanner.Key{\"b\"})\nsingerStore.Reader(ctx, tx).GetColumnAll(keys, \"Name\", \u0026names)\n```\n\n#### In the case you want to fetch multiple columns\nMaking temporal struct to map columns is the best solution.\n```go\ntype cols struct {\n  Name string `spanner:\"Name\"`\n  Score spanner.NullInt64 `spanner:\"Score\"`\n}\nvar res cols\nsingerStore.Reader(ctx, tx).FindOne(spanner.Key{\"1\"}, \u0026res)\n```\n\n### 3. Select records using query\n```go\nvar singer Singer\nquery := \"select * from `Singers` where SingerId=@singerId\"\nparams := map[string]any{\"singerId\": \"a\"}\nsingerStore.Reader(ctx, tx).QueryOne(query, params, \u0026singer)\n\nvar singers []Singer\nquery = \"select * from Singers\"\nsingerStore.Reader(ctx, tx).Query(query, nil, \u0026singers)\n```\n\n### 4. Select one value using query\n```go\nvar cnt int64\nquery := \"select count(*) as cnt from Singers\"\nsingerStore.Reader(ctx, tx).QueryValue(query, nil, \u0026cnt)\n```\n\n### * Notes\n- `FindOne`, `GetColumn` method uses `ReadRow` method of `spanner.ReadWrite(ReadOnly)Transaction`.\n- `FindAll`, `GetColumnAll` method uses `Read` method.\n- `QueryOne`, `Query` Method uses `Query` method.\n\n## Mutation API\nExecuting mutation API using spnr is badly simple! Here's the example 👇\n```go\nsinger := \u0026Singer{SingerID: \"a\", Name: \"Alice\"}\nsingers := []Singer{{SingerID: \"b\", Name: \"Bob\"}, {SingerID: \"c\", Name: \"Carol\"}}\n\nsingerStore := spnr.New(\"Singers\") // specify table name\n\nsingerStore.InsertOrUpdate(tx, singer)  // Insert or update\nsingerStore.InsertOrUpdate(tx, \u0026singers) // Insert or update multiple records\n\nsingerStore.Update(tx, singer)  // Update\nsingerStore.Update(tx, \u0026singers) // Update multple records\n\nsingerStore.Delete(tx, singer)  // Delete\nsingerStore.Delete(tx, \u0026singers) // Delete multiple records\n```\n\nDon't want to use in transaction? You can use `ApplyXXX`.\n```go\nsingerStore.ApplyInsertOrUpdate(ctx, client, singer) // client is spanner.Dataclient\nsingerStore.ApplyDelete(ctx, client, \u0026singers)\n```\n\n## DML\nspnr parses struct then build DML 💪\n```go\nsinger := \u0026Singer{SingerID: \"a\", Name: \"Alice\"}\nsingers := []Singer{{SingerID: \"b\", Name: \"Bob\"}, {SingerID: \"c\", Name: \"Carol\"}}\n\nsingerStore := spnr.NewDML(\"Singers\") // specify table name\n\nsingerStore.Insert(ctx, tx, singer)\n// -\u003e INSERT INTO `Singers` (`SingerId`, `Name`) VALUES (@SingerId, @Name)\n\nsingerStore.Insert(ctx, tx, \u0026singers)\n// -\u003e INSERT INTO `Singers` (`SingerId`, `Name`) VALUES (@SingerId_0, @Name_0), (@SingerId_1, @Name_1)\n\nsingerStore.Update(ctx, tx, singer)\n// -\u003e UPDATE `Singers` SET `Name`=@Name WHERE `SingerId`=@w_SingerId\nsingerStore.Update(ctx, tx, \u0026singers)\n// -\u003e UPDATE `Singers` SET `Name`=@Name WHERE `SingerId`=@w_SingerId\n// -\u003e UPDATE `Singers` SET `Name`=@Name WHERE `SingerId`=@w_SingerId\n\nsingerStore.Delete(ctx, tx, singer)\n// -\u003e DELETE FROM `Singers` WHERE `SingerId`=@w_SingerId\n```\n\n### Want to use raw SQL?\nYou don't need spnr in this case! Plain spanner SDK is enough.\n```go\nsql := \"UPDATE `Singers` SET `Name` = xx WHERE `Id` = @Id\"\nparams := map[string]any\nspannerClient.Update(tx, spanner.Statement{SQL: sql, Params: params})\n```\n\n## Embedding\nspnr is also designed to use with embedding.\u003cbr/\u003e\nYou can make structs to manipulate records for each table \u0026 can add any methods you want.\n\n```go\ntype SingerStore struct {\n\tspnr.DML // use spnr.Mutation for mutation API\n}\n\nfunc NewSingerStore() *SingerStore {\n\treturn \u0026SingerStore{DML: *spnr.NewDML(\"Singers\")}\n}\n\n// Any methods you want to add\nfunc (s *SingerStore) GetCount(ctx context.Context, tx spnr.Transaction, cnt any) error {\n\tquery := \"select count(*) as cnt from Singers\"\n\treturn s.Reader(ctx, tx).Query(query, nil, \u0026cnt)\n}\n\nfunc useSingerStore(ctx context.Context, client *spanner.Client) {\n\tsingerStore := NewSingerStore()\n\n\tclient.ReadWriteTransaction(ctx, func(ctx context.Context, tx *spanner.ReadWriteTransaction) error {\n\t\t// You can use all operations that spnr.DML has \n\t\tsingerStore.Insert(ctx, tx, \u0026Singer{SingerID: \"a\", Name: \"Alice\"})\n\t\tvar singer Singer\n\t\tsingerStore.Reader(ctx, tx).FindOne(spanner.Key{\"a\"}, \u0026singer)\n\n\t\t// And you can use the methods you added !!\n\t\tvar cnt int\n\t\tsingerStore.GetCount(ctx, tx, \u0026cnt)\n\n\t\treturn nil\n\t})\n}\n```\n\n## Code generation\nTired to write struct code to map records for every table?\u003cbr/\u003e\nDon't worry! spnr provides code generation 🚀\n```sh\ngo install github.com/kanjih/go-spnr/cmd/spnr@latest\nspnr build -p {PROJECT_ID} -i {INSTANCE_ID} -d {DATABASE_ID} -n {PACKAGE_NAME} -o {OUTPUT_DIR}\n```\n\n## Helper functions\nspnr provides some helper functions to reduce boilerplates.\n- **`NewNullXXX`**\n  - `spanner.NullString{StringVal: \"a\", Valid: true}` can be `spnr.NewNullString(\"a\")`\n- **`ToKeySets`**\n  - You can convert slice to keysets using `spnr.ToKeySets([]string{\"a\", \"b\"})`\n\nLove reporting issues! \n\n[godev-image]: https://pkg.go.dev/badge/github.com/kanjih/go-spnr\n[godev-url]: https://pkg.go.dev/github.com/kanjih/go-spnr\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkanjih%2Fgo-spnr","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fkanjih%2Fgo-spnr","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkanjih%2Fgo-spnr/lists"}