Rows

Insert Rows

To insert rows, use insert procs. There is a variant that takes a single model instance or a sequence of them.

Instances passed to insert must be mutable for Norm to be able to update their id fields.

In your inim session, run:

var
  userFoo = newUser("foo@foo.foo")
  userBar = newUser("bar@bar.bar")
  alice = newCustomer(some "Alice", userFoo)
  bob = newCustomer(some "Bob", userFoo)
  sam = newCustomer(some "Sam", userBar)
  aliceAndBob = [alice, bob]

Those are the objects we'll insert as rows in the database:

import std/with

with dbConn:
  insert aliceAndBob
  insert userBar
  insert sam

echo()
INSERT INTO "User" (email) VALUES(?) <- @['foo@foo.foo']
INSERT INTO "Customer" (name, user) VALUES(?, ?) <- @['Alice', 1]
Object ID is not 0, skipping insertion. Type: User, ID: 1
INSERT INTO "Customer" (name, user) VALUES(?, ?) <- @['Bob', 1]
INSERT INTO "User" (email) VALUES(?) <- @['bar@bar.bar']
Object ID is not 0, skipping insertion. Type: User, ID: 2
INSERT INTO "Customer" (name, user) VALUES(?, ?) <- @['Sam', 2]

When Norm attempts to insert alice, it detects that userFoo that it referenced in it has not been inserted yet, so there's no id to store as foreign key. So, Norm inserts userFoo automatically and then uses its new id (in this case, 1) as the foreign key value.

With bob, there's no need to do that since userFoo is already in the database.

When inserting Norm Model, it is possible to force the id to a given value by setting the id attribute of the Model. In order for the insertion to proceed, it is necessary to specify force=true when inserting:

var userBaz = newUser("baz@baz.baz")
userBaz.id = 156
with dbConn:
  insert(userBaz, force = true)
echo "userBaz.id == 156 ?", (userBaz.id == 156)

echo()
INSERT INTO "User" (email, id) VALUES(?, ?) <- @['baz@baz.baz', 156]
userBaz.id == 156 ?true

Select Row

Select in general

To select a rows with norm, you instantiate a model that serves as a container for the selected data and call select.

One curious thing about select is that its result depends not only on the condition you pass but also on the container. If the container has Model fields that are not None, Norm will select the related rows in a single JOIN query giving you a fully populated model object. However, if the container has a none Model field, it is just ignored.

In other words, Norm will automatically handle the "n+1" problem.

Let's see how that works:

var customerBar = newCustomer()
dbConn.select(customerBar, "User.email = ?", "bar@bar.bar")

echo()
SELECT "Customer".name, "Customer".user, "user".email, "user".id, "Customer".id FROM "Customer" LEFT JOIN "User" AS "user" ON "Customer".user = "user".id WHERE User.email = ? LIMIT 1 <- ['bar@bar.bar']

Let's examine how Norm populated customerBar:

echo customerBar[]
(name: some("Sam"), user: ..., id: 3)
echo customerBar.user[]
(email: "bar@bar.bar", id: 2)

If you pass a sequence to select, you'll get many rows:

var customersFoo = @[newCustomer()]
dbConn.select(customersFoo, "User.email = ?", "foo@foo.foo")

echo()
SELECT "Customer".name, "Customer".user, "user".email, "user".id, "Customer".id FROM "Customer" LEFT JOIN "User" AS "user" ON "Customer".user = "user".id WHERE User.email = ? <- ['foo@foo.foo']

The generated query is similar to the previous one, but the result is populated objects, not one:

for customer in customersFoo:
  echo customer[]
  echo customer.user[]
(name: some("Alice"), user: ..., id: 1)
(email: "foo@foo.foo", id: 1)
(name: some("Bob"), user: ..., id: 2)
(email: "foo@foo.foo", id: 1)

If you query relationships that are nested, such as when customers can have pets and you want to query all pets of all customers of users with a specific email address, you will need to concatenate the foreign-key fields, separeted by a _ in your query.

import norm/model

type Pet* = ref object of Model
  name*: string
  owner*: Customer

func newPet*(name = "", owner = newCustomer()): Pet =
  Pet(name: name, owner: owner)

dbConn.createTables(newPet())

var fluffi: Pet = newPet("Fluffi", bob)
dbConn.insert(fluffi)


var petsFoo = @[newPet()]
dbConn.select(petsFoo, "owner_user.email LIKE ?", "foo%")

for pet in petsFoo:
  echo pet[]
