Skip to content

Commit

Permalink
Throw exceptions when Qt type conversions fail; Work around
Browse files Browse the repository at this point in the history
nim-lang/Nim#5140, disabling automatic conversions of Qt types to
Nim types.
  • Loading branch information
philip-wernersbach committed Jun 30, 2017
1 parent 994e497 commit fbb0d7f
Show file tree
Hide file tree
Showing 12 changed files with 311 additions and 111 deletions.
41 changes: 37 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
Nim binding for Qt 5's Qt SQL library that integrates with the features of the
Nim language.

##Features
## Features
* Production-ready
* All of the features that you would expect from Qt SQL
* Such as a single API for multiple database engines
Expand All @@ -12,15 +12,48 @@ Nim language.
* For instance, instead of providing full bindings for `QVariant`, this
provides a minimal binding and lets C++'s type system take care of
converting to and from `QVariant` objects automatically.
* Abstracts certain Qt behaviors that are not Nim-like, and presents a
Nim-like API.

##Known Limitations
## Qt Type Conversions
Qt uses a system of default values, null properties, and invalid properties to
indicate the failure of a type conversion. The objects and values produced by
failed conversions often function like regular objects and values, but produce
unexpected results when used. This behavior leads to a silent failure
situation if the result of a Qt type conversion is not checked properly by the
program.

In Nim, most type conversion errors are not possible due to the type system
and compiler checks. When it is impossible for a type conversion error to be
checked at runtime, the Nim runtime and most Nim procedures will raise
exceptions, which prevents silent failure situations.

`nim-qt5_qtsql` abstracts the Qt behavior and changes it to the Nim behavior.
When possible, the binding checks for default values, null properties, and
invalid properties after Qt type conversions. If these are present, the
binding will throw a `QObjectConversionError`, which inherits from Nim's
`ObjectConversionError`. Default values, null properties, and invalid
properties are only checked for type conversions. Programs can still
purposefully create Qt objects with default values, null properties, and
invalid properties.

