{"id":19302472,"url":"https://github.com/bitlytwiser/tinyorm","last_synced_at":"2026-05-16T06:03:43.073Z","repository":{"id":65349736,"uuid":"582102539","full_name":"BitlyTwiser/tinyORM","owner":"BitlyTwiser","description":"A tiny package for your basic data layer needs","archived":false,"fork":false,"pushed_at":"2023-11-23T23:18:58.000Z","size":170,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-02-24T01:51:07.842Z","etag":null,"topics":["database","golang","golang-package","object-relational-mapper","object-relational-mapping","orm","orm-framework","orm-library"],"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/BitlyTwiser.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}},"created_at":"2022-12-25T17:25:40.000Z","updated_at":"2023-06-26T01:06:24.000Z","dependencies_parsed_at":"2023-11-24T00:23:46.571Z","dependency_job_id":"77a3ef39-4ef9-4f17-ad8f-4ae6ce54872d","html_url":"https://github.com/BitlyTwiser/tinyORM","commit_stats":{"total_commits":42,"total_committers":2,"mean_commits":21.0,"dds":"0.47619047619047616","last_synced_commit":"9db1954a4c597e7d2840a65acd865addf53034fb"},"previous_names":[],"tags_count":8,"template":false,"template_full_name":null,"purl":"pkg:github/BitlyTwiser/tinyORM","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/BitlyTwiser%2FtinyORM","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/BitlyTwiser%2FtinyORM/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/BitlyTwiser%2FtinyORM/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/BitlyTwiser%2FtinyORM/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/BitlyTwiser","download_url":"https://codeload.github.com/BitlyTwiser/tinyORM/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/BitlyTwiser%2FtinyORM/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":284711515,"owners_count":27050939,"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","status":"online","status_checked_at":"2025-11-16T02:00:05.974Z","response_time":65,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"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":["database","golang","golang-package","object-relational-mapper","object-relational-mapping","orm","orm-framework","orm-library"],"created_at":"2024-11-09T23:22:18.775Z","updated_at":"2025-11-16T13:02:50.734Z","avatar_url":"https://github.com/BitlyTwiser.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# tinyORM [![Go Report Card](https://goreportcard.com/badge/github.com/BitlyTwiser/tinyORM)](https://goreportcard.com/report/github.com/BitlyTwiser/tinyORM)\nA tiny ORM for all of your basic data layer needs\n\n## Premise:\nTinyORM is a functional, small, and simple ORM that was created with simplicity as a primary goal and built only utilizing the standard library and well known database drivers. \n\n## Usage:\n### Connecting:\nTo initiall perform the database connection, the following line can be used:\n```\n  database := \"development\"\n\tdb, err := tinyorm.Connect(database)\n\tif err != nil {\n\t\tt.Fatalf(\"error occurred connecting to database %s. %v\", database, err.Error())\n\t}\n```\nThe database string is any value that is found within the database.yml file.\nAlso see the [multi-tenant](#multi-tenant-connections) section below on utlizing multiple database connections.\n\n### Database YAML\n- Utilizing a simple database.yml file, one can enter multiple database connetions for the ORM to establish connections to.\ni.e. Development, Production, ReadOnly endpoint, FluentD etc..\n\nDatabase.yml:\n```\n---\ndevelopment:\n  dialect: postgres\n  database: development \n  user: devUser \n  connect: true\n  password: devPassword \n  host: 126.0.0.1\n  port: 5431\n\nproduction-read-only:\n  dialect: postgres\n  database: ro \n  user: ro-user \n  password: ro-password \n  connect: false\n  host: 126.0.0.1\n  port: 5431\n```\n\n- Connection without a flag will create a connection to EACH specific connection.\n- if Connect is false, a connection will not be established. If true or missing, a connection will be attempted to the given database. \n- Note: conflicting connection names will not work, only the first connection will be created.\n- i.e.\n```\ndevelopment:\n  dialect: postgres\n  etc..\n\ndevelopment: \n  dialct: mysql\n  etc..\n```\n- Note: only the postgres connection will be established, any repeating connections of the same name will be ignored.\n- Also see the [multi-tenant](#multi-tenant-connections) section below on utlizing multiple database connections.\n### Create:\nThe create functionality will create database records per the given model. All modesl are pluralized, thus they are expected to be passedin as a singular case. If the suffix of the model name is already 's' or pluralized, then the no additional pluralizations are done.\ni.e. User -\u003e Users, but if the pluralized Users is passed, then naming will remain as Users.\n\nIf no ID is present in the model attributes when the Create method is called, an ID will be genereated. \nThis will ONLY occur if the Model itself has an ID attribute. If there is no ID attribute on the Model, no ID is generated. (See TestNoID model in the tests for examples)\n\nExample:\n```\n// The User model utlized here is derrived from the User struct within the tinyorm_test.go\n\n// User will have an ID generated for the asset and have Name John with Age 111\nuser := \u0026User{Name: \"John\", Age: 111}\n  if err := db.Create(user); err != nil {\n    t.Fatalf(\"error updating model. error: %v\", err.Error())\n  }\n\n// User will have the given ID passed into the model attribute, Name Carl, and Age 123\nsecondaryUser := \u0026User{ID: uuid.New(), Name: \"Carl\", Age: \"123\"}\n  if err := db.Create(secondaryUser); err != nil {\n    t.Fatalf(\"error updating model. error: %v\", err.Error())\n  }\n```\n\n### Update:\nUpdate will perform the update operation on the given Model. An ID is expected, in the case of a Model with no ID as a primary key, one is expected to utilize ```Raw``` queries to update these objects.\n\nExample:\n```\nuser := \u0026User{ID: uuid.New(), Name: \"John\", Age: 111}\n\ndb.Create(user) // Create user\n\nuser.Name = \"Carl\"\n\n// The id present within the model is used to lookup the user and perform the update operation\ndb.Update(user) // Update name to Carl\n\n```\n\n### Find:\nFind will either accept a slice of models or a single model. You can pass an ID to Find as the last argument to find a specific value by ID\nIf a slice is passed, the slice is filled with all found assets from the given model. (Note: This could be an expensive operation as this is a SELECT * FROM query (wrapping attributes in a COALESCE function))\nIf no ID is passed and a empty model is passed (not a slice), then the first asset within the given table is pulled \nFind is protected from nil assets via wrapping the attributes in the COALESCE function.\n\nExample:\n```\ntype User struct {\n\tID       uuid.UUID `json:\"id\"`\n\tName     string    `json:\"name\"`\n\tEmail    string    `json:\"email\"`\n\tUsername string    `json:\"username,omitempty\"`\n\tPassword string    `json:\"password\"`\n\tAge      int       `json:\"age,omitempty\"`\n}\n\ntype Users []User\n \nid := uuid.MustParse(\"4c0ea40b-4aeb-4b67-a407-4da25901ec8d\")\nuser := \u0026User{ID: id,  Name: \"Carl\"}\n\ndb.Create(user)\n\nfindUser := new(User) // Create pointer to user struct\ndb.Find(findUser, id)\n\nfmt.Println(user) // Found user, and print out all attributes\n\n// Finding slice of user (i.e. all users in the database)\nusers := new(Users)\ndb.Find(Users)\n\n// Iterate through found users and print out each user model\nfor _, user := range users {\n  fmt.Println(user)\n}\n\n// Find first user:\nattributeUser := new(User) \ndb.Find(attributeUser)\n\nfmt.Println(attributeUser)\n\n```\n### Delete:\nDelete can be used to delete specifc database values \n\nTo utilize delete, you can pass in a model with an ID of the object you wish to delete, or you can pass a model with attributes you wish to match on.\nExample:\n```\ntype User struct {\n  ID uuid.UUID\n  Age int\n  Name string\n}\n\nuser := \u0026Users{Name: \"Carl\"}\n\ndb.Delete(user) // Will delete ALL users with the name of carl.\n\n\nnextUser := \u0026Users{ID: \u003cuuid of user record\u003e}\n\ndb.Delete(nextUser) // Will delete specific user with this ID.\n\ntertiaryUser := \u0026User{Name: \"Bob\", Age: 111}\n\ndb.Delete(tertiaryUser) // Will delete the user bob with age 111\n``` \nA mixing of attributes to fit your deletion needs can be utilized or just specific ID's of the object. You can conjoin the delete functionality with Find/Where to discover a user with a query, then delete said user.\nNote: If the ID is present, the attributes are ignored! Th record with the ID that matches will be deleted\n\n### BulkDelete:\nThough not a common use case, you can delete all values in a table, you can pass an empty slice of a specific model to the BulkDelete function.\n\nExample:\n```\n\ntype Vehicle struct {}\ntype Vehicles []Vehicle\n\nv := new(Vehicles)\n\ndb.BulkDelete(v) // Will delete ALL vehicles.\n```\n### Where:\nWhere is a more advanced utility than Find allowing the user to craft statements that are used to locate objects in the database.\nThe user is expected to pass in a statement and any arguments to be used in conjunction with the statement.\nWhere, like Find, protects against null values by building queries wrapping attributes in the COALESCE function.\nIf a slice is passed to where, the slice will be filled with all scanned rows data.\n\n#### Limit:\nbrief side note about limit, the limit is an int value that is always passed. You can use 0 (or any signed/unsigned int). If the limit is \u003e 0, that will be the total results passed back by the where clause.\n\nExample: \n```\nuser := new(User)\nstmt := \"WHERE name = ?\"\nlimit = 5 // Will limit the return to 5 values\nargs := []any{\"Carl\"}\n\n// Where any user where the name = 'Carl'\nif err := db.Where(user, stmt, limit, args); err != nil {\n  t.Fatalf(\"error updating model. error: %v\", err.Error())\n}\n\n// Prints all user attributes\nfmt.Println(user)\n\n```\n\n### Raw:\nRaw is really just that, a rather raw implementation giving most full control over to the user for building queries.\nNo null value safeguards are in place nor vetting of queries/attributes.\n- When calling raw, you will have the pointer receiver methods available to you: ```All``` and ```Exec```. \n- ```All``` expects a model (or slice of models) and will insert the data into said model. Note: Model MUST be a pointer.\n- ```Exec``` will simply execute a given query and that is all. \n\nExample ```Exec()```:\n```\nquery := \"insert into test_no_ids VALUES($1, $2)\" \nargs := []any{\"Things\", \"TestTest\"}\nif q, err := db.Raw(query, args...); err == nil {\n  if err := q.Exec(); err != nil {\n    t.Fatalf(\"error executing raw query. %s\", err.Error())\n  }\n}\n```\n\nExample ```All()```:\n```\nuser := new(User)\nquery := \"SELECT * FROM users\"\nif q, err := db.Raw(query); err == nil {\n  if err := q.All(user); err != nil {\n    t.Fatalf(\"error executing raw query. %s\", err.Error())\n  }\n}\n```\n\nThe ```stmt``` is the query, followed by arguments to supplament the query with data as needed.\ni.e. ```stmt := \"select * from foo\"```\nExamples of functionality are within the tinyorm_test.go.\n\nRaw:\n- Raw is really just that, a rather raw implementation giving most full control over to the user. No nil value safeguards are in place nor vetting of queries/attributes.\n- When calling raw, you will have the pointer receiver methods available to you: ```All``` and ```Exec```. The rather common nomenclature for ORM's.\n- ```All``` expects a model (or slice of models) and will insert the data into said model. Note: Model MUST be a pointer.\n- ```Exec``` will simply execute a given query and that is all. \n- A snipper of raw functionality can be seen here:\n```\n\t\t\t\tif q, err := db.Raw(test.stmt, test.sliceArgs...); err == nil {\n\t\t\t\t\tif err := q.Exec(); err != nil {\n\t\t\t\t\t\tt.Fatalf(\"error executing raw query. %s\", err.Error())\n\t\t\t\t\t}\n\t\t\t\t}\n```\nThe ```stmt``` is the query, followed by arguments to supplament the query with data as needed.\ni.e. ```stmt := \"select * from foo\"```\nExamples of functionality are within the tinyorm_test.go\n\n## Custom Types:\n- Natively, database/sql does not offer support for slices or maps.\n- To accommodate for these datatypes, the ```custom``` package was added.\n- One can create custom types to utilize within their models akin to the ```Vehicle``` struct found in the tests.\n\nExample:\n```\ntype Vehicle struct {\n\tID            uuid.UUID    `json:\"id\"`\n\tManufacturers custom.Slice `json:\"manufacturers\"`\n\tData          custom.Map   `json:\"data\"`\n\tColor         string       `json:\"color\"`\n\tRecall        bool         `json:\"recall\"`\n}\n\n// Creating a vehicle using the custom types:\n  v := \u0026Vehicle{\n    ID:            uuid.New(),\n    Manufacturers: custom.Slice{},\n    Data:          make(custom.Map),\n    Color:         \"Red\",\n    Recall:        false,\n  }\n```\n\nThe custom types of customer.Slice{} has a built in ```Append``` method for inserting other types into the slice.\nExample:\n```\nv := \u0026Vehicle{\n  ID:            uuid.New(),\n  Manufacturers: custom.Slice{},\n  Data:          make(custom.Map),\n  Color:         \"Red\",\n  Recall:        false,\n}\n\nv.Manufacturers = custom.Slice{\"Ford\", \"Tesla\", \"Mercedes\"}\n\nreturn v\n\n```\n\ncustom.Map also has methods for dealing with the underlying map structure. \n```\nAdd(key string, value any)\nDelete(key strig)\n```\nExample:\n```\nv := \u0026Vehicle{\n  Data:   make(custom.Map),\n  Color:  \"Blue\",\n  Recall: true,\n}\nv.Data.Add(\"Hello Testing\", 123123)\n\nreturn v\n\n```\nmethods exist on the custom.Map type to insert and delete records.\n\nBoth custom.Slice and custom.Map have a ```Values()``` method to return the contents of the data structures.\n\n## Null values:\n- The ```database/sql``` pacakge does not handle nil values in the Scan functionality. The ```Custom``` package does supply the user with the ability to utilize slices and maps, the primary code wraps all queryes for model attributes into a COALESCE statement.\n- If one desire, you can also utilize the SQL package sql.NullStrings, sqlNullBool, etc...\n- You can also utilize a pointer to the asset that may be nil on the model.\n```\ntype TestModel struct {\n  Age int\n  Name *string // Note pointer usage\n}\n```\n- This performs identicall to the sql.NullString implementation. Quoting Russ Cox:\nhttps://groups.google.com/g/golang-nuts/c/vOTFu2SMNeA/m/GB5v3JPSsicJ\n```\nThere is no effective difference.\n``` \n- Operationally, tinyorm handles the nil values by default using the coalesce, this is baked into the application, so the user will not have to accoint for nil values unless you are using the ```Raw``` functionality, no guards are in place there to protect the user.\n\n\n\n## Multi Tenant connections:\n- tinyORM has the ability to connect and keep-alive multiple connections to different databases.\n- Utilizing the multi-connect utility, you can connect to multiple databases and switch between them easily.\n\nExample:\n```\n\tmtc, err := tinyorm.MultiConnect(databaseConnections...)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tif err := mtc.SwitchDB(\"development\").Create(\u0026TestNoID{Stuff: \"More Test PSQL\"}); err != nil {\n\t\tt.Fatalf(\"error creating test on psqlDB. error: %v\", err.Error())\n\t}\n```\n- The above example is pulled from the tinyorm_multitenant_test. \n- Utilizing the ```MultiConnect``` function, you can use the methods built into the dialects.MultiTenantDialectHandler{} struct.\n\n```\ntype MultiTenantDialectHandler struct {\n\tHandlers map[string]DialectHandler\n}\n\n// Append will add database handlers to the Handlers slice\nfunc (mtd *MultiTenantDialectHandler) Set(key string, handler DialectHandler) {\n\tmtd.Handlers[key] = handler\n}\n\n// Empty will determine if there are not database handlers present\nfunc (mtd MultiTenantDialectHandler) Empty() bool {\n\treturn len(mtd.Handlers) == 0\n}\n\n// Switch allows the caller to alter to different databases to perform executions again\nfunc (mtd MultiTenantDialectHandler) SwitchDB(database string) DialectHandler {\n\tif db, found := mtd.Handlers[database]; found {\n\t\treturn db\n\t}\n\n\treturn nil\n}\n```\n\n## Setting timeouts:\ntinyorm allows for the setting of the following database/sql values for open/idle connections:\n```\nSetConnMaxIdleTime\nSetConnMaxLifetime\nSetMaxIdleConns\nSetMaxOpenConns\n```\nThese can be set wtihin the database.yml file per connnection. If left blank, database/sql defaults will be used.\nExample:\n```\ndevelopment:\n  dialect: postgres\n  database: tinyorm\n  user: tiny \n  password: password123!\n  connect: true\n  host: 127.0.0.1\n  port: 5432\n  name: \"development\"\n  maxIdleTime: 60\n  maxLifetime: 100\n  maxIdleConn: 0\n  maxOpenConn: 10\n```\n\n## Package notes:\n- This ORM uses google uuid to generate UUID's for the application, the UUID's may be expected whilst using structs as models\n\n\n## Notes for MYSQL:\n- AS MYSQL does not have UUID as a type, one must ensure they create their columns, if using UUID's as BINARY(36).\ni.e.\n```\n// For users table\ncreate table users (id BINARY(36), name text, email text, username text, password text, age int);\n\n// For vehicles table \ncreate table vehicles (id BINARY(36), manufacturers json, data json, color text, recall bool);\n```\n- this will ensure that the UUID can be marshalled correctly.\n\n## Notes for SQLITE3:\n- This ORM does support the [Auth Feature](https://github.com/mattn/go-sqlite3#user-authentication).\n- One must set the ```Auth``` flag to ```true``` within the database.yml file and compile the application with the auth flag.\n```go build --tags sqlite_userauth```\n- Note: The default ```_auth_crypt``` used to secure the SQLITE password is SHA512\n- Auth is not enabled by default and the flag does have to be used in order for Auth feature to function.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbitlytwiser%2Ftinyorm","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fbitlytwiser%2Ftinyorm","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbitlytwiser%2Ftinyorm/lists"}