Flesh out documentation with a worked example.
This commit is contained in:
parent
9bf3c4f3ec
commit
a3dbb0bbbc
@ -6,30 +6,204 @@
|
||||
## It supports a simple, opinionated model mapper to generate SQL queries based
|
||||
## on Nim objects. It also includes a simple connection pooling implementation.
|
||||
##
|
||||
## Fiber ORM is not intended to be a 100% all-cases-covered ORM that handles
|
||||
## every potential data access pattern one might wish to implement. It is best
|
||||
## thought of as a collection of common SQL generation patterns. It is intended
|
||||
## to cover 90% of the common queries and functions one might write when
|
||||
## implementing an SQL-based access layer. It is expected that there may be a
|
||||
## few more complicated queries that need to be implemented to handle specific
|
||||
## access patterns.
|
||||
##
|
||||
## The simple mapping pattern provided by Fiber ORM also works well on top of
|
||||
## databases that encapsulate data access logic in SQL with, for example,
|
||||
## views.
|
||||
##
|
||||
## .. _Postgres: https://nim-lang.org/docs/db_postgres.html
|
||||
## .. _SQLite: https://nim-lang.org/docs/db_sqlite.html
|
||||
##
|
||||
## Basic Usage
|
||||
## ===========
|
||||
##
|
||||
## Consider a simple TODO list application that keeps track of TODO items as
|
||||
## well as time logged against those items. You might have a schema such as:
|
||||
##
|
||||
## .. code-block:: SQL
|
||||
## create extension if not exists "pgcrypto";
|
||||
##
|
||||
## create table todo_items columns (
|
||||
## id uuid not null primary key default gen_random_uuid(),
|
||||
## owner varchar not null,
|
||||
## summary varchar not null,
|
||||
## details varchar default null,
|
||||
## priority integer not null default 0,
|
||||
## related_todo_item_ids uuid[] not null default '{}'
|
||||
## );
|
||||
##
|
||||
## create table time_entries columns (
|
||||
## id uuid not null primary key default gen_random_uuid(),
|
||||
## todo_item_id uuid not null references todo_items (id) on delete cascade,
|
||||
## start timestamp with timezone not null default current_timestamp,
|
||||
## stop timestamp with timezone default null,
|
||||
## );
|
||||
##
|
||||
## Models may be defined as:
|
||||
##
|
||||
## .. code-block:: Nim
|
||||
## # models.nim
|
||||
## import std/options, std/times
|
||||
## import uuids
|
||||
##
|
||||
## type
|
||||
## TodoItem* = object
|
||||
## id*: UUID
|
||||
## owner*: string
|
||||
## summary*: string
|
||||
## details*: Option[string]
|
||||
## priority*: int
|
||||
## relatedTodoItemIds*: seq[UUID]
|
||||
##
|
||||
## TimeEntry* = object
|
||||
## id*: UUID
|
||||
## todoItemId*: Option[UUID]
|
||||
## start*: DateTime
|
||||
## stop*: Option[DateTime]
|
||||
##
|
||||
## Using Fiber ORM we can generate a data access layer with:
|
||||
##
|
||||
## .. code-block:: Nim
|
||||
## # db.nim
|
||||
## import fiber_orm
|
||||
## import ./models.nim
|
||||
##
|
||||
## type TodoDB* = DbConnPool
|
||||
##
|
||||
## proc initDb*(connString: string): TodoDB =
|
||||
## fiber_orm.initPool(connect =
|
||||
## proc(): DbConn = open("", "", "", connString))
|
||||
##
|
||||
## generateProcsForModels(TodoDB, [TodoItem, TimeEntry])
|
||||
##
|
||||
## generateLookup(TodoDB, TimeEntry, @["todoItemId"])
|
||||
##
|
||||
## This will generate the following procedures:
|
||||
##
|
||||
## .. code-block:: Nim
|
||||
## proc getTodoItem*(db: TodoDB, id: UUID): TodoItem;
|
||||
## proc getAllTodoItems*(db: TodoDB): seq[TodoItem];
|
||||
## proc createTodoItem*(db: TodoDB, rec: TodoItem): TodoItem;
|
||||
## proc updateTodoItem*(db: TodoDB, rec: TodoItem): bool;
|
||||
## proc deleteTodoItem*(db: TodoDB, rec: TodoItem): bool;
|
||||
## proc deleteTodoItem*(db: TodoDB, id: UUID): bool;
|
||||
##
|
||||
## proc findTodoItemsWhere*(db: TodoDB, whereClause: string,
|
||||
## values: varargs[string, dbFormat]): seq[TodoItem];
|
||||
##
|
||||
## proc getTimeEntry*(db: TodoDB, id: UUID): TimeEntry;
|
||||
## proc getAllTimeEntries*(db: TodoDB): seq[TimeEntry];
|
||||
## proc createTimeEntry*(db: TodoDB, rec: TimeEntry): TimeEntry;
|
||||
## proc updateTimeEntry*(db: TodoDB, rec: TimeEntry): bool;
|
||||
## proc deleteTimeEntry*(db: TodoDB, rec: TimeEntry): bool;
|
||||
## proc deleteTimeEntry*(db: TodoDB, id: UUID): bool;
|
||||
##
|
||||
## proc findTimeEntriesWhere*(db: TodoDB, whereClause: string,
|
||||
## values: varargs[string, dbFormat]): seq[TimeEntry];
|
||||
##
|
||||
## proc findTimeEntriesByTodoItemId(db: TodoDB, todoItemId: UUID): seq[TimeEntry];
|
||||
##
|
||||
## Object-Relational Modeling
|
||||
## ==========================
|
||||
##
|
||||
## Model Class
|
||||
## -----------
|
||||
##
|
||||
## Table Name
|
||||
## ``````````
|
||||
## Fiber ORM uses simple Nim `object`s and `ref object`s as model classes.
|
||||
## Fiber ORM expects there to be one table for each model class.
|
||||
##
|
||||
## Column Names
|
||||
## Name Mapping
|
||||
## ````````````
|
||||
## Fiber ORM uses `snake_case` for database identifiers (column names, table
|
||||
## names, etc.) and `camelCase` for Nim identifiers. We automatically convert
|
||||
## model names to and from table names (`TodoItem` <-> `todo_items`), as well
|
||||
## as column names (`userId` <-> `user_id`).
|
||||
##
|
||||
## Notice that table names are automatically pluralized from model class names.
|
||||
## In the above example, you have:
|
||||
##
|
||||
## =========== ================
|
||||
## Model Class Table Name
|
||||
## =========== ================
|
||||
## TodoItem todo_items
|
||||
## TimeEntry time_entries
|
||||
## =========== ================
|
||||
##
|
||||
## Because Nim is style-insensitive, you can generall refer to model classes
|
||||
## and fields using `snake_case`, `camelCase`, or `PascalCase` in your code and
|
||||
## expect Fiber ORM to be able to map the names to DB identifier names properly
|
||||
## (though FiberORM will always use `camelCase` internally).
|
||||
##
|
||||
## See the `identNameToDb`_, `dbNameToIdent`_, `tableName`_ and `dbFormat`_
|
||||
## procedures in the `fiber_orm/util`_ module for details.
|
||||
##
|
||||
## .. _identNameToDb: fiber_orm/util.html#identNameToDb,string
|
||||
## .. _dbNameToIdent: fiber_orm/util.html#dbNameToIdent,string
|
||||
## .. _tableName: fiber_orm/util.html#tableName,type
|
||||
## .. _dbFormat: fiber_orm/util.html#dbFormat,DateTime
|
||||
## .. _util: fiber_orm/util.html
|
||||
##
|
||||
## ID Field
|
||||
## ````````
|
||||
##
|
||||
## Fiber ORM expects every model class to have a field named `id`, with a
|
||||
## corresponding `id` column in the model table. This field must be either a
|
||||
## `string`, `integer`, or `UUID`_.
|
||||
##
|
||||
## When creating a new record the `id` field will be omitted if it is empty
|
||||
## (`Option.isNone`_, `UUID.isZero`_, value of `0`, or only whitespace). This
|
||||
## is intended to allow for cases like the example where the database may
|
||||
## generate an ID when a new record is inserted. If a non-zero value is
|
||||
## provided, the create call will include the `id` field in the `INSERT` query.
|
||||
##
|
||||
## .. _Option.isNone: https://nim-lang.org/docs/options.html#isNone,Option[T]
|
||||
## .. _UUID.isZero: https://github.com/pragmagic/uuids/blob/8cb8720b567c6bcb261bd1c0f7491bdb5209ad06/uuids.nim#L72
|
||||
##
|
||||
## Supported Data Types
|
||||
## --------------------
|
||||
##
|
||||
## The following Nim data types are supported by Fiber ORM:
|
||||
##
|
||||
## =============== ====================== =================
|
||||
## Nim Type Postgres Type SQLite Type
|
||||
## =============== ====================== =================
|
||||
## `string` `varchar`_
|
||||
## `int` `integer`_
|
||||
## `float` `double`_
|
||||
## `bool` `boolean`_
|
||||
## `DateTime`_ `timestamp`_
|
||||
## `seq[]` `array`_
|
||||
## `UUID`_ `uuid (pg)`_
|
||||
## `Option`_ *allows* `NULL` [#f1]_
|
||||
## `JsonNode`_ `jsonb`_
|
||||
## =============== ====================== =================
|
||||
##
|
||||
## .. [#f1] Note that this implies that all `NULL`-able fields should be typed
|
||||
## as optional using `Option[fieldType]`. Conversely, any fields with
|
||||
## non-optional types should also be constrained to be `NOT NULL` in
|
||||
## the database schema.
|
||||
##
|
||||
## .. _DateTime: https://nim-lang.org/docs/times.html#DateTime
|
||||
## .. _UUID: https://github.com/pragmagic/uuids
|
||||
## .. _Option: https://nim-lang.org/docs/options.html#Option
|
||||
## .. _JsonNode: https://nim-lang.org/docs/json.html#JsonNode
|
||||
##
|
||||
## .. _varchar: https://www.postgresql.org/docs/current/datatype-character.html
|
||||
## .. _integer: https://www.postgresql.org/docs/current/datatype-numeric.html#DATATYPE-INT
|
||||
## .. _double: https://www.postgresql.org/docs/current/datatype-numeric.html#DATATYPE-FLOAT
|
||||
## .. _boolean: https://www.postgresql.org/docs/current/datatype-boolean.html
|
||||
## .. _timestamp: https://www.postgresql.org/docs/current/datatype-datetime.html
|
||||
## .. _array: https://www.postgresql.org/docs/current/arrays.html
|
||||
## .. _uuid (pg): https://www.postgresql.org/docs/current/datatype-uuid.html
|
||||
## .. _jsonb: https://www.postgresql.org/docs/current/datatype-json.html
|
||||
##
|
||||
## Database Object
|
||||
## ===============
|
||||
|
||||
@ -171,19 +345,19 @@ template findRecordsBy*(db: DbConn, modelType: type, lookups: seq[tuple[field: s
|
||||
|
||||
macro generateProcsForModels*(dbType: type, modelTypes: openarray[type]): untyped =
|
||||
## Generate all standard access procedures for the given model types. For a
|
||||
## `model class`_ named `SampleRecord`, this will generate the following
|
||||
## `model class`_ named `TodoItem`, this will generate the following
|
||||
## procedures:
|
||||
##
|
||||
## .. code-block:: Nim
|
||||
## proc getSampleRecord*(db: dbType): SampleRecord;
|
||||
## proc getAllSampleRecords*(db: dbType): SampleRecord;
|
||||
## proc createSampleRecord*(db: dbType, rec: SampleRecord): SampleRecord;
|
||||
## proc deleteSampleRecord*(db: dbType, rec: SampleRecord): bool;
|
||||
## proc deleteSampleRecord*(db: dbType, id: idType): bool;
|
||||
## proc updateSampleRecord*(db: dbType, rec: SampleRecord): bool;
|
||||
## proc getTodoItem*(db: TodoDB, id: idType): TodoItem;
|
||||
## proc getAllTodoItems*(db: TodoDB): TodoItem;
|
||||
## proc createTodoItem*(db: TodoDB, rec: TodoItem): TodoItem;
|
||||
## proc deleteTodoItem*(db: TodoDB, rec: TodoItem): bool;
|
||||
## proc deleteTodoItem*(db: TodoDB, id: idType): bool;
|
||||
## proc updateTodoItem*(db: TodoDB, rec: TodoItem): bool;
|
||||
##
|
||||
## proc findSampleRecordsWhere*(
|
||||
## db: dbType, whereClause: string, values: varargs[string]): SampleRecord;
|
||||
## proc findTodoItemsWhere*(
|
||||
## db: TodoDB, whereClause: string, values: varargs[string]): TodoItem;
|
||||
##
|
||||
## `dbType` is expected to be some type that has a defined `withConn`_
|
||||
## procedure.
|
||||
@ -223,14 +397,16 @@ macro generateProcsForModels*(dbType: type, modelTypes: openarray[type]): untype
|
||||
|
||||
macro generateLookup*(dbType: type, modelType: type, fields: seq[string]): untyped =
|
||||
## Create a lookup procedure for a given set of field names. For example,
|
||||
## given the TODO database demostrated above,
|
||||
##
|
||||
## .. code-block:: Nim
|
||||
## generateLookup(SampleDB, SampleRecord, ["name", "location"])
|
||||
## generateLookup(TodoDB, TodoItem, ["owner", "priority"])
|
||||
##
|
||||
## will generate the following procedure:
|
||||
##
|
||||
## .. code-block:: Nim
|
||||
## proc findSampleRecordsByNameAndLocation*(db: SampleDB,
|
||||
## proc findTodoItemsByOwnerAndPriority*(db: SampleDB,
|
||||
## owner: string, priority: int): seq[TodoItem]
|
||||
let fieldNames = fields[1].mapIt($it)
|
||||
let procName = ident("find" & pluralize($modelType.getType[1]) & "By" & fieldNames.mapIt(it.capitalize).join("And"))
|
||||
|
||||
|
@ -325,6 +325,7 @@ proc typeOfColumn*(modelType: NimNode, colName: string): NimNode =
|
||||
proc isEmpty(val: int): bool = return val == 0
|
||||
proc isEmpty(val: UUID): bool = return val.isZero
|
||||
proc isEmpty(val: string): bool = return val.isEmptyOrWhitespace
|
||||
proc isEmpty[T](val: Option[T]): bool = return val.isNone
|
||||
|
||||
macro populateMutateClauses*(t: typed, newRecord: bool, mc: var MutateClauses): untyped =
|
||||
## Given a record type, create the datastructure used to generate SQL clauses
|
||||
|
Loading…
x
Reference in New Issue
Block a user