# Backend for a simple todo program with sqlite persistence. # # Most procs dealing with a TDbConn object may raise an EDb exception. import db_sqlite, parseutils, strutils, times type TTodo* = object ## A todo object holding the information serialized to the database. id: int64 ## Unique identifier of the object in the ## database, use the getId() accessor to read it. text*: string ## Description of the task to do. priority*: int ## The priority can be any user defined integer. isDone*: bool ## Done todos are still kept marked. modificationDate: Time ## The modification time can't be modified from ## outside of this module, use the ## getModificationDate accessor. TPagedParams* = object ## Contains parameters for a query, initialize default values with ## initDefaults(). pageSize*: int64 ## Lines per returned query page, -1 for ## unlimited. priorityAscending*: bool ## Sort results by ascending priority. dateAscending*: bool ## Sort results by ascending modification date. showUnchecked*: bool ## Get unchecked objects. showChecked*: bool ## Get checked objects. # - General procs # proc initDefaults*(params: var TPagedParams) = ## Sets sane defaults for a TPagedParams object. ## ## Note that you should always provide a non zero pageSize, either a specific ## positive value or negative for unbounded query results. params.pageSize = high(int64) params.priorityAscending = false params.dateAscending = false params.showUnchecked = true params.showChecked = false proc openDatabase*(path: string): TDbConn = ## Creates or opens the sqlite3 database. ## ## Pass the path to the sqlite database, if the database doesn't exist it ## will be created. The proc may raise a EDB exception let conn = db_sqlite.open(path, "user", "pass", "db") query = sql"""CREATE TABLE IF NOT EXISTS Todos ( id INTEGER PRIMARY KEY, priority INTEGER NOT NULL, is_done BOOLEAN NOT NULL, desc TEXT NOT NULL, modification_date INTEGER NOT NULL, CONSTRAINT Todos UNIQUE (id))""" db_sqlite.exec(conn, query) result = conn # - Procs related to TTodo objects # proc initFromDB(id: int64; text: string; priority: int, isDone: bool; modificationDate: Time): TTodo = ## Returns an initialized TTodo object created from database parameters. ## ## The proc assumes all values are right. Note this proc is NOT exported. assert(id >= 0, "Identity identifiers should not be negative") result.id = id result.text = text result.priority = priority result.isDone = isDone result.modificationDate = modificationDate proc getId*(todo: TTodo): int64 = ## Accessor returning the value of the private id property. return todo.id proc getModificationDate*(todo: TTodo): Time = ## Returns the last modification date of a TTodo entry. return todo.modificationDate proc update*(todo: var TTodo; conn: TDbConn): bool = ## Checks the database for the object and refreshes its variables. ## ## Use this method if you (or another entity) have modified the database and ## want to update the object you have with whatever the database has stored. ## Returns true if the update succeeded, or false if the object was not found ## in the database any more, in which case you should probably get rid of the ## TTodo object. assert(todo.id >= 0, "The identifier of the todo entry can't be negative") let query = sql"""SELECT desc, priority, is_done, modification_date FROM Todos WHERE id = ?""" try: let rows = conn.getAllRows(query, $todo.id) if len(rows) < 1: return assert(1 == len(rows), "Woah, didn't expect so many rows") todo.text = rows[0][0] todo.priority = rows[0][1].parseInt todo.isDone = rows[0][2].parseBool todo.modificationDate = Time(rows[0][3].parseInt) result = true except: echo("Something went wrong selecting for id " & $todo.id) proc save*(todo: var TTodo; conn: TDbConn): bool = ## Saves the current state of text, priority and isDone to the database. ## ## Returns true if the database object was updated (in which case the ## modification date will have changed). The proc can return false if the ## object wasn't found, for instance, in which case you should drop that ## object anyway and create a new one with addTodo(). Also EDb can be raised. assert(todo.id >= 0, "The identifier of the todo entry can't be negative") let currentDate = getTime() query = sql"""UPDATE Todos SET desc = ?, priority = ?, is_done = ?, modification_date = ? WHERE id = ?""" rowsUpdated = conn.execAffectedRows(query, $todo.text, $todo.priority, $todo.isDone, $int(currentDate), $todo.id) if 1 == rowsUpdated: todo.modificationDate = currentDate result = true # - Procs dealing directly with the database # proc addTodo*(conn: TDbConn; priority: int; text: string): TTodo = ## Inserts a new todo into the database. ## ## Returns the generated todo object. If there is an error EDb will be raised. let currentDate = getTime() query = sql"""INSERT INTO Todos (priority, is_done, desc, modification_date) VALUES (?, 'false', ?, ?)""" todoId = conn.insertId(query, priority, text, $int(currentDate)) result = initFromDB(todoId, text, priority, false, currentDate) proc deleteTodo*(conn: TDbConn; todoId: int64): int64 {.discardable.} = ## Deletes the specified todo identifier. ## ## Returns the number of rows which were affected (1 or 0) let query = sql"""DELETE FROM Todos WHERE id = ?""" result = conn.execAffectedRows(query, $todoId) proc getNumEntries*(conn: TDbConn): int = ## Returns the number of entries in the Todos table. ## ## If the function succeeds, returns the zero or positive value, if something ## goes wrong a negative value is returned. let query = sql"""SELECT COUNT(id) FROM Todos""" try: let row = conn.getRow(query) result = row[0].parseInt except: echo("Something went wrong retrieving number of Todos entries") result = -1 proc getPagedTodos*(conn: TDbConn; params: TPagedParams; page = 0'i64): seq[TTodo] = ## Returns the todo entries for a specific page. ## ## Pages are calculated based on the params.pageSize parameter, which can be ## set to a negative value to specify no limit at all. The query will be ## affected by the TPagedParams, which should have sane values (call ## initDefaults). assert(page >= 0, "You should request a page zero or bigger than zero") result = @[] # Well, if you don't want to see anything, there's no point in asking the db. if not params.showUnchecked and not params.showChecked: return let order_by = [ if params.priorityAscending: "ASC" else: "DESC", if params.dateAscending: "ASC" else: "DESC"] query = sql("""SELECT id, desc, priority, is_done, modification_date FROM Todos WHERE is_done = ? OR is_done = ? ORDER BY priority $1, modification_date $2, id DESC LIMIT ? * ?,?""" % order_by) args = @[$params.showChecked, $(not params.showUnchecked), $params.pageSize, $page, $params.pageSize] #echo("Query " & string(query)) #echo("args: " & args.join(", ")) var newId: BiggestInt for row in conn.fastRows(query, args): let numChars = row[0].parseBiggestInt(newId) assert(numChars > 0, "Huh, couldn't parse identifier from database?") result.add(initFromDB(int64(newId), row[1], row[2].parseInt, row[3].parseBool, Time(row[4].parseInt))) proc getTodo*(conn: TDbConn; todoId: int64): ref TTodo = ## Returns a reference to a TTodo or nil if the todo could not be found. var tempTodo: TTodo tempTodo.id = todoId if tempTodo.update(conn): new(result) result[] = tempTodo