CREATE TABLE IF NOT EXISTS "User"(email TEXT NOT NULL, id INTEGER NOT NULL PRIMARY KEY)
CREATE TABLE IF NOT EXISTS "Customer"(name TEXT, user INTEGER NOT NULL, id INTEGER NOT NULL PRIMARY KEY, FOREIGN KEY(user) REFERENCES "User"(id))
CREATE TABLE IF NOT EXISTS "Pet"(name TEXT NOT NULL, owner INTEGER NOT NULL, id INTEGER NOT NULL PRIMARY KEY, FOREIGN KEY(owner) REFERENCES "Customer"(id))
Object ID is not 0, skipping insertion. Type: Customer, ID: 2
INSERT INTO "Pet" (name, owner) VALUES(?, ?) <- @['Fluffi', 2]
SELECT "Pet".name, "Pet".owner, "owner".name, "owner".user, "owner_user".email, "owner_user".id, "owner".id, "Pet".id FROM "Pet" LEFT JOIN "Customer" AS "owner" ON "Pet".owner = "owner".id LEFT JOIN "User" AS "owner_user" ON "owner".user = "owner_user".id WHERE owner_user.email LIKE ? <- ['foo%']
(name: "Fluffi", owner: ..., id: 1)

Update Rows

To update a row, you just update the object and call update on it:

customerBar.name = some "Saaam"
dbConn.update(customerBar)

echo()
UPDATE "User" SET email = ? WHERE id = 2 <- @['bar@bar.bar']
UPDATE "Customer" SET name = ?, user = ? WHERE id = 3 <- @['Saaam', 2]

Since customer references a user, to update a customer, we also need to update its user. Norm handles that automatically by generating two queries.

Updating rows in bulk is also possible:

for customer in customersFoo:
  customer.name = some ("Mega" & get(customer.name))

dbConn.update(customersFoo)

echo()
UPDATE "User" SET email = ? WHERE id = 1 <- @['foo@foo.foo']
UPDATE "User" SET email = ? WHERE id = 1 <- @['foo@foo.foo']
UPDATE "Customer" SET
user = CASE id
WHEN 1 THEN ?
WHEN 2 THEN ?
END,
name = CASE id
WHEN 1 THEN ?
WHEN 2 THEN ?
END
WHERE id IN (1, 2) <- @[1, 1, 'MegaAlice', 'MegaBob']

Delete Rows

To delete a row, call delete on an object:

dbConn.delete(sam)

echo()
DELETE FROM "Customer" WHERE id = 3

After deletion, the object becomes nil:

echo sam.isNil
true

Count Rows

Selecting rows is expensive if many rows are fetched. Knowing the number of rows you have before doing the actual select is useful.

To count the rows without fetching them, use count:

echo dbConn.count(Customer)
SELECT COUNT( *) FROM "Customer" WHERE 1 <- []
2

To count only unique records, use dist = true in conjunction with the column name you want to check for uniqueness:

echo dbConn.count(Customer, "user", dist = true)
SELECT COUNT(DISTINCT user) FROM "Customer" WHERE 1 <- []
1

You can also count rows matching condition:

echo dbConn.count(Customer, "*", dist = false, "name LIKE ?", "alice")
SELECT COUNT( *) FROM "Customer" WHERE name LIKE ? <- ['alice']
0

Sum Column Values

To get a sum of column values, use sum proc:

import norm/model

type Chair = ref object of Model
  legCount: Natural

func newChair(legCount = 0): Chair = Chair(legCount: legCount)

dbConn.createTables(newChair())

var
  threeLeggedChair = newChair(3)
  fourLeggedChair = newChair(4)
  anotherFourLeggedChair = newChair(4)

dbConn.insert(threeLeggedChair)
dbConn.insert(fourLeggedChair)
dbConn.insert(anotherFourLeggedChair)

echo dbConn.sum(Chair, "legCount")
echo dbConn.sum(Chair, "legCount", dist = true)
echo dbConn.sum(Chair, "legCount", dist = false, "legCount > ?", 3)
CREATE TABLE IF NOT EXISTS "Chair"(legCount INTEGER NOT NULL, id INTEGER NOT NULL PRIMARY KEY)
INSERT INTO "Chair" (legCount) VALUES(?) <- @[3]
INSERT INTO "Chair" (legCount) VALUES(?) <- @[4]
INSERT INTO "Chair" (legCount) VALUES(?) <- @[4]
SELECT SUM( legCount) FROM "Chair" WHERE 1 <- []
11.0
SELECT SUM(DISTINCT legCount) FROM "Chair" WHERE 1 <- []
7.0
SELECT SUM( legCount) FROM "Chair" WHERE legCount > ? <- [3]
8.0

Check If Row Exists

If you need to check if a row selected by a given condition exists, use exists proc:

echo dbConn.exists(Customer, "name = ?", "Alice")
SELECT EXISTS(SELECT NULL FROM "Customer" WHERE name = ?) <- ['Alice']
false