Skip to content

Commit

Permalink
Merge pull request #126 from moigagoo/feature/125_readonly_models
Browse files Browse the repository at this point in the history
Forbid mutating procs for types with `ro` pragma.
  • Loading branch information
moigagoo committed Jan 6, 2022
2 parents d046c5e + 16378f9 commit 52c4e09
Show file tree
Hide file tree
Showing 14 changed files with 217 additions and 4 deletions.
18 changes: 18 additions & 0 deletions book/models.nim
Expand Up @@ -86,6 +86,24 @@ nbText """
This will result in this schema:
CREATE TABLE IF NOT EXISTS "ThingTable"(attr TEXT NOT NULL, id INTEGER NOT NULL PRIMARY KEY)
## Read-only Models
To slim down DB queries when you don't need to fetch the full model, use read-only models.
A read-only model is a model that defines a subset of fields of another model. Another important property of read-only model is that you can't use it to insert, update, or delete data; just select.
To define a read-only model, annotate your `Model` subtype with `ro` pragma and point it to an existing table with `tableName`:
"""

nbCode:
type
ClientName {.ro, tableName: "Client".} = ref object of Model
name: string

nbText """
Now you can query data from the DB into `ClientName` instances just like you do with any other model.
"""

nbSave
Expand Down
7 changes: 6 additions & 1 deletion changelog.md
Expand Up @@ -7,9 +7,14 @@
- [t]—test suite improvement
- [d]—docs improvement

## 2.3.4 (WIP)

