diff --git a/README.md b/README.md index 5dd3b1f..007537c 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/config.nims b/config.nims index a9fa5fd..e4af98a 100644 --- a/config.nims +++ b/config.nims @@ -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" diff --git a/ormin/db_utils.nim b/ormin/db_utils.nim index 0d0398b..2cef3f1 100644 --- a/ormin/db_utils.nim +++ b/ormin/db_utils.nim @@ -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) diff --git a/ormin/ormin_sqlite.nim b/ormin/ormin_sqlite.nim index c343e51..c62fd62 100644 --- a/ormin/ormin_sqlite.nim +++ b/ormin/ormin_sqlite.nim @@ -1,7 +1,7 @@ {.deadCodeElim: on.} -import json, times +import json, times, math, options import db_connector/db_common import db_connector/sqlite3 @@ -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") @@ -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) = diff --git a/tests/model_sqlite.sql b/tests/model_sqlite.sql index b6f4c0f..fb04a28 100644 --- a/tests/model_sqlite.sql +++ b/tests/model_sqlite.sql @@ -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 ); diff --git a/tests/tdb_utils.nim b/tests/tdb_utils.nim index 525c1ef..c0106e4 100644 --- a/tests/tdb_utils.nim +++ b/tests/tdb_utils.nim @@ -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:", "", "", "") diff --git a/tests/tfloatnull.nim b/tests/tfloatnull.nim new file mode 100644 index 0000000..3f4d0e1 --- /dev/null +++ b/tests/tfloatnull.nim @@ -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