{"id":13412163,"url":"https://github.com/galeone/igor","last_synced_at":"2026-03-18T02:35:25.476Z","repository":{"id":57486701,"uuid":"53592091","full_name":"galeone/igor","owner":"galeone","description":"igor is an abstraction layer for PostgreSQL with a gorm like syntax.","archived":false,"fork":false,"pushed_at":"2024-04-14T16:58:48.000Z","size":159,"stargazers_count":123,"open_issues_count":0,"forks_count":4,"subscribers_count":10,"default_branch":"master","last_synced_at":"2024-05-02T02:12:45.668Z","etag":null,"topics":["abstraction","dbms","go","golang","gorm","igor","postgresql","prepared-statements"],"latest_commit_sha":null,"homepage":"","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/galeone.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":".github/FUNDING.yml","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},"funding":{"github":["galeone"],"patreon":null,"open_collective":null,"ko_fi":null,"tidelift":null,"community_bridge":null,"liberapay":null,"issuehunt":null,"otechie":null,"custom":null}},"created_at":"2016-03-10T14:45:08.000Z","updated_at":"2024-06-18T19:48:26.310Z","dependencies_parsed_at":"2023-12-25T10:43:45.298Z","dependency_job_id":"c93da168-708d-4de7-ab2a-6d884b486aa9","html_url":"https://github.com/galeone/igor","commit_stats":null,"previous_names":[],"tags_count":14,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/galeone%2Figor","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/galeone%2Figor/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/galeone%2Figor/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/galeone%2Figor/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/galeone","download_url":"https://codeload.github.com/galeone/igor/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":230438185,"owners_count":18225870,"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":["abstraction","dbms","go","golang","gorm","igor","postgresql","prepared-statements"],"created_at":"2024-07-30T20:01:21.682Z","updated_at":"2026-03-18T02:35:20.446Z","avatar_url":"https://github.com/galeone.png","language":"Go","funding_links":["https://github.com/sponsors/galeone"],"categories":["Database","数据库","Generators","\u003cspan id=\"数据库-database\"\u003e数据库 Database\u003c/span\u003e","Data Integration Frameworks","数据库  `go语言实现的数据库`","Uncategorized"],"sub_categories":["SQL Query Builders","SQL查询生成器","Advanced Console UIs","\u003cspan id=\"高级控制台用户界面-advanced-console-uis\"\u003e高级控制台用户界面 Advanced Console UIs\u003c/span\u003e","SQL 查询语句构建库"],"readme":"# igor\nigor is an abstraction layer for PostgreSQL, written in Go. Igor syntax is (almost) compatible with [GORM](https://github.com/jinzhu/gorm \"The fantastic ORM library for Golang, aims to be developer friendly\").\n\n[![GoDoc](https://godoc.org/github.com/galeone/igor?status.svg)](https://godoc.org/github.com/galeone/igor)\n[![Build Status](https://travis-ci.org/galeone/igor.svg?branch=master)](https://travis-ci.org/galeone/igor)\n\n## When to use igor\nYou should use igor when your DBMS is PostgreSQL and you want to place an abstraction layer on top of it and do CRUD operations in a smart, easy, secure and fast way.\n\nThus with igor you __do not__ create a new schema. In general igor does not support DDL (you can do it with the `Raw` and `Exec`, but there are not method created ad-hoc for this purpose).\n\n## What igor does\n- Always uses prepared statements: no sql injection and good performance.\n- Supports transactions\n- Supports PostgreSQL JSON and JSONB types with `igor.JSON`\n- Supports PostgreSQL [LISTEN/NOTIFY](http://www.postgresql.org/docs/current/static/sql-notify.html)\n- Uses a GORM like syntax\n- Uses the same logic in insertion and update: handle default values in a coherent manner\n- Uses GORM models and conventions (partially, see [Differences](#differences))\n- Exploits PostgreSQL `RETURNING` statement to update models fields with the updated values (even when changed on db side; e.g. when having a default value)\n- Automatically handle reserved keywords when used as a table name or fields. Does not quote every field (that's not recommended) but only the ones conflicting with a reserved keyword.\n \n\n## What igor is not\n- An ORM (and thus a complete GORM replacement):\n  - Does not support associations\n  - Does not support callbacks\n  - Does not have any specific method for data migration and DDL operations\n  - Does not support soft delete\n\n## Install\n```go\ngo get -u github.com/galeone/igor\n```\n\n## GORM compatible\nigor uses the same syntax of GORM. Thus in a great number of cases you can replace GORM with igor by only changing the import path.\n\n__Warning__: igor is not a complete GORM replacement. See the [Differences](#differences).\n\n## Model definition\nModels are the [same used in GORM.](http://jinzhu.me/gorm/models.html#model-definition)\n\nThe main differences are:\n\n- Igor does not handle associations. Thus, if you have a field that refers to another table, disable it with the annotation `sql:\"-\"` (see the code below).\n- Every model __must__ implement the `igor.DBTable` interface. Therefore every model must have the method `TableName() string`, that returns the table name associated with the model.\n- Every model __must__ explicit the primary key field (using the tag `igor:\"primary_key\"`).\n- Since igor does not deal with DDL, `sql:\"type:\u003ctype\u003e\"` is ignored.\n\nLike:\n\n```go\ntype User struct {\n\tCounter uint64 `igor:\"primary_key\"`\n    Username string\n    Password string\n    Name string\n    Surname string\n    Profile Profile `sql:\"-\"`\n}\n\ntype (User) TableName() string {\n    return \"users\"\n}\n```\n\n### Array support\n\nigor supports PostgreSQL fields natively, without the need to use the `pg.Array` type - you can use just plain structs.\n\n```go\ntype NestMe struct {\n\tID            int64 `igor:\"primary_key\"`\n\tSliceOfString []string\n\tSliceOfInt64  []int64\n}\n```\n\nThis structure maps this table definition:\n\n```sql\nCREATE TABLE nest_table(\n    id bigserial not null PRIMARY KEY,\n    slice_of_string text[] not null,\n    slice_of_int64 bigint[] not null\n)\n```\n\n### Nested types support\n\nigor allows you to embed types, and overwrite fields of the inner type. In particular, you can add the `sql` decorator (or change type, potentially).\n\n\n```go\ntype NestMe struct {\n\tID            int64 `igor:\"primary_key\"`\n\tOverwriteMe   int64\n\tSliceOfString []string\n\tSliceOfInt64  []int64\n}\n\ntype NestTable struct {\n\tNestMe\n\tOverwriteMe int64 `sql:\"-\"`\n}\nfunc (NestTable) TableName() string {\n\treturn \"nest_table\"\n}\n```\n\nThe `NestTable` type disables the SQL generation for the field `OverwriteMe` that's present in the embedded type `NestMe`.\n\n## Methods\n- [igor](#igor)\n  - [When to use igor](#when-to-use-igor)\n  - [What igor does](#what-igor-does)\n  - [What igor is not](#what-igor-is-not)\n  - [Install](#install)\n  - [GORM compatible](#gorm-compatible)\n  - [Model definition](#model-definition)\n    - [Array support](#array-support)\n    - [Nested types support](#nested-types-support)\n  - [Methods](#methods)\n    - [Connect](#connect)\n    - [Log](#log)\n    - [Model](#model)\n    - [Joins](#joins)\n    - [Table](#table)\n    - [CTE](#cte)\n    - [Select](#select)\n    - [Where](#where)\n    - [Create](#create)\n    - [Delete](#delete)\n    - [Updates](#updates)\n    - [Pluck](#pluck)\n    - [Count](#count)\n    - [First](#first)\n    - [Scan](#scan)\n    - [Raw](#raw)\n    - [Exec](#exec)\n    - [Where](#where-1)\n    - [Limit](#limit)\n    - [Offset](#offset)\n    - [Order](#order)\n    - [DB](#db)\n    - [Begin](#begin)\n    - [Commit](#commit)\n    - [Rollback](#rollback)\n    - [Listen](#listen)\n    - [Unlisten](#unlisten)\n    - [UnlistenAll](#unlistenall)\n    - [Notify](#notify)\n  - [Differences](#differences)\n    - [Select and Where call order](#select-and-where-call-order)\n    - [Models](#models)\n    - [Open method](#open-method)\n    - [Logger](#logger)\n    - [Methods return value](#methods-return-value)\n    - [Scan and Find methods](#scan-and-find-methods)\n    - [Scan](#scan-1)\n    - [Delete](#delete-1)\n    - [First](#first-1)\n  - [Other](#other)\n    - [JSON and JSONB support](#json-and-jsonb-support)\n    - [LISTEN / NOTIFY support](#listen--notify-support)\n    - [Contributing](#contributing)\n    - [Testing](#testing)\n    - [License](#license)\n    - [About the author](#about-the-author)\n\n### Connect\n```go\nimport \"github.com/galeone/igor\"\n\nfunc main() {\n  db, err := igor.Connect(\"user=galeone dbname=igor sslmode=disable\")\n}\n```\n\n### Log\nSee: [Logger](#logger).\n\n### Model\n`Model(DBModel)` sets the table name for the current query\n\n```go\nvar logged bool\nvar counter uint64\n\ndb.Model(User{}).Select(\"login(?, ?) AS logged, counter\", username, password).Where(\"LOWER(username) = ?\", username).Scan(\u0026logged, \u0026counter);\n```\n\nit generates:\n```sql\nSELECT login($1, $2) AS logged, counter FROM users WHERE LOWER(username) = $3 ;\n```\n\n### Joins\nJoins append the join string to the current model\n\n```go\ntype Post struct {\n\tHpid    uint64    `igor:\"primary_key\"`\n\tFrom    uint64\n\tTo      uint64\n\tPid     uint64    `sql:\"default:0\"`\n\tMessage string\n\tTime    time.Time `sql:\"default:(now() at time zone 'utc')\"`\n\tLang    string\n\tNews    bool\n\tClosed  bool\n}\n\ntype UserPost struct {\n\tPost\n}\n\nfunc (UserPost) TableName() string {\n    return \"posts\"\n}\n\nusers := new(User).TableName()\nposts := new(UserPost).TableName()\n\nvar userPosts []UserPost\ndb.Model(UserPost{}).Order(\"hpid DESC\").\n    Joins(\"JOIN \"+users+\" ON \"+users+\".counter = \"+posts+\".to\").\n    Where(`\"to\" = ?`, user.Counter).Scan(\u0026userPost)\n```\n\nit generates:\n```sql\nSELECT posts.hpid,posts.\"from\",posts.\"to\",posts.pid,posts.message,posts.\"time\",posts.lang,posts.news,posts.closed\nFROM posts\nJOIN users ON users.counter = posts.to\nWHERE \"to\" = $1\n```\n\n### Table\nTable appends the table string to FROM. It has the same behavior of Model, but passing the table name directly as a string\n\nSee example in [Joins](#joins)\n\n### CTE\nCTE allows to define a Common Table Expression that precedes the query.\n\n__Warning__: use it with the [Table](#table) method.\n\n```go\nvar usernames []string\nvar ids []uint64 // fill them - not the usage of = any since this is converted to a pq.Array\n\ndb.CTE(`WITH full_users_id AS (\nSELECT counter FROM users WHERE name = ? AND counter = any(?))`, \"Paolo\", ids).\nTable(\"full_users_id as fui\").\nSelect(\"username\").\nJoins(\"JOIN users ON fui.counter = users.counter\").Scan(\u0026usernames)\n```\n\nit generates:\n```sql\nWITH full_users_id AS (\n\tSELECT counter FROM users WHERE name = $1 AND counter = any($2)\n)\nSELECT username FROM full_users_id as fui JOIN users ON fui.counter = users.counter;\n```\n\n### Select\nSelect sets the fields to retrieve. Appends fields to SELECT (See example in [Model](#model)).\n\nWhen select is not specified, every field is selected in the Model order (See example in [Joins](#joins)).\n\n__Warning__: calling `Select` using parameters without type is allowed only if the stored procedure on the DBMS define the type.\n\nEg: if we have a function on PostgreSQL that accepts two parameters like\n```pgsql\nlogin(_username text, _pass text, OUT ret boolean) RETURNS boolean\n```\nwe can call this function in that way\n\n```go\ndb.Select('login(?,?)', username, password)\n```\n\nBut, if the DBMS can't infer the parameters (in every other case except the one previous mentioned), we __must__ make parameters type explicit.\n\nThis is due to the use of prepared statements.\n\n```go\ndb.Select(\"?::int, ?::int, ?::int\", 1, 2, 3)\n```\n\n### Where\nWhere works with `DBModel`s or strings.\n\nWhen using a `DBModel`, if the primary key fields is not blank, the query will generate a where clause in the form:\n\nThus:\n\n```go\ndb.Model(UserPost{}).Where(\u0026UserPost{Hpid: 1, From:1, To:1})\n```\n\nit generates:\n\n```sql\nSELECT posts.hpid,posts.\"from\",posts.\"to\",posts.pid,posts.message,posts.\"time\",posts.lang,posts.news,posts.closed\nFROM posts\nWHERE posts.hpid = $1\n```\n\nIgnoring values that are not primary keys.\n\nIf the primary key field is blank, generates the where clause `AND`ing the conditions:\n\n```go\ndb.Model(UserPost{}).Where(\u0026UserPost{From:1, To:1})\n```\n\nThe conditions will be:\n\n```sql\nWHERE posts.from = $1 AND posts.to = $2\n```\n\nWhen using a string, you can use the `?` as placeholder for parameters substitution. Thus\n\n```go\ndb.Model(UserPost{}).Where(`\"to\" = ?`, user.Counter)\n```\n\nit generates:\n\n ```sql\nSELECT posts.hpid,posts.\"from\",posts.\"to\",posts.pid,posts.message,posts.\"time\",posts.lang,posts.news,posts.closed\nFROM posts\nWHERE \"to\" = $1\n```\n\nWhere supports slices as well:\n\n```go\ndb.Model(UserPost{}).Where(`\"to\" IN (?) OR \"from\" = ?`, []uint64{1,2,3,4,6}, 88)\n```\n\nit generates:\n\n```sql\nSELECT posts.hpid,posts.\"from\",posts.\"to\",posts.pid,posts.message,posts.\"time\",posts.lang,posts.news,posts.closed\nFROM posts\nWHERE \"to\" IN ($1,$2,$3,$4,$5) OR \"from\" = $6\n```\n\n### Create\nCreate `INSERT` a new row into the table specified by the DBModel.\n\n`Create` handles default values using the following rules:\n\nIf a field is blank and has a default value and this default value is the Go Zero value for that field, igor does not generate the query part associated with the insertion of that fields (let the DBMS handle the default value generation).\n\nIf a field is blank and has a default value that's different from the Go Zero value for that filed, insert the specified default value.\n\nCreate exploits the `RETURNING` clause of PostgreSQL to fetch the new row and update the DBModel passed as argument.\n\nIn that way igor always have the up-to-date fields of DBModel.\n\n```go\npost := \u0026UserPost{\n    From: 1,\n    To: 1,\n    Pid: 10,\n    Message: \"hi\",\n    Lang: \"en\",\n}\ndb.Create(post)\n```\n\nit generates:\n\n```sql\nINSERT INTO posts(\"from\",\"to\",pid,message,lang) VALUES ($1,$2,$3,$4,$5)  RETURNING posts.hpid,posts.\"from\",posts.\"to\",posts.pid,posts.message,posts.\"time\",posts.lang,posts.news,posts.closed;\n```\n\nThe resulting row (the result of `RETURNING`) is used as a source for the  `Scan` method, having the DBModel as argument.\n\nThus, in the example, the variable post.Time has the `(now() at time zone 'utc')` evaluation result value.\n\n### Delete\n\nSee [Delete](#delete-1)\n\n### Updates\n\nUpdates uses the same logic of [Create](#create) (thus the default value handling is the same).\n\nThe only difference is that Updates `UPDATE` rows.\n\n`Update` tries to infer the table name from the DBModel passed as argument __if__ a `Where` clause has not been specified. Otherwise uses the `Where` clause to generate the `WHERE` part and the Model to generate the `field = $n` part.\n\n```go\nvar user User\ndb.First(\u0026user, 1) // handle errors\nuser.Username = \"username changed\"\n\ndb.Updates(\u0026user)\n```\n\nit generates:\n\n```sql\nUPDATE users SET users.username = \"username changed\" WHERE users.counter = 1 RETURNING users.counter,users.last,users.notify_story,users.private,users.lang,users.username,users.email,users.name,users.surname,users.gender,users.birth_date,users.board_lang,users.timezone,users.viewonline,users.registration_time\n```\n\nThe `RETURNING` clause is handled in the same manner of [Create](#create).\n\n### Pluck\nPluck fills the slice with the query result.\nIt calls `Scan` internally, thus the slice can be a slice of structures or a slice of simple types.\n\nIt panics if slice is not a slice or the query is not well formulated.\n\n```go\ntype Blacklist struct {\n\tFrom       uint64\n\tTo         uint64\n\tMotivation string\n\tTime       time.Time `sql:\"default:(now() at time zone 'utc')\"`\n\tCounter    uint64    `igor:\"primary_key\"`\n}\n\nfunc (Blacklist) TableName() string {\n\treturn \"blacklist\"\n}\n\nvar blacklist []uint64\ndb.Model(Blacklist{}).Where(\u0026Blacklist{From: user.Counter}).Pluck(`\"to\"`, \u0026blacklist)\n```\n\nit generates\n\n```sql\nSELECT \"to\" FROM blacklist WHERE blacklist.\"from\" = $1\n```\n\n### Count\nCount sets the query result to be count(*) and scan the result into value.\n\n```go\nvar count int\ndb.Model(Blacklist{}).Where(\u0026Blacklist{From: user.Counter}).Count(\u0026count\n```\n\nit generates:\n\n```sql\nSELECT COUNT(*) FROM blacklist WHERE blacklist.\"from\" = $1\n```\n\n### First\n\nSee [First](#first-1)\n\n### Scan\n\nSee [Scan and Find methods](#scan-and-find-methods)\n\n### Raw\n\nPrepares and executes a raw query, the results is available for the Scan method.\n\nSee [Scan and Find methods](#scan-and-find-methods)\n\n### Exec\n\nPrepares and executes a raw query, the results is discarded. Useful when you don't need the query result or the operation have no result.\n\n```go\ntx := db.Begin()\ntx.Exec(\"DROP TABLE IF EXISTS users\")\ntx.Exec(`CREATE TABLE users (\n\tcounter bigint NOT NULL,\n\tlast timestamp without time zone DEFAULT timezone('utc'::text, now()) NOT NULL,\n\tnotify_story jsonb,\n\tprivate boolean DEFAULT false NOT NULL,\n\tlang character varying(2) DEFAULT 'en'::character varying NOT NULL,\n\tusername character varying(90) NOT NULL,\n\tpassword character varying(60) NOT NULL,\n\tname character varying(60) NOT NULL,\n\tsurname character varying(60) NOT NULL,\n\temail character varying(350) NOT NULL,\n\tgender boolean NOT NULL,\n\tbirth_date date NOT NULL,\n\tboard_lang character varying(2) DEFAULT 'en'::character varying NOT NULL,\n\ttimezone character varying(35) DEFAULT 'UTC'::character varying NOT NULL,\n\tviewonline boolean DEFAULT true NOT NULL,\n\tremote_addr inet DEFAULT '127.0.0.1'::inet NOT NULL,\n\thttp_user_agent text DEFAULT ''::text NOT NULL,\n\tregistration_time timestamp(0) with time zone DEFAULT now() NOT NULL\n\t)`)\ntx.Commit()\n```\n\n`Exec` does not use prepared statements if there are no parameters to replace in the query. This make it possible to use a single call to `Exec` to execute multiple statements `;`-terminated. e.g.\n\n```go\ntx.Exec(`DROP TABLE IF EXISTS users;\n        CREATE TABLE users (\n        counter bigint NOT NULL,\n        ...\n    )`)\ntx.Commit()\n```\n\n### Where\nWhere builds the WHERE clause.\n\nIf a primary key is present in the struct passed as argument only that field is used.\n\n```go\nuser.Counter = 2\nuser.Name = \"paolo\"\ndb.Select(\"username\").Where(\u0026user)\n```\n\nit generates:\n\n```sql\nSELECT username FROM users WHERE users.counter = $1\n```\n\nbecause `Counter` is the primary key.\n\nIf the primary key is blank every non empty field is and-end.\n\n```go\nuser.Counter = 0 // 0 is a blank primary key\n```\n\nit generates\n\n```sql\nSELECT username FROM users WHERE users.name = $1\n```\n\nYou can use a string to build the where clause and pass parameters if needed.\n\n```go\ndb.Model(User{}).Select(\"username\").Where(\"counter IN (?) AND name ILIKE ?\",[]uint64{1,2,4,5}, \"nino\")\n\n```\n\nit generates:\n\n```sql\nSELECT username FROM users WHERE counter in ($1,$2,$3,$4) AND name ILIKE $5\n```\n\nIf a where condition can't be generated, Where panics\n\n### Limit\nLimit sets the LIMIT value to the query\n\n### Offset\nOffset sets the OFFSET value to the query\n\n### Order\nOrder sets the ORDER BY value to the query\n\n### DB\nDB returns the current `*sql.DB`. It panics if called during a transaction\n\n### Begin\nBegin initialize a transaction. It panics if begin has been already called.\n\nIl returns a `*igor.Database`, thus you can use every other `*Database` method on the returned value.\n\n```go\ntx := db.Begin()\n```\n\n### Commit\nCommit commits the transaction. It panics if the transaction is not started (you have to call Begin before)\n\n```go\ntx.Create(\u0026user)\ntx.Commit()\n// Now you can use the db variable again\n```\n\n### Rollback\nRollback rollbacks the transaction. It panics if the transaction is not started (you have to call Begin before\n\n```go\nif e := tx.Create(\u0026user); e != nil {\n    tx.Rollback()\n} else {\n    tx.Commit()\n}\n// Now you can use the db variable again\n\n```\n\n### Listen\nListen executes `LISTEN channel`. Uses f to handle received notifications on channel.\n\n```go\nif e := db.Listen(\"notification_channel\", func(payload ...string) {\n    if len(payload) \u003e 0 {\n        pl := strings.Join(payload, \",\")\n        fmt.Println(\"Received notification on channel notification_channel, having payload: \" + pl)\n    } else {\n        fmt.Println(\"Received notification on channel notification_channel without payload\")\n    }\n}); e != nil {\n    // handle error\n}\n```\n\n### Unlisten\nUnlisten executes`UNLISTEN channel`. Unregister function f, that was registered with Listen(channel ,f).\n\n```go\ne := db.Unlisten(\"notification_channel\")\n// handle error\n```\n\nYou can unlisten from every channel calling `db.Unlisten(\"*\")` or using the method `UnlistenAll`\n\n### UnlistenAll\nUnlistenAll executes `UNLISTEN *`. Thus do not receive any notification from any channel.\n\n### Notify\nWith Notify you can send a notification with or without payload on a channel.\n\n```go\ne = db.Notify(\"notification_channel\") // empty payload\ne = db.Notify(\"notification_channel\", \"payload 1\", \"payload 2\", \"test\")\n```\n\nWhen sending a payload, the strings are joined together. Therefore the payload sent with previous call to `Notify` is: `payload 1, payload 2, test`\n\n## Differences\n\n### Select and Where call order\nIn GORM, you can execute\n```go\ndb.Model(User{}).Select(\"username\")\n```\n\n```go\ndb.Select(\"username\").Model(User{})\n```\n\nand achieve the same result.\n\nIn igor this is not possible. You __must__ call `Model` before `Select`.\n\nThus always use: \n\n```go\ndb.Model(User{}).Select(\"username\")\n```\n\nThe reason is that igor generates queries in the form `SELECT table.field1, table.filed2 FROM table [WHERE] RETURNING  table.field1, table.filed2`.\n\nIn order to avoid ambiguities when using `Joins`, the `RETURNING` part of the query must be in the form `table.field1, table.filed2, ...`, and table is the `TableName()` result of the `DBModel` passed as `Model` argument.\n\n### Models\nIgor models are __the same__ as GORM models (except that you have to use the `igor` tag field to define the primary key). The `sql` tag field is used to define default value and column value. Eg:\n\n```go\ntype Test struct {\n    ID      uint64 `igor:\"primary_key column:id_column\"`\n    Time    time.Time `sql:\"default:(now() at time zone 'utc')\"`\n}\n```\n\nThe other main difference is that igor models require the implementation of the `DBModel` interface.\n\nIn GORM, you can optionally define the `TableName` method on your Model. With igor this is mandatory.\n\nThis constraint gives to igor the ability to generate conditions (like the `WHERE` or `INSERT` or `UPDATE` part of the query) that have a counter part on DB size for sure.\n\nIf a type does not implement the `DBModel` interface your program will not compile (and thus you can easily find the error and fix it). Otherwise igor could generate a wrong query and we're trying to avoid that.\n\n### Open method\nSince igor is PostgreSQL only, the `gorm.Open` method has been replaced with\n\n```go\nConnect(connectionString string) (*Database, error)\n```\n\n### Logger\nThere's no `db.LogMode(bool)` method in igor. If you want to log the prepared statements, you have to manually set a logger for igor.\n\n```go\nlogger := log.New(os.Stdout, \"query-logger: \", log.LUTC)\ndb.Log(logger)\n```\n\nIf you want to disable the logger, set it to nil\n\n```go\ndb.Log(nil)\n```\n\nPrivacy: you'll __never__ see the values of the variables, but only the prepared statement and the PostgreSQL placeholders. Respect your user privacy, do not log user input (like credentials).\n\n### Methods return value\nIn GORM, every method (even the ones that execute queries) returns a `*DB`.\n\nIn igor:\n\n- methods that execute queries returns `error`\n- methods that build the query returns `*Database`, thus you can chain the methods (GORM-like) and build the query.\n\n### Scan and Find methods\nIn GORM, `Scan` method is used to scan query results into a struct. The `Find` method is almost the same.\n\nIn igor:\n- `Scan` method executes the `SELECT` query. Thus return an error if `Scan` fails (see the previous section).\n  \n  `Scan` handle every type. You can scan query results in:\n   - slice of struct `.Scan(\u0026sliceOfStruct)`\n   - single struct `.Scan(\u0026singleStruct)`\n   - single value `.Scan(\u0026integerType)`\n   - a comma separated list of values (because `Scan` is a variadic arguments function) `.Scan(\u0026firstInteger, \u0026firstString, \u0026secondInteger, \u0026floatDestination)`\n\n- `Find` method does not exists, is completely replaced by `Scan`.\n\n### Scan\nIn addiction to the previous section, there's another difference between GORM ad igor.\n\n`Scan` method __do not__ scans the selected fields into results using the selected fields name, but uses the order (to increase the performance).\n\nThus, having:\n```go\ntype Conversation struct {\n\tFrom   string    `json:\"from\"`\n\tTime   time.Time `json:\"time\"`\n\tToRead bool      `json:\"toRead\"`\n}\n\nvar convList []Conversation\nerr := Db().Raw(`SELECT DISTINCT otherid, MAX(times) as \"time\", to_read FROM (\n    (SELECT MAX(\"time\") AS times, \"from\" as otherid, to_read FROM pms WHERE \"to\" = ? GROUP BY \"from\", to_read)\n    UNION\n    (SELECT MAX(\"time\") AS times, \"to\" as otherid, FALSE AS to_read FROM pms WHERE \"from\" = ? GROUP BY \"to\", to_read)\n) AS tmp GROUP BY otherid, to_read ORDER BY to_read DESC, \"time\" DESC`, user.Counter, user.Counter).Scan(\u0026convList)\n```\n\nDo not cause any problem, but if we change the SELECT clause, inverting the order, like\n\n```go\nquery := \"SELECT DISTINCT otherid, to_read, MAX(times) as time \" +\n...\n\n```\n\nScan will fail because it will try to Scan the boolean value in second position `to_read`, into the `time.Time` field of the Conversation structure.\n\n\n### Delete\nIn GORM, if you do not specify a primary key or a where clause (or if the value of the primary key is blank) the generated query will be\n```\nDELETE FROM \u003ctable\u003e\n```\n\nThat will delete everything from your table.\n\nIn igor this is not possible.\n\nYou __must__ specify a `Where` clause or pass to `Delete` a non empty model that will be used to build the where clause.\n\n```go\ndb.Delete(\u0026UserPost{}) // this panics\n\npost := UserPost{\n    Hpid: 10,\n    From: 1,\n}\n\ndb.Delete(\u0026post)\n//generates DELETE FROM posts WHERE hpid = $1, because hpid is a primary key\n\ndb.Where(\u0026post).Delete(\u0026UserPost{}) // ^ generates the same query\n\ndb.Delete(\u0026UserPost{From:1,To:1})\n// generates: DELETE FROM posts WHERE \"from\" = $1 AND \"to\" = $2\n```\n\n### First\nIn GORM `First` is used to get the first record, with or without a second parameter that is the primary key value.\n\nIn igor this is not possible. `First` works only with 2 parameter.\n\n-  `DBModel`: that's the model you want to fill\n-  `key interface{}` that's the primary key value, that __must__ be of the same type of the `DBModel` primary key.\n\n```go\nvar user User\ndb.First(\u0026user, uint64(1))\n\ndb.First(\u0026user, \"1\") // panics, because \"1\" is not of the same type of user.Counter (uint64)\n```\n\nit generates:\n\n```sql\nSELECT users.counter,users.last,users.notify_story,users.private,users.lang,users.username,users.email,users.name,users.surname,users.gender,users.birth_date,users.board_lang,users.timezone,users.viewonline,users.registration_time\nFROM users\nWHERE users.counter = $1\n```\n\n## Other\nEvery other GORM method is not implemented.\n\n### JSON and JSONB support\nIgor supports PostgreSQL JSON and JSONB types natively.\n\nJust define the field in the DBModel with the type `igor.JSON`.\nAfter that, you can work with JSON in the following way:\n\n```go\nuser := createUser()\n\nvar ns igor.JSON = make(igor.JSON) // use it like a map[string]interface{}\n\nns[\"0\"] = struct {\n    From    uint64 `json:from`\n    To      uint64 `json:to`\n    Message string `json:message`\n}{\n    From:    1,\n    To:      1,\n    Message: \"hi bob\",\n}\nns[\"numbers\"] = 1\nns[\"test\"] = 2\n\nuser.NotifyStory = ns\n\nif e = db.Updates(\u0026user); e != nil {\n    t.Errorf(\"updates should work but got: %s\\n\", e.Error())\n}\n\n// To use JSON with json, use:\n// printableJSON, _ := json.Marshal(user.NotifyStory)\n// fmt.Printf(\"%s\\n\", printableJSON)\n\nvar nsNew igor.JSON\nif e = db.Model(User{}).Select(\"notify_story\").Where(\u0026user).Scan(\u0026nsNew); e != nil {\n    t.Errorf(\"Problem scanning into igor.JSON: %s\\n\", e.Error())\n}\n```\n\n### LISTEN / NOTIFY support\nPostgreSQL give us a beautiful method to avoid polling the DBMS, using a simple publish/subscribe model over database connections (read more on the [docs](http://www.postgresql.org/docs/current/static/sql-notify.html)).\n\nIgor gives you the ability to generate notification and subscribe to notifications sent over a channel, using the methods `Listen` and `Notify`.\n\nBelow there's a working example:\n\n```go\ncount := 0\nif e = db.Listen(\"notification_without_payload\", func(payload ...string) {\n    count++\n    t.Log(\"Received notification on channel: notification_without_payload\\n\")\n}); e != nil {\n    t.Fatalf(\"Unable to listen on channel: %s\\n\", e.Error())\n}\n\nfor i := 0; i \u003c 4; i++ {\n    if e = db.Notify(\"notification_without_payload\"); e != nil {\n        t.Fatalf(\"Unable to send notification: %s\\n\", e.Error())\n    }\n}\n\n// wait some time to handle all notifications, because are asynchronous\ntime.Sleep(100 * time.Millisecond)\nif count != 4 {\n    t.Errorf(\"Expected to receive 4 notifications, but counted only: %d\\n\", count)\n}\n\n// listen on an opened channel should fail\nif e = db.Listen(\"notification_without_payload\", func(payload ...string) {}); e == nil {\n    t.Errorf(\"Listen on an opened channel should fail, but succeeded\\n\")\n}\n\n// Handle payload\n\n// listen on more channels, with payload\ncount = 0\nif e = db.Listen(\"np\", func(payload ...string) {\n    count++\n    t.Logf(\"channel np: received payload: %s\\n\", payload)\n}); e != nil {\n    t.Fatalf(\"Unable to listen on channel: %s\\n\", e.Error())\n}\n\n// test sending payload with notify\nfor i := 0; i \u003c 4; i++ {\n    if e = db.Notify(\"np\", strconv.Itoa(i)+\" payload\"); e != nil {\n        t.Fatalf(\"Unable to send notification with payload: %s\\n\", e.Error())\n    }\n}\n\n// wait some time to handle all notifications\ntime.Sleep(100 * time.Millisecond)\nif count != 4 {\n    t.Errorf(\"Expected to receive 4 notifications, but counted only: %d\\n\", count)\n}\n\n// test unlisten\nif e = db.Unlisten(\"notification_without_payload\"); e != nil {\n    t.Errorf(\"Unable to unlisten from notification_without_payload, got: %s\\n\", e.Error())\n}\n\n// test UnlistenAll\nif e = db.UnlistenAll(); e != nil {\n    t.Errorf(\"Unable to unlistenAll, got: %s\\n\", e.Error())\n}\n\n```\n\n### Contributing\nDo you want to add some new method to improve GORM compatibility or add some new method to improve igor?\n\nFeel free to contribute via Pull Request.\n\n### Testing\nTo test igor, you must create a igor user on PostgreSQL and make it own the igor database.\nOn Archlinux, with `postgres` as the PostgreSQL superuser this can be achieved by:\n\n```sh\ncreateuser -U postgres igor\ncreatedb -U postgres igor igor\npsql -U postgres -d igor -c \"GRANT USAGE, CREATE ON SCHEMA public TO igor;\"\n```\n\nYou can run tests with the usual command:\n\n```sh\ngo test\n```\n\n### License\nCopyright 2016-2023 Paolo Galeone. All right reserved.\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n\n### About the author\n\nFeel free to contact me (you can find my email address and other ways to contact me in my GitHub profile page).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgaleone%2Figor","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fgaleone%2Figor","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgaleone%2Figor/lists"}