Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,12 @@ importModel(DbBackend.sqlite, "model_sqlite")
let db {.global.} = open(":memory:", "", "", "")
```

Note: Ormin now properly handles quoted table names in `dropTable`. The compile flag `-d:orminLegacySqliteDropNames` restores that older drop-table behavior by using the normalized lookup name instead of the preserved SQL identifier. The old behavior only worked in SQlite, not Postgres.
SQLite float null handling is controlled by compile flag:

Use compile flag `-d:ormin.sqliteNullFloatAsNaN` to opt in to mapping SQLite `NULL` float values to `NaN`.
Without the flag, nullable float reads keep the legacy behavior (`NULL` reads as `0.0`).

Note: Ormin now properly handles quoted table names in `dropTable`. The compile flag `-d:ormin.sqliteLegacyDropNames` restores that older drop-table behavior by using the normalized lookup name instead of the preserved SQL identifier. The old behavior only worked in SQlite, not Postgres.

### PostgreSQL

Expand Down
2 changes: 2 additions & 0 deletions config.nims
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ task test, "Run all test suite":

exec "nim c -f -r tests/tfeature"
exec "nim c -f -r tests/tcommon"
exec "nim c -f -r tests/tfloatnull"
exec "nim c -f -d:ormin.sqliteNullFloatAsNaN -r tests/tfloatnull"
exec "nim c -f -r tests/tsqlite"
exec "nim c -f -r tests/tdb_utils"
exec "nim c -f -r tests/timportstatic"
Expand Down
3 changes: 2 additions & 1 deletion ormin/db_utils.nim
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,8 @@ proc quoteDbIdentifier*(name: string): DbId =
proc dropTableName(db: DbConn; tableName, lookupName: string) =
# SQL parameters bind values, not identifiers, so DROP TABLE needs the
# identifier rendered into the statement text with correct quoting.
when defined(sqlite) and defined(orminLegacySqliteDropNames):
when defined(sqlite) and
(defined(orminLegacySqliteDropNames) or defined(ormin.sqliteLegacyDropNames)):
db.exec(sql("drop table if exists " & lookupName))
else:
let tableIdentSql = quoteDbIdentifier(tableName)
Expand Down
23 changes: 19 additions & 4 deletions ormin/ormin_sqlite.nim
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@

{.deadCodeElim: on.}

import json, times
import json, times, math, options

import db_connector/db_common
import db_connector/sqlite3
Expand Down Expand Up @@ -53,8 +53,17 @@ template bindParam*(db: DbConn; s: PStmt; idx: int; x, t: untyped) =
if bind_text(s, idx.cint, cstring(x), x.len.cint, SQLITE_STATIC) != SQLITE_OK:
dbError(db)
elif t is float64:
if bind_double(s, idx.cint, x) != SQLITE_OK:
dbError(db)
let xf = x
when xf is Option[SomeFloat]:
if xf.isSome:
if bind_double(s, idx.cint, xf.get.float64) != SQLITE_OK:
dbError(db)
else:
if bind_null(s, idx.cint) != SQLITE_OK:
dbError(db)
else:
if bind_double(s, idx.cint, xf) != SQLITE_OK:
dbError(db)
elif t is DateTime:
let xx = if x.nanosecond > 0:
x.utc().format("yyyy-MM-dd HH:mm:ss\'.\'fff")
Expand Down Expand Up @@ -161,7 +170,13 @@ template bindResult*(db: DbConn; s: PStmt; idx: int; dest: var blobType;

template bindResult*(db: DbConn; s: PStmt; idx: int; dest: float64;
t: typedesc; name: string) =
dest = column_double(s, idx.cint)
when defined(orminSqliteNullFloatAsNaN) or defined(ormin.sqliteNullFloatAsNaN):
if column_type(s, idx.cint) == SQLITE_NULL:
dest = NaN
else:
dest = column_double(s, idx.cint)
else:
dest = column_double(s, idx.cint)

template bindResult*(db: DbConn; s: PStmt; idx: int; dest: bool;
t: typedesc; name: string) =
Expand Down
5 changes: 5 additions & 0 deletions tests/model_sqlite.sql
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ create table if not exists tb_float(
typfloat real not null
);

create table if not exists tb_nullable_float(
id integer primary key,
typfloat real null
);

create table if not exists tb_string(
typstring text not null
);
Expand Down
4 changes: 3 additions & 1 deletion tests/tdb_utils.nim
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ import db_connector/db_common
from db_connector/db_sqlite import open, exec, getValue
import ormin/db_utils

const usesLegacySqliteDropNames = defined(sqlite) and defined(orminLegacySqliteDropNames)
const usesLegacySqliteDropNames =
defined(sqlite) and
(defined(orminLegacySqliteDropNames) or defined(ormin.sqliteLegacyDropNames))

let
db {.global.} = open(":memory:", "", "", "")
Expand Down
68 changes: 68 additions & 0 deletions tests/tfloatnull.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import unittest, json, options, os
import ormin
import ormin/db_utils
from db_connector/db_sqlite import getValue

const backend = DbBackend.sqlite
importModel(backend, "model_sqlite")

let
db {.global.} = open(":memory:", "", "", "")
testDir = currentSourcePath.parentDir()
sqlFile = Path(testDir / "model_sqlite.sql")

suite "sqlite float nullable behavior":
setup:
db.dropTable(sqlFile, "tb_nullable_float")
db.createTable(sqlFile, "tb_nullable_float")

test "sqlite stores nan as null and float reads follow compile mode":
let nanValue = NaN
query:
insert tb_nullable_float(id = 1, typfloat = 1.25)
query:
insert tb_nullable_float(id = 2, typfloat = null)
query:
insert tb_nullable_float(id = 3, typfloat = ?nanValue)

check db.getValue(sql"select count(*) from tb_nullable_float where typfloat is null") == "2"

let res = query:
select tb_nullable_float(typfloat)
orderby id
check res.len == 3
check res[0] == 1.25
when defined(orminSqliteNullFloatAsNaN) or defined(ormin.sqliteNullFloatAsNaN):
check res[1] != res[1]
check res[2] != res[2]
else:
check res[1] == 0.0
check res[2] == 0.0

test "option_float placeholder maps none to null":
let someValue = some(4.5)
let noneValue = none(float)
query:
insert tb_nullable_float(id = 10, typfloat = ?someValue)
query:
insert tb_nullable_float(id = 11, typfloat = ?noneValue)

let res = query:
select tb_nullable_float(typfloat)
where id in {10, 11}
orderby id
check res.len == 2
check res[0] == 4.5
when defined(orminSqliteNullFloatAsNaN) or defined(ormin.sqliteNullFloatAsNaN):
check res[1] != res[1]
else:
check res[1] == 0.0

test "json preserves null for nullable float":
query:
insert tb_nullable_float(id = 20, typfloat = null)
let res = query:
select tb_nullable_float(id, typfloat)
where id == 20
produce json
check res[0]["typfloat"].kind == JNull
Loading