- [+] Add the ability to define read-only models (see [#125](https://github.com/moigagoo/norm/issues/125)).


## 2.3.3 (December 26, 2021)

- [+] SQLite: Add `conflictPolicy` param to `insert` procs. It determines how insertion conflicts should be handled: `cpRaise` (default) means raise an `DbError`, `cpIgnore` means keep the old row and ignore the new one, and `cpReplace` means replace the old row.
- [+] SQLite: Add `conflictPolicy` param to `insert` procs. It determines how insertion conflicts should be handled: `cpRaise` (default) means raise an `DbError`, `cpIgnore` means keep the old row and ignore the new one, and `cpReplace` means replace the old row (see [#120](https://github.com/moigagoo/norm/issues/120)).

## 2.3.2 (November 19, 2021)

Expand Down
6 changes: 6 additions & 0 deletions src/norm/model.nim
Expand Up @@ -108,3 +108,9 @@ func joinGroups*[T: Model](obj: T, flds: seq[string] = @[]): seq[tuple[tbl, tAls

result.add grp & subMod.joinGroups(flds & fld)

proc checkRo*(T: typedesc[Model]) =
## Stop compilation if an object has `ro`_ pragma.

when T.hasCustomPragma(ro):
{.error: "can't use mutating procs with read-only models".}

8 changes: 7 additions & 1 deletion src/norm/postgres.nim
Expand Up @@ -8,7 +8,7 @@ import private/[dot, log]
import model
import pragmas

export dbtypes
export dbtypes, macros


type
Expand Down Expand Up @@ -127,6 +127,8 @@ proc insert*[T: Model](dbConn; obj: var T, force = false) =
By default, if the inserted object's ``id`` is not 0, the object is considered already inserted and is not inserted again. You can force new insertion with ``force = true``.
]##

checkRo(T)

if obj.id != 0 and not force:
log("Object ID is not 0, skipping insertion. Type: $#, ID: $#" % [$T, $obj.id])

Expand Down Expand Up @@ -235,6 +237,8 @@ proc count*(dbConn; T: typedesc[Model], col = "*", dist = false, cond = "TRUE",
proc update*[T: Model](dbConn; obj: var T) =
## Update rows for `Model`_ instance and its `Model`_ fields.

checkRo(T)

for fld, val in obj[].fieldPairs:
if val.model.isSome:
var subMod = get val.model
Expand All @@ -260,6 +264,8 @@ proc update*[T: Model](dbConn; objs: var openArray[T]) =
proc delete*[T: Model](dbConn; obj: var T) =
## Delete rows for `Model`_ instance and its `Model`_ fields.

checkRo(T)

let qry = "DELETE FROM $# WHERE id = $#" % [T.table, $obj.id]

log(qry)
Expand Down
4 changes: 3 additions & 1 deletion src/norm/pragmas.nim
Expand Up @@ -8,7 +8,9 @@ template pk* {.pragma.}
]##

template ro* {.pragma.}
##[ Mark field as read-only.
##[ Mark model or field as read-only.
Read-only models can't be mutated, i.e. you can't call ``insert``, ``update``, or ``delete`` on their instances.
Read-only fields are ignored in ``insert`` and ``update`` procs unless ``force`` is passed.
Expand Down
8 changes: 7 additions & 1 deletion src/norm/sqlite.nim
Expand Up @@ -8,7 +8,7 @@ import private/[dot, log]
import model
import pragmas

export dbtypes
export dbtypes, macros


type
Expand Down Expand Up @@ -125,6 +125,8 @@ proc insert*[T: Model](dbConn; obj: var T, force = false, conflictPolicy = cpRai
``conflictPolicy`` determines how the proc reacts to insertion conflicts. ``cpRaise`` means raise a ``DbError``, ``cpIgnore`` means ignore the conflict and do not insert the conflicting row, ``cpReplace`` means overwrite the older row with the newer one.
]##

checkRo(T)

if obj.id != 0 and not force:
log("Object ID is not 0, skipping insertion. Type: $#, ID: $#" % [$T, $obj.id])

Expand Down Expand Up @@ -235,6 +237,8 @@ proc count*(dbConn; T: typedesc[Model], col = "*", dist = false, cond = "1", par
proc update*[T: Model](dbConn; obj: var T) =
## Update rows for `Model`_ instance and its `Model`_ fields.

checkRo(T)

for fld, val in obj[].fieldPairs:
if val.model.isSome:
var subMod = get val.model
Expand All @@ -260,6 +264,8 @@ proc update*[T: Model](dbConn; objs: var openArray[T]) =
proc delete*[T: Model](dbConn; obj: var T) =
## Delete rows for `Model`_ instance and its `Model`_ fields.

checkRo(T)

let qry = "DELETE FROM $# WHERE id = $#" % [T.table, $obj.id]

log(qry)
Expand Down
1 change: 1 addition & 0 deletions src/norm/types.nim
Expand Up @@ -25,3 +25,4 @@ func `$`*[_](s: StringOfCap[_]): string =

func `$`*[_](s: PaddedStringOfCap[_]): string =
string(s)

5 changes: 5 additions & 0 deletions tests/models.nim
Expand Up @@ -15,6 +15,9 @@ type
name* {.unique.}: string
pet* {.onDelete: "CASCADE"}: Option[Pet]

PersonName* {.ro, tableName: "Person".} = ref object of Model
name*: string

PetPerson* = ref object of Model
pet*: Pet
person*: Person
Expand Down Expand Up @@ -152,3 +155,5 @@ func `===`*(a, b: String): bool =
func newTable*(legCount: Positive = 4): Table =
Table(legCount: legCount)

func newPersonName*: PersonName = PersonName(name: "")

46 changes: 46 additions & 0 deletions tests/postgres/tro.nim
@@ -0,0 +1,46 @@
import std/[unittest, strutils, sugar, options]

import norm/[model, postgres]

import ../models


const
dbHost = "postgres"
dbUser = "postgres"
dbPassword = "postgres"
dbDatabase = "postgres"


suite "Read-only models, non-mutating procs":
proc resetDb =
let dbConn = open(dbHost, dbUser, dbPassword, "template1")
dbConn.exec(sql "DROP DATABASE IF EXISTS $#" % dbDatabase)
dbConn.exec(sql "CREATE DATABASE $#" % dbDatabase)
close dbConn

setup:
resetDb()
let dbConn = open(dbHost, dbUser, dbPassword, dbDatabase)

var
alice = newPerson("Alice", none Pet)
bob = newPerson("Bob", none Pet)

dbConn.createTables(newPerson())
dbConn.insert(alice)
dbConn.insert(bob)

teardown:
close dbConn
resetDb()

test "Select rows":
var personNames = @[newPersonName()]

dbConn.selectAll(personNames)

assert len(personNames) == 2
assert personNames[0].name == "Alice"
assert personNames[1].name == "Bob"

42 changes: 42 additions & 0 deletions tests/postgres/tronocomp.nim
@@ -0,0 +1,42 @@
discard """
action: "reject"
errormsg: "can't use mutating procs with read-only models"
file: "model.nim"
"""

import std/[unittest, strutils, sugar]

import norm/[model, postgres]

import ../models


const
dbHost = "postgres"
dbUser = "postgres"
dbPassword = "postgres"
dbDatabase = "postgres"


suite "Read-only models, mutating procs":
proc resetDb =
let dbConn = open(dbHost, dbUser, dbPassword, "template1")
dbConn.exec(sql "DROP DATABASE IF EXISTS $#" % dbDatabase)
dbConn.exec(sql "CREATE DATABASE $#" % dbDatabase)
close dbConn

setup:
resetDb()
let dbConn = open(dbHost, dbUser, dbPassword, dbDatabase)

dbConn.createTables(newPerson())

teardown:
close dbConn
resetDb()

test "Insert row":
var personName = PersonName(name: "Name")

dbConn.insert(personName)

1 change: 1 addition & 0 deletions tests/sqlite/tcount.nim
Expand Up @@ -41,3 +41,4 @@ suite "Count":
test "Conditions":
check dbConn.count(Person, "*", dist = false, "name LIKE ?", "alice") == 1
check dbConn.count(Person, "pet", dist = true, "pet = ?", 1) == 1

1 change: 1 addition & 0 deletions tests/sqlite/tdb.nim
Expand Up @@ -25,3 +25,4 @@ suite "Database manipulation":
dropDb()

check not fileExists(dbFile)

39 changes: 39 additions & 0 deletions tests/sqlite/tro.nim
@@ -0,0 +1,39 @@
import std/[unittest, os, strutils, options]

import norm/[model, sqlite]

import ../models


const dbFile = "test.db"


suite "Read-only models, non-mutating procs":
setup:
removeFile dbFile

putEnv(dbHostEnv, dbFile)

var
alice = newPerson("Alice", none Pet)
bob = newPerson("Bob", none Pet)

withDb:
db.createTables(newPerson())
db.insert(alice)
db.insert(bob)

teardown:
delEnv(dbHostEnv)
removeFile dbFile

test "Select rows":
var personNames = @[newPersonName()]

withDb:
db.selectAll(personNames)

assert len(personNames) == 2
assert personNames[0].name == "Alice"
assert personNames[1].name == "Bob"

35 changes: 35 additions & 0 deletions tests/sqlite/tronocomp.nim
@@ -0,0 +1,35 @@
discard """
action: "reject"
errormsg: "can't use mutating procs with read-only models"
file: "model.nim"
"""

import std/[unittest, os, strutils]

import norm/[model, sqlite]

import ../models


const dbFile = "test.db"


suite "Read-only models, mutating procs":
setup:
removeFile dbFile

putEnv(dbHostEnv, dbFile)

withDb:
db.createTables(newPerson())

teardown:
delEnv(dbHostEnv)
removeFile dbFile

test "Insert row":
var personName = PersonName(name: "Name")

withDb:
db.insert(personName)

0 comments on commit 52c4e09

Please sign in to comment.