## Known Limitations
* Qt SQL does not deep copy `QSqlQuery` objects, so they must be kept in scope
on the stack during usage.
* Automatic conversions from Qt types to Nim types are temporarily disabled in
version 1.1.x of the binding.
* The Nim 0.17.0 compiler produces buggy code for procedures that return
C++ objects. This has been worked around, but the work around requires
changing `converter`s to `proc`s, which disables automatic conversion.
Manual conversion is still possible by calling the conversion procedures
in the binding.
* See [nim-lang/Nim#5140](https://github.com/nim-lang/Nim/issues/5140)
for more details.

##Usage
## Usage
See [`qt5_qtsql/tests/sqlcat.nim`](qt5_qtsql/tests/sqlcat.nim) for an example
program that reads and writes a SQLite database.

##License
## License
This project is licensed under the MIT License. For full license text, see
[`LICENSE`](LICENSE).
2 changes: 2 additions & 0 deletions qt5_qtsql.nim
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import qt5_qtsql/src/qsqlerror
import qt5_qtsql/src/qsqlrecord
import qt5_qtsql/src/qsqlquery
import qt5_qtsql/src/qsqldatabase
import qt5_qtsql/src/qobjectconversionerror

export immutablecstring
export qbytearray
Expand All @@ -46,3 +47,4 @@ export qsqlerror
export qsqlrecord
export qsqlquery
export qsqldatabase
export qobjectconversionerror
2 changes: 1 addition & 1 deletion qt5_qtsql.nimble
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[Package]
name = "qt5_qtsql"
version = "1.0.3"
version = "1.1.0"
author = "Philip Wernersbach <philip.wernersbach@gmail.com>"
description = "Binding for Qt 5's Qt SQL library. Provides a single API for multiple database engines."
license = "MIT"
Expand Down
6 changes: 5 additions & 1 deletion qt5_qtsql/src/qbytearray.nim
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
# SOFTWARE.

import immutablecstring
import qobjectconversionerror

const QBYTEARRAY_H = "<QtCore/QByteArray>"

Expand All @@ -34,9 +35,12 @@ proc isNull*(self: QByteArrayObj): bool {.header: QBYTEARRAY_H, importcpp: "isNu
proc isEmpty*(self: QByteArrayObj): bool {.header: QBYTEARRAY_H, importcpp: "isEmpty".}

#proc constDataUnsafe(self: QByteArrayObj): cstring {.header: QBYTEARRAY_H, importcpp: "constData".}
proc constData*(self: QByteArrayObj): immutablecstring =
proc constData*(self: QByteArrayObj): immutablecstring {.raises: [QObjectConversionError].} =
var mutableData: cstring

if unlikely(self.isNull == true):
raise newException(QObjectConversionError, "Failed to convert QByteArrayObj to immutablecstring, QByteArrayObj is null!")

{.emit: "`mutabledata` = (char *)`self`.constData();".}

return (mutableData: mutableData)
19 changes: 17 additions & 2 deletions qt5_qtsql/src/qdatetime.nim
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import qstring
import qtimezone
import qttimespec
import qobjectconversionerror

const QDATETIME_H = "<QtCore/QDateTime>"

Expand Down Expand Up @@ -57,8 +58,22 @@ template newQDateTimeObj*(msecs: qint64, timeSpec: QtTimeSpec): QDateTimeObj =

proc currentQDateTimeUtc*(): QDateTimeObj {.header: QDATETIME_H, importcpp: "QDateTime::currentDateTimeUtc".}

proc toQStringObj*(dateTime: QDateTimeObj, format: cstring): QStringObj {.header: QDATETIME_H, importcpp: "toString".}
proc toMSecsSinceEpoch*(dateTime: QDateTimeObj): qint64 {.header: QDATETIME_H, importcpp: "toMSecsSinceEpoch".}
proc internalToQStringObj*(dateTime: QDateTimeObj, format: cstring): QStringObj {.header: QDATETIME_H, importcpp: "toString".}
proc internalToMSecsSinceEpoch*(dateTime: QDateTimeObj): qint64 {.header: QDATETIME_H, importcpp: "toMSecsSinceEpoch".}

template toQStringObj*(dateTime: QDateTimeObj, format: cstring): QStringObj = #{.raises: [QObjectConversionError].} =
var result = dateTime.internalToQStringObj(format)

if unlikely(result.isEmpty == true):
raise newException(QObjectConversionError, "Failed to convert QDateTimeObj to QStringObj!")

result

proc toMSecsSinceEpoch*(dateTime: QDateTimeObj): qint64 {.raises: [QObjectConversionError].} =
if unlikely(dateTime.isValid == false):
raise newException(QObjectConversionError, "Failed to convert QDateTimeObj to qint64!")
else:
result = dateTime.internalToMSecsSinceEpoch

proc setTimeZone*(dateTime: QDateTimeObj, toZone: QTimeZoneObj) {.header: QDATETIME_H, importcpp: "setTimeZone".}
proc addMSecs*(a: var QDateTimeObj, b: qint64): QDateTimeObj {.header: QDATETIME_H, importcpp: "addMSecs".}
Expand Down
2 changes: 2 additions & 0 deletions qt5_qtsql/src/qobjectconversionerror.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
type
QObjectConversionError* = object of ObjectConversionError
76 changes: 46 additions & 30 deletions qt5_qtsql/src/qsqldatabase.nim
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,15 @@
import qbytearray
import qstring
import qsqlerror
import qobjectconversionerror

const QSQLDATABASE_H = "<QtSql/QSqlDatabase>"

type
QSqlDatabaseObj* {.final, header: QSQLDATABASE_H, importc: "QSqlDatabase".} = object
InvalidQSqlDatabaseException* = object of SystemError

proc isValid*(self: QSqlDatabaseObj): bool {.header: QSQLDATABASE_H, importcpp: "isValid".}

# uc stands for Unsafe Cast
template qSqlDatabaseProcWithOneArgThatReturnsVoid(typ: typedesc, name: expr, importcppname: string) =
Expand All @@ -39,10 +43,26 @@ template qSqlDatabaseProcWithOneArgThatReturnsVoid(typ: typedesc, name: expr, im
template qSqlDatabaseProcThatReturnsVoid(name: expr, importcppname: string) =
proc `name`*(self: QSqlDatabaseObj) {.header: QSQLDATABASE_H, importcpp: importcppname.}

proc qSqlDatabaseAddDatabase*(typ: cstring): QSqlDatabaseObj {.header: QSQLDATABASE_H, importcpp: "QSqlDatabase::addDatabase(@)".}
proc qSqlDatabaseAddDatabase*(typ: cstring, connectionName: cstring): QSqlDatabaseObj {.header: QSQLDATABASE_H, importcpp: "QSqlDatabase::addDatabase(@)".}
proc internalQSqlDatabaseAddDatabase*(typ: cstring): QSqlDatabaseObj {.header: QSQLDATABASE_H, importcpp: "QSqlDatabase::addDatabase(@)".}
proc internalQSqlDatabaseAddDatabase*(typ: cstring, connectionName: cstring): QSqlDatabaseObj {.header: QSQLDATABASE_H, importcpp: "QSqlDatabase::addDatabase(@)".}
proc qSqlDatabaseRemoveDatabase*(connectionName: cstring) {.header: QSQLDATABASE_H, importcpp: "QSqlDatabase::removeDatabase(@)".}

template qSqlDatabaseAddDatabase(typ: cstring): QSqlDatabaseObj = #{.raises: [InvalidQSqlDatabaseException].}=
var result = typ.internalQSqlDatabaseAddDatabase

if unlikely(result.isValid == false):
raise newException(InvalidQSqlDatabaseException, "QSqlDatabaseObj is invalid!")

result

template qSqlDatabaseAddDatabase(typ: cstring, connectionName: cstring): QSqlDatabaseObj = #{.raises: [InvalidQSqlDatabaseException].} =
var result = typ.internalQSqlDatabaseAddDatabase(connectionName)

if unlikely(result.isValid == false):
raise newException(InvalidQSqlDatabaseException, "QSqlDatabaseObj is invalid!")

result

#proc cppNew(other: QSqlDatabase): ptr QSqlDatabaseObj {.header: QSQLDATABASE_H, importcpp: "new QSqlDatabase::QSqlDatabase(@)".}
#proc cppDelete(self: ptr QSqlDatabaseObj) {.header: QSQLDATABASE_H, importcpp: "delete @".}

Expand Down Expand Up @@ -76,7 +96,15 @@ template newQSqlDatabase*(typ: cstring, connectionName: cstring): expr =
# self.up.cppDelete()
# self.up = nil

proc getQSqlDatabase*(connectionName: cstring, open = true): QSqlDatabaseObj {.header: QSQLDATABASE_H, importcpp: "QSqlDatabase::database(@)".}
proc internalGetQSqlDatabase*(connectionName: cstring, open = true): QSqlDatabaseObj {.header: QSQLDATABASE_H, importcpp: "QSqlDatabase::database(@)".}

template getQSqlDatabase*(connectionName: cstring, open = true): QSqlDatabaseObj = #{.raises: [InvalidQSqlDatabaseException].} =
var result = connectionName.internalGetQSqlDatabase(open)

if unlikely(result.isValid == false):
raise newException(InvalidQSqlDatabaseException, "QSqlDatabaseObj is invalid!")

result

proc internalOpen(self: QSqlDatabaseObj): bool {.header: QSQLDATABASE_H, importcpp: "open".}
proc internalOpen(self: QSqlDatabaseObj, user: cstring, password: cstring): bool {.header: QSQLDATABASE_H, importcpp: "open".}
Expand All @@ -86,53 +114,41 @@ proc internalTransaction(self: QSqlDatabaseObj): bool {.header: QSQLDATABASE_H,
proc internalCommit(self: QSqlDatabaseObj): bool {.header: QSQLDATABASE_H, importcpp: "commit".}
proc internalRollback(self: QSqlDatabaseObj): bool {.header: QSQLDATABASE_H, importcpp: "rollback"}

proc isValid*(self: QSqlDatabaseObj): bool {.header: QSQLDATABASE_H, importcpp: "isValid".}

template nativeErrorCodeCString*(self: QSqlErrorObj): expr =
self.nativeErrorCode().toUtf8().constData()

template textCString*(self: QSqlErrorObj): expr =
self.text().toUtf8().constData()

proc open*(self: var QSqlDatabaseObj): bool {.raises: [QSqlException], discardable.} =
let status = self.internalOpen()
proc open*(self: var QSqlDatabaseObj): bool {.raises: [QSqlException, QObjectConversionError], discardable.} =
result = self.internalOpen()

if status != true:
if unlikely(result == false):
raise newQSqlError(self.lastError())
else:
return true

proc open*(self: var QSqlDatabaseObj, user: cstring, password: cstring): bool {.raises: [QSqlException], discardable.} =
let status = self.internalOpen(user, password)
proc open*(self: var QSqlDatabaseObj, user: cstring, password: cstring): bool {.raises: [QSqlException, QObjectConversionError], discardable.} =
result = self.internalOpen(user, password)

if status != true:
if unlikely(result == false):
raise newQSqlError(self.lastError())
else:
return true

proc beginTransaction*(self: var QSqlDatabaseObj): bool {.raises: [QSqlException], discardable.} =
let status = self.internalTransaction()
proc beginTransaction*(self: var QSqlDatabaseObj): bool {.raises: [QSqlException, QObjectConversionError], discardable.} =
result = self.internalTransaction()

if status != true:
if unlikely(result == false):
raise newQSqlError(self.lastError())
else:
return true

proc commitTransaction*(self: var QSqlDatabaseObj): bool {.raises: [QSqlException], discardable.} =
let status = self.internalCommit()
proc commitTransaction*(self: var QSqlDatabaseObj): bool {.raises: [QSqlException, QObjectConversionError], discardable.} =
result = self.internalCommit()

if status != true:
if unlikely(result == false):
raise newQSqlError(self.lastError())
else:
return true

proc rollback*(self: var QSqlDatabaseObj): bool {.raises: [QSqlException], discardable.} =
let status = self.internalRollback()
proc rollback*(self: var QSqlDatabaseObj): bool {.raises: [QSqlException, QObjectConversionError], discardable.} =
result = self.internalRollback()

if status != true:
if unlikely(result == false):
raise newQSqlError(self.lastError())
else:
return true

qSqlDatabaseProcWithOneArgThatReturnsVoid(cstring, setHostName, "setHostName")
qSqlDatabaseProcWithOneArgThatReturnsVoid(cint, setPort, "setPort")
Expand Down
59 changes: 39 additions & 20 deletions qt5_qtsql/src/qsqlquery.nim
Original file line number Diff line number Diff line change
Expand Up @@ -28,16 +28,15 @@ import qvariant
import qstring
import qsqlerror
import qsqldatabase
import qobjectconversionerror

const QSQLQUERY_H = "<QtSql/QSqlQuery>"

type
QSqlQueryObj* {.final, header: QSQLQUERY_H, importc: "QSqlQuery".} = object

proc qSqlQuery*(db: var QSqlDatabaseObj): QSqlQueryObj {.header: QSQLQUERY_H, importcpp: "QSqlQuery::QSqlQuery(@)".}
proc newQSqlQuery*(query: cstring, db: var QSqlDatabaseObj): QSqlQueryObj {.header: QSQLQUERY_H, importcpp: "QSqlQuery::QSqlQuery(@)"}
proc qSqlQuery*(db: var QSqlDatabaseObj, query: cstring): QSqlQueryObj =
newQSqlQuery(query, db)
proc qSqlQuery*(query: cstring, db: var QSqlDatabaseObj): QSqlQueryObj {.header: QSQLQUERY_H, importcpp: "QSqlQuery::QSqlQuery(@)"}

proc internalPrepare(self: QSqlQueryObj, query: cstring): bool {.header: QSQLQUERY_H, importcpp: "prepare".}

Expand All @@ -59,33 +58,53 @@ proc internalExec(self: QSqlQueryObj): bool {.header: QSQLQUERY_H, importcpp: "e

proc lastError*(self: QSqlQueryObj): QSqlErrorObj {.header: QSQLQUERY_H, importcpp: "lastError".}

proc exec*(self: var QSqlQueryObj, query: cstring): bool {.raises: [QSqlException], discardable.} =
let status = self.internalExec(query)
proc exec*(self: var QSqlQueryObj, query: cstring): bool {.raises: [QSqlException, QObjectConversionError], discardable.} =
result = self.internalExec(query)

if status != true:
if unlikely(result == false):
raise newQSqlError(self.lastError())
else:
return true

proc exec*(self: var QSqlQueryObj): bool {.raises: [QSqlException], discardable.} =
let status = self.internalExec()
proc exec*(self: var QSqlQueryObj): bool {.raises: [QSqlException, QObjectConversionError], discardable.} =
result = self.internalExec()

if status != true:
if unlikely(result == false):
raise newQSqlError(self.lastError())
else:
return true

proc prepare*(self: var QSqlQueryObj, query: cstring): bool {.raises: [QSqlException], discardable.} =
let status = self.internalPrepare(query)
proc prepare*(self: var QSqlQueryObj, query: cstring): bool {.raises: [QSqlException, QObjectConversionError], discardable.} =
result = self.internalPrepare(query)

if status != true:
if unlikely(result == false):
raise newQSqlError(self.lastError())
else:
return true

proc internalValue*(self: QSqlQueryObj, index: cint): QVariantObj {.header: QSQLQUERY_H, importcpp: "value".}
proc internalValue*(self: QSqlQueryObj, index: cstring): QVariantObj {.header: QSQLQUERY_H, importcpp: "value".}

proc next*(self: QSqlQueryObj): bool {.header: QSQLQUERY_H, importcpp: "next".}
proc value*(self: QSqlQueryObj, index: cint): QVariantObj {.header: QSQLQUERY_H, importcpp: "value".}
proc value*(self: QSqlQueryObj, index: cstring): QVariantObj {.header: QSQLQUERY_H, importcpp: "value".}

when compileOption("boundChecks"):
template value*(query: QSqlQueryObj, index: cint): QVariantObj = #{.raises: [IndexError].} =
var result = query.internalValue(index)

if unlikely(result.isValid == false):
raise newException(IndexError, "Field " & $(index + 1) & " requested from current query record, but the field is invalid!")

result

template value*(query: QSqlQueryObj, name: cstring): QVariantObj = #{.raises: [IndexError].} =
var result = query.internalValue(name)

if unlikely(result.isValid == false):
raise newException(IndexError, "Field with name \"" & $name & "\" requested from current query record, but the field is invalid!")

result
else:
template value*(query: QSqlQueryObj, index: cint): QVariantObj =
query.internalValue(index)

template value*(query: QSqlQueryObj, name: cstring): QVariantObj =
query.internalValue(name)

proc isNull*(self: QSqlQueryObj, index: cint): bool {.header: QSQLQUERY_H, importcpp: "isNull".}
proc isNull*(self: QSqlQueryObj, name: cstring): bool {.header: QSQLQUERY_H, importcpp: "isNull".}
proc isValid*(self: QSqlQueryObj): bool {.header: QSQLQUERY_H, importcpp: "isValid".}
proc isActive*(self: QSqlQueryObj): bool {.header: QSQLQUERY_H, importcpp: "isActive".}
Loading

0 comments on commit fbb0d7f

Please sign in to comment.