diff options
Diffstat (limited to 'lib/impure')
-rw-r--r-- | lib/impure/db_mysql.nim | 424 | ||||
-rw-r--r-- | lib/impure/db_odbc.nim | 532 | ||||
-rw-r--r-- | lib/impure/db_postgres.nim | 590 | ||||
-rw-r--r-- | lib/impure/db_sqlite.nim | 950 | ||||
-rw-r--r-- | lib/impure/nre.nim | 178 | ||||
-rw-r--r-- | lib/impure/nre/private/util.nim | 2 | ||||
-rw-r--r-- | lib/impure/rdstdin.nim | 21 | ||||
-rw-r--r-- | lib/impure/re.nim | 121 |
8 files changed, 190 insertions, 2628 deletions
diff --git a/lib/impure/db_mysql.nim b/lib/impure/db_mysql.nim deleted file mode 100644 index 242ea1b0d..000000000 --- a/lib/impure/db_mysql.nim +++ /dev/null @@ -1,424 +0,0 @@ -# -# -# Nim's Runtime Library -# (c) Copyright 2015 Andreas Rumpf -# -# See the file "copying.txt", included in this -# distribution, for details about the copyright. -# - -## A higher level `mySQL`:idx: database wrapper. The same interface is -## implemented for other databases too. -## -## See also: `db_odbc <db_odbc.html>`_, `db_sqlite <db_sqlite.html>`_, -## `db_postgres <db_postgres.html>`_. -## -## Parameter substitution -## ====================== -## -## All `db_*` modules support the same form of parameter substitution. -## That is, using the `?` (question mark) to signify the place where a -## value should be placed. For example: -## -## .. code-block:: Nim -## sql"INSERT INTO myTable (colA, colB, colC) VALUES (?, ?, ?)" -## -## -## Examples -## ======== -## -## Opening a connection to a database -## ---------------------------------- -## -## .. code-block:: Nim -## import std/db_mysql -## let db = open("localhost", "user", "password", "dbname") -## db.close() -## -## Creating a table -## ---------------- -## -## .. code-block:: Nim -## db.exec(sql"DROP TABLE IF EXISTS myTable") -## db.exec(sql("""CREATE TABLE myTable ( -## id integer, -## name varchar(50) not null)""")) -## -## Inserting data -## -------------- -## -## .. code-block:: Nim -## db.exec(sql"INSERT INTO myTable (id, name) VALUES (0, ?)", -## "Dominik") -## -## Larger example -## -------------- -## -## .. code-block:: Nim -## -## import std/[db_mysql, math] -## -## let theDb = open("localhost", "nim", "nim", "test") -## -## theDb.exec(sql"Drop table if exists myTestTbl") -## theDb.exec(sql("create table myTestTbl (" & -## " Id INT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY, " & -## " Name VARCHAR(50) NOT NULL, " & -## " i INT(11), " & -## " f DECIMAL(18,10))")) -## -## theDb.exec(sql"START TRANSACTION") -## for i in 1..1000: -## theDb.exec(sql"INSERT INTO myTestTbl (name,i,f) VALUES (?,?,?)", -## "Item#" & $i, i, sqrt(i.float)) -## theDb.exec(sql"COMMIT") -## -## for x in theDb.fastRows(sql"select * from myTestTbl"): -## echo x -## -## let id = theDb.tryInsertId(sql"INSERT INTO myTestTbl (name,i,f) VALUES (?,?,?)", -## "Item#1001", 1001, sqrt(1001.0)) -## echo "Inserted item: ", theDb.getValue(sql"SELECT name FROM myTestTbl WHERE id=?", id) -## -## theDb.close() - - -import strutils, mysql - -import db_common -export db_common - -import std/private/since - -type - DbConn* = distinct PMySQL ## encapsulates a database connection - Row* = seq[string] ## a row of a dataset. NULL database values will be - ## converted to nil. - InstantRow* = object ## a handle that can be used to get a row's - ## column text on demand - row: cstringArray - len: int - -proc dbError*(db: DbConn) {.noreturn.} = - ## raises a DbError exception. - var e: ref DbError - new(e) - e.msg = $mysql.error(PMySQL db) - raise e - -when false: - proc dbQueryOpt*(db: DbConn, query: string, args: varargs[string, `$`]) = - var stmt = mysql_stmt_init(db) - if stmt == nil: dbError(db) - if mysql_stmt_prepare(stmt, query, len(query)) != 0: - dbError(db) - var - binding: seq[MYSQL_BIND] - discard mysql_stmt_close(stmt) - -proc dbQuote*(s: string): string = - ## DB quotes the string. - result = newStringOfCap(s.len + 2) - result.add "'" - for c in items(s): - # see https://cheatsheetseries.owasp.org/cheatsheets/SQL_Injection_Prevention_Cheat_Sheet.html#mysql-escaping - case c - of '\0': result.add "\\0" - of '\b': result.add "\\b" - of '\t': result.add "\\t" - of '\l': result.add "\\n" - of '\r': result.add "\\r" - of '\x1a': result.add "\\Z" - of '"': result.add "\\\"" - of '\'': result.add "\\'" - of '\\': result.add "\\\\" - of '_': result.add "\\_" - else: result.add c - add(result, '\'') - -proc dbFormat(formatstr: SqlQuery, args: varargs[string]): string = - result = "" - var a = 0 - for c in items(string(formatstr)): - if c == '?': - add(result, dbQuote(args[a])) - inc(a) - else: - add(result, c) - -proc tryExec*(db: DbConn, query: SqlQuery, args: varargs[string, `$`]): bool {. - tags: [ReadDbEffect, WriteDbEffect].} = - ## tries to execute the query and returns true if successful, false otherwise. - var q = dbFormat(query, args) - return mysql.realQuery(PMySQL db, q, q.len) == 0'i32 - -proc rawExec(db: DbConn, query: SqlQuery, args: varargs[string, `$`]) = - var q = dbFormat(query, args) - if mysql.realQuery(PMySQL db, q, q.len) != 0'i32: dbError(db) - -proc exec*(db: DbConn, query: SqlQuery, args: varargs[string, `$`]) {. - tags: [ReadDbEffect, WriteDbEffect].} = - ## executes the query and raises EDB if not successful. - var q = dbFormat(query, args) - if mysql.realQuery(PMySQL db, q, q.len) != 0'i32: dbError(db) - -proc newRow(L: int): Row = - newSeq(result, L) - for i in 0..L-1: result[i] = "" - -proc properFreeResult(sqlres: mysql.PRES, row: cstringArray) = - if row != nil: - while mysql.fetchRow(sqlres) != nil: discard - mysql.freeResult(sqlres) - -iterator fastRows*(db: DbConn, query: SqlQuery, - args: varargs[string, `$`]): Row {.tags: [ReadDbEffect].} = - ## executes the query and iterates over the result dataset. - ## - ## This is very fast, but potentially dangerous. Use this iterator only - ## if you require **ALL** the rows. - ## - ## Breaking the fastRows() iterator during a loop will cause the next - ## database query to raise an [EDb] exception `Commands out of sync`. - rawExec(db, query, args) - var sqlres = mysql.useResult(PMySQL db) - if sqlres != nil: - var - L = int(mysql.numFields(sqlres)) - row: cstringArray - result: Row - backup: Row - newSeq(result, L) - while true: - row = mysql.fetchRow(sqlres) - if row == nil: break - for i in 0..L-1: - setLen(result[i], 0) - result[i].add row[i] - yield result - properFreeResult(sqlres, row) - -iterator instantRows*(db: DbConn, query: SqlQuery, - args: varargs[string, `$`]): InstantRow - {.tags: [ReadDbEffect].} = - ## Same as fastRows but returns a handle that can be used to get column text - ## on demand using []. Returned handle is valid only within the iterator body. - rawExec(db, query, args) - var sqlres = mysql.useResult(PMySQL db) - if sqlres != nil: - let L = int(mysql.numFields(sqlres)) - var row: cstringArray - while true: - row = mysql.fetchRow(sqlres) - if row == nil: break - yield InstantRow(row: row, len: L) - properFreeResult(sqlres, row) - -proc setTypeName(t: var DbType; f: PFIELD) = - t.name = $f.name - t.maxReprLen = Natural(f.max_length) - if (NOT_NULL_FLAG and f.flags) != 0: t.notNull = true - case f.ftype - of TYPE_DECIMAL: - t.kind = dbDecimal - of TYPE_TINY: - t.kind = dbInt - t.size = 1 - of TYPE_SHORT: - t.kind = dbInt - t.size = 2 - of TYPE_LONG: - t.kind = dbInt - t.size = 4 - of TYPE_FLOAT: - t.kind = dbFloat - t.size = 4 - of TYPE_DOUBLE: - t.kind = dbFloat - t.size = 8 - of TYPE_NULL: - t.kind = dbNull - of TYPE_TIMESTAMP: - t.kind = dbTimestamp - of TYPE_LONGLONG: - t.kind = dbInt - t.size = 8 - of TYPE_INT24: - t.kind = dbInt - t.size = 3 - of TYPE_DATE: - t.kind = dbDate - of TYPE_TIME: - t.kind = dbTime - of TYPE_DATETIME: - t.kind = dbDatetime - of TYPE_YEAR: - t.kind = dbDate - of TYPE_NEWDATE: - t.kind = dbDate - of TYPE_VARCHAR, TYPE_VAR_STRING, TYPE_STRING: - t.kind = dbVarchar - of TYPE_BIT: - t.kind = dbBit - of TYPE_NEWDECIMAL: - t.kind = dbDecimal - of TYPE_ENUM: t.kind = dbEnum - of TYPE_SET: t.kind = dbSet - of TYPE_TINY_BLOB, TYPE_MEDIUM_BLOB, TYPE_LONG_BLOB, - TYPE_BLOB: t.kind = dbBlob - of TYPE_GEOMETRY: - t.kind = dbGeometry - -proc setColumnInfo(columns: var DbColumns; res: PRES; L: int) = - setLen(columns, L) - for i in 0..<L: - let fp = mysql.fetch_field_direct(res, cint(i)) - setTypeName(columns[i].typ, fp) - columns[i].name = $fp.name - columns[i].tableName = $fp.table - columns[i].primaryKey = (fp.flags and PRI_KEY_FLAG) != 0 - #columns[i].foreignKey = there is no such thing in mysql - -iterator instantRows*(db: DbConn; columns: var DbColumns; query: SqlQuery; - args: varargs[string, `$`]): InstantRow = - ## Same as fastRows but returns a handle that can be used to get column text - ## on demand using []. Returned handle is valid only within the iterator body. - rawExec(db, query, args) - var sqlres = mysql.useResult(PMySQL db) - if sqlres != nil: - let L = int(mysql.numFields(sqlres)) - setColumnInfo(columns, sqlres, L) - var row: cstringArray - while true: - row = mysql.fetchRow(sqlres) - if row == nil: break - yield InstantRow(row: row, len: L) - properFreeResult(sqlres, row) - - -proc `[]`*(row: InstantRow, col: int): string {.inline.} = - ## Returns text for given column of the row. - $row.row[col] - -proc unsafeColumnAt*(row: InstantRow, index: int): cstring {.inline.} = - ## Return cstring of given column of the row - row.row[index] - -proc len*(row: InstantRow): int {.inline.} = - ## Returns number of columns in the row. - row.len - -proc getRow*(db: DbConn, query: SqlQuery, - args: varargs[string, `$`]): Row {.tags: [ReadDbEffect].} = - ## Retrieves a single row. If the query doesn't return any rows, this proc - ## will return a Row with empty strings for each column. - rawExec(db, query, args) - var sqlres = mysql.useResult(PMySQL db) - if sqlres != nil: - var L = int(mysql.numFields(sqlres)) - result = newRow(L) - var row = mysql.fetchRow(sqlres) - if row != nil: - for i in 0..L-1: - setLen(result[i], 0) - add(result[i], row[i]) - properFreeResult(sqlres, row) - -proc getAllRows*(db: DbConn, query: SqlQuery, - args: varargs[string, `$`]): seq[Row] {.tags: [ReadDbEffect].} = - ## executes the query and returns the whole result dataset. - result = @[] - rawExec(db, query, args) - var sqlres = mysql.useResult(PMySQL db) - if sqlres != nil: - var L = int(mysql.numFields(sqlres)) - var row: cstringArray - var j = 0 - while true: - row = mysql.fetchRow(sqlres) - if row == nil: break - setLen(result, j+1) - newSeq(result[j], L) - for i in 0..L-1: - result[j][i] = $row[i] - inc(j) - mysql.freeResult(sqlres) - -iterator rows*(db: DbConn, query: SqlQuery, - args: varargs[string, `$`]): Row {.tags: [ReadDbEffect].} = - ## same as `fastRows`, but slower and safe. - for r in items(getAllRows(db, query, args)): yield r - -proc getValue*(db: DbConn, query: SqlQuery, - args: varargs[string, `$`]): string {.tags: [ReadDbEffect].} = - ## executes the query and returns the first column of the first row of the - ## result dataset. Returns "" if the dataset contains no rows or the database - ## value is NULL. - result = getRow(db, query, args)[0] - -proc tryInsertId*(db: DbConn, query: SqlQuery, - args: varargs[string, `$`]): int64 {.tags: [WriteDbEffect].} = - ## executes the query (typically "INSERT") and returns the - ## generated ID for the row or -1 in case of an error. - var q = dbFormat(query, args) - if mysql.realQuery(PMySQL db, q, q.len) != 0'i32: - result = -1'i64 - else: - result = mysql.insertId(PMySQL db) - -proc insertId*(db: DbConn, query: SqlQuery, - args: varargs[string, `$`]): int64 {.tags: [WriteDbEffect].} = - ## executes the query (typically "INSERT") and returns the - ## generated ID for the row. - result = tryInsertID(db, query, args) - if result < 0: dbError(db) - -proc tryInsert*(db: DbConn, query: SqlQuery, pkName: string, - args: varargs[string, `$`]): int64 - {.tags: [WriteDbEffect], raises: [], since: (1, 3).} = - ## same as tryInsertID - tryInsertID(db, query, args) - -proc insert*(db: DbConn, query: SqlQuery, pkName: string, - args: varargs[string, `$`]): int64 - {.tags: [WriteDbEffect], since: (1, 3).} = - ## same as insertId - result = tryInsert(db, query,pkName, args) - if result < 0: dbError(db) - -proc execAffectedRows*(db: DbConn, query: SqlQuery, - args: varargs[string, `$`]): int64 {. - tags: [ReadDbEffect, WriteDbEffect].} = - ## runs the query (typically "UPDATE") and returns the - ## number of affected rows - rawExec(db, query, args) - result = mysql.affectedRows(PMySQL db) - -proc close*(db: DbConn) {.tags: [DbEffect].} = - ## closes the database connection. - if PMySQL(db) != nil: mysql.close(PMySQL db) - -proc open*(connection, user, password, database: string): DbConn {. - tags: [DbEffect].} = - ## opens a database connection. Raises `EDb` if the connection could not - ## be established. - var res = mysql.init(nil) - if res == nil: dbError("could not open database connection") - let - colonPos = connection.find(':') - host = if colonPos < 0: connection - else: substr(connection, 0, colonPos-1) - port: int32 = if colonPos < 0: 0'i32 - else: substr(connection, colonPos+1).parseInt.int32 - if mysql.realConnect(res, host, user, password, database, - port, nil, 0) == nil: - var errmsg = $mysql.error(res) - mysql.close(res) - dbError(errmsg) - result = DbConn(res) - -proc setEncoding*(connection: DbConn, encoding: string): bool {. - tags: [DbEffect].} = - ## sets the encoding of a database connection, returns true for - ## success, false for failure. - result = mysql.set_character_set(PMySQL connection, encoding) == 0 diff --git a/lib/impure/db_odbc.nim b/lib/impure/db_odbc.nim deleted file mode 100644 index 1e4032b34..000000000 --- a/lib/impure/db_odbc.nim +++ /dev/null @@ -1,532 +0,0 @@ -# -# -# Nim's Runtime Library -# (c) Copyright 2015 Nim Contributors -# -# See the file "copying.txt", included in this -# distribution, for details about the copyright. -# - -## A higher level `ODBC` database wrapper. -## -## This is the same interface that is implemented for other databases. -## -## This has NOT yet been (extensively) tested against ODBC drivers for -## Teradata, Oracle, Sybase, MSSqlvSvr, et. al. databases. -## -## Currently all queries are ANSI calls, not Unicode. -## -## See also: `db_postgres <db_postgres.html>`_, `db_sqlite <db_sqlite.html>`_, -## `db_mysql <db_mysql.html>`_. -## -## Parameter substitution -## ====================== -## -## All `db_*` modules support the same form of parameter substitution. -## That is, using the `?` (question mark) to signify the place where a -## value should be placed. For example: -## -## .. code-block:: Nim -## sql"INSERT INTO myTable (colA, colB, colC) VALUES (?, ?, ?)" -## -## -## Examples -## ======== -## -## Opening a connection to a database -## ---------------------------------- -## -## .. code-block:: Nim -## import std/db_odbc -## var db = open("localhost", "user", "password", "dbname") -## db.close() -## -## Creating a table -## ---------------- -## -## .. code-block:: Nim -## db.exec(sql"DROP TABLE IF EXISTS myTable") -## db.exec(sql("""CREATE TABLE myTable ( -## id integer, -## name varchar(50) not null)""")) -## -## Inserting data -## -------------- -## -## .. code-block:: Nim -## db.exec(sql"INSERT INTO myTable (id, name) VALUES (0, ?)", -## "Andreas") -## -## Large example -## ------------- -## -## .. code-block:: Nim -## -## import std/[db_odbc, math] -## -## var theDb = open("localhost", "nim", "nim", "test") -## -## theDb.exec(sql"Drop table if exists myTestTbl") -## theDb.exec(sql("create table myTestTbl (" & -## " Id INT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY, " & -## " Name VARCHAR(50) NOT NULL, " & -## " i INT(11), " & -## " f DECIMAL(18,10))")) -## -## theDb.exec(sql"START TRANSACTION") -## for i in 1..1000: -## theDb.exec(sql"INSERT INTO myTestTbl (name,i,f) VALUES (?,?,?)", -## "Item#" & $i, i, sqrt(i.float)) -## theDb.exec(sql"COMMIT") -## -## for x in theDb.fastRows(sql"select * from myTestTbl"): -## echo x -## -## let id = theDb.tryInsertId(sql"INSERT INTO myTestTbl (name,i,f) VALUES (?,?,?)", -## "Item#1001", 1001, sqrt(1001.0)) -## echo "Inserted item: ", theDb.getValue(sql"SELECT name FROM myTestTbl WHERE id=?", id) -## -## theDb.close() - -import strutils, odbcsql -import db_common -export db_common - -import std/private/since - -type - OdbcConnTyp = tuple[hDb: SqlHDBC, env: SqlHEnv, stmt: SqlHStmt] - DbConn* = OdbcConnTyp ## encapsulates a database connection - Row* = seq[string] ## a row of a dataset. NULL database values will be - ## converted to nil. - InstantRow* = tuple[row: seq[string], len: int] ## a handle that can be - ## used to get a row's - ## column text on demand - -var - buf: array[0..4096, char] - -proc properFreeResult(hType: int, sqlres: var SqlHandle) {. - tags: [WriteDbEffect], raises: [].} = - try: - discard SQLFreeHandle(hType.TSqlSmallInt, sqlres) - sqlres = nil - except: discard - -proc getErrInfo(db: var DbConn): tuple[res: int, ss, ne, msg: string] {. - tags: [ReadDbEffect], raises: [].} = - ## Returns ODBC error information - var - sqlState: array[0..512, char] - nativeErr: array[0..512, char] - errMsg: array[0..512, char] - retSz: TSqlSmallInt = 0 - res: TSqlSmallInt = 0 - try: - sqlState[0] = '\0' - nativeErr[0] = '\0' - errMsg[0] = '\0' - res = SQLErr(db.env, db.hDb, db.stmt, - cast[PSQLCHAR](sqlState.addr), - cast[PSQLCHAR](nativeErr.addr), - cast[PSQLCHAR](errMsg.addr), - 511.TSqlSmallInt, retSz.addr) - except: - discard - return (res.int, $(addr sqlState), $(addr nativeErr), $(addr errMsg)) - -proc dbError*(db: var DbConn) {. - tags: [ReadDbEffect, WriteDbEffect], raises: [DbError] .} = - ## Raises an `[DbError]` exception with ODBC error information - var - e: ref DbError - ss, ne, msg: string = "" - isAnError = false - res: int = 0 - prevSs = "" - while true: - prevSs = ss - (res, ss, ne, msg) = db.getErrInfo() - if prevSs == ss: - break - # sqlState of 00000 is not an error - elif ss == "00000": - break - elif ss == "01000": - echo "\nWarning: ", ss, " ", msg - continue - else: - isAnError = true - echo "\nError: ", ss, " ", msg - if isAnError: - new(e) - e.msg = "ODBC Error" - if db.stmt != nil: - properFreeResult(SQL_HANDLE_STMT, db.stmt) - properFreeResult(SQL_HANDLE_DBC, db.hDb) - properFreeResult(SQL_HANDLE_ENV, db.env) - raise e - -proc sqlCheck(db: var DbConn, resVal: TSqlSmallInt) {.raises: [DbError]} = - ## Wrapper that raises [EDb] if `resVal` is neither SQL_SUCCESS or SQL_NO_DATA - if resVal notIn [SQL_SUCCESS, SQL_NO_DATA]: dbError(db) - -proc sqlGetDBMS(db: var DbConn): string {. - tags: [ReadDbEffect, WriteDbEffect], raises: [] .} = - ## Returns the ODBC SQL_DBMS_NAME string - const - SQL_DBMS_NAME = 17.SqlUSmallInt - var - sz: TSqlSmallInt = 0 - buf[0] = '\0' - try: - db.sqlCheck(SQLGetInfo(db.hDb, SQL_DBMS_NAME, cast[SqlPointer](buf.addr), - 4095.TSqlSmallInt, sz.addr)) - except: discard - return $(addr buf) - -proc dbQuote*(s: string): string {.noSideEffect.} = - ## DB quotes the string. - result = "'" - for c in items(s): - if c == '\'': add(result, "''") - else: add(result, c) - add(result, '\'') - -proc dbFormat(formatstr: SqlQuery, args: varargs[string]): string {. - noSideEffect.} = - ## Replace any `?` placeholders with `args`, - ## and quotes the arguments - result = "" - var a = 0 - for c in items(string(formatstr)): - if c == '?': - add(result, dbQuote(args[a])) - inc(a) - else: - add(result, c) - -proc prepareFetch(db: var DbConn, query: SqlQuery, - args: varargs[string, `$`]): TSqlSmallInt {. - tags: [ReadDbEffect, WriteDbEffect], raises: [DbError].} = - # Prepare a statement, execute it and fetch the data to the driver - # ready for retrieval of the data - # Used internally by iterators and retrieval procs - # requires calling - # properFreeResult(SQL_HANDLE_STMT, db.stmt) - # when finished - db.sqlCheck(SQLAllocHandle(SQL_HANDLE_STMT, db.hDb, db.stmt)) - var q = dbFormat(query, args) - db.sqlCheck(SQLPrepare(db.stmt, q.PSQLCHAR, q.len.TSqlSmallInt)) - db.sqlCheck(SQLExecute(db.stmt)) - result = SQLFetch(db.stmt) - db.sqlCheck(result) - -proc prepareFetchDirect(db: var DbConn, query: SqlQuery, - args: varargs[string, `$`]) {. - tags: [ReadDbEffect, WriteDbEffect], raises: [DbError].} = - # Prepare a statement, execute it and fetch the data to the driver - # ready for retrieval of the data - # Used internally by iterators and retrieval procs - # requires calling - # properFreeResult(SQL_HANDLE_STMT, db.stmt) - # when finished - db.sqlCheck(SQLAllocHandle(SQL_HANDLE_STMT, db.hDb, db.stmt)) - var q = dbFormat(query, args) - db.sqlCheck(SQLExecDirect(db.stmt, q.PSQLCHAR, q.len.TSqlSmallInt)) - db.sqlCheck(SQLFetch(db.stmt)) - -proc tryExec*(db: var DbConn, query: SqlQuery, args: varargs[string, `$`]): bool {. - tags: [ReadDbEffect, WriteDbEffect], raises: [].} = - ## Tries to execute the query and returns true if successful, false otherwise. - var - res:TSqlSmallInt = -1 - try: - db.prepareFetchDirect(query, args) - var - rCnt:TSqlLen = -1 - res = SQLRowCount(db.stmt, rCnt) - properFreeResult(SQL_HANDLE_STMT, db.stmt) - if res != SQL_SUCCESS: dbError(db) - except: discard - return res == SQL_SUCCESS - -proc rawExec(db: var DbConn, query: SqlQuery, args: varargs[string, `$`]) {. - tags: [ReadDbEffect, WriteDbEffect], raises: [DbError].} = - db.prepareFetchDirect(query, args) - -proc exec*(db: var DbConn, query: SqlQuery, args: varargs[string, `$`]) {. - tags: [ReadDbEffect, WriteDbEffect], raises: [DbError].} = - ## Executes the query and raises EDB if not successful. - db.prepareFetchDirect(query, args) - properFreeResult(SQL_HANDLE_STMT, db.stmt) - -proc newRow(L: int): Row {.noSideEFfect.} = - newSeq(result, L) - for i in 0..L-1: result[i] = "" - -iterator fastRows*(db: var DbConn, query: SqlQuery, - args: varargs[string, `$`]): Row {. - tags: [ReadDbEffect, WriteDbEffect], raises: [DbError].} = - ## Executes the query and iterates over the result dataset. - ## - ## This is very fast, but potentially dangerous. Use this iterator only - ## if you require **ALL** the rows. - ## - ## Breaking the fastRows() iterator during a loop may cause a driver error - ## for subsequent queries - ## - ## Rows are retrieved from the server at each iteration. - var - rowRes: Row - sz: TSqlLen = 0 - cCnt: TSqlSmallInt = 0 - res: TSqlSmallInt = 0 - res = db.prepareFetch(query, args) - if res == SQL_NO_DATA: - discard - elif res == SQL_SUCCESS: - res = SQLNumResultCols(db.stmt, cCnt) - rowRes = newRow(cCnt) - rowRes.setLen(max(cCnt,0)) - while res == SQL_SUCCESS: - for colId in 1..cCnt: - buf[0] = '\0' - db.sqlCheck(SQLGetData(db.stmt, colId.SqlUSmallInt, SQL_C_CHAR, - cast[cstring](buf.addr), 4095, sz.addr)) - rowRes[colId-1] = $(addr buf) - yield rowRes - res = SQLFetch(db.stmt) - properFreeResult(SQL_HANDLE_STMT, db.stmt) - db.sqlCheck(res) - -iterator instantRows*(db: var DbConn, query: SqlQuery, - args: varargs[string, `$`]): InstantRow - {.tags: [ReadDbEffect, WriteDbEffect].} = - ## Same as fastRows but returns a handle that can be used to get column text - ## on demand using []. Returned handle is valid only within the iterator body. - var - rowRes: Row = @[] - sz: TSqlLen = 0 - cCnt: TSqlSmallInt = 0 - res: TSqlSmallInt = 0 - res = db.prepareFetch(query, args) - if res == SQL_NO_DATA: - discard - elif res == SQL_SUCCESS: - res = SQLNumResultCols(db.stmt, cCnt) - rowRes = newRow(cCnt) - rowRes.setLen(max(cCnt,0)) - while res == SQL_SUCCESS: - for colId in 1..cCnt: - buf[0] = '\0' - db.sqlCheck(SQLGetData(db.stmt, colId.SqlUSmallInt, SQL_C_CHAR, - cast[cstring](buf.addr), 4095, sz.addr)) - rowRes[colId-1] = $(addr buf) - yield (row: rowRes, len: cCnt.int) - res = SQLFetch(db.stmt) - properFreeResult(SQL_HANDLE_STMT, db.stmt) - db.sqlCheck(res) - -proc `[]`*(row: InstantRow, col: int): string {.inline.} = - ## Returns text for given column of the row - $row.row[col] - -proc unsafeColumnAt*(row: InstantRow, index: int): cstring {.inline.} = - ## Return cstring of given column of the row - row.row[index] - -proc len*(row: InstantRow): int {.inline.} = - ## Returns number of columns in the row - row.len - -proc getRow*(db: var DbConn, query: SqlQuery, - args: varargs[string, `$`]): Row {. - tags: [ReadDbEffect, WriteDbEffect], raises: [DbError].} = - ## Retrieves a single row. If the query doesn't return any rows, this proc - ## will return a Row with empty strings for each column. - var - rowRes: Row - sz: TSqlLen = 0 - cCnt: TSqlSmallInt = 0 - res: TSqlSmallInt = 0 - res = db.prepareFetch(query, args) - if res == SQL_NO_DATA: - result = @[] - elif res == SQL_SUCCESS: - res = SQLNumResultCols(db.stmt, cCnt) - rowRes = newRow(cCnt) - rowRes.setLen(max(cCnt,0)) - for colId in 1..cCnt: - buf[0] = '\0' - db.sqlCheck(SQLGetData(db.stmt, colId.SqlUSmallInt, SQL_C_CHAR, - cast[cstring](buf.addr), 4095, sz.addr)) - rowRes[colId-1] = $(addr buf) - res = SQLFetch(db.stmt) - result = rowRes - properFreeResult(SQL_HANDLE_STMT, db.stmt) - db.sqlCheck(res) - -proc getAllRows*(db: var DbConn, query: SqlQuery, - args: varargs[string, `$`]): seq[Row] {. - tags: [ReadDbEffect, WriteDbEffect], raises: [DbError] .} = - ## Executes the query and returns the whole result dataset. - var - rows: seq[Row] = @[] - rowRes: Row - sz: TSqlLen = 0 - cCnt: TSqlSmallInt = 0 - res: TSqlSmallInt = 0 - res = db.prepareFetch(query, args) - if res == SQL_NO_DATA: - result = @[] - elif res == SQL_SUCCESS: - res = SQLNumResultCols(db.stmt, cCnt) - rowRes = newRow(cCnt) - rowRes.setLen(max(cCnt,0)) - while res == SQL_SUCCESS: - for colId in 1..cCnt: - buf[0] = '\0' - db.sqlCheck(SQLGetData(db.stmt, colId.SqlUSmallInt, SQL_C_CHAR, - cast[cstring](buf.addr), 4095, sz.addr)) - rowRes[colId-1] = $(addr buf) - rows.add(rowRes) - res = SQLFetch(db.stmt) - result = rows - properFreeResult(SQL_HANDLE_STMT, db.stmt) - db.sqlCheck(res) - -iterator rows*(db: var DbConn, query: SqlQuery, - args: varargs[string, `$`]): Row {. - tags: [ReadDbEffect, WriteDbEffect], raises: [DbError].} = - ## Same as `fastRows`, but slower and safe. - ## - ## This retrieves ALL rows into memory before - ## iterating through the rows. - ## Large dataset queries will impact on memory usage. - for r in items(getAllRows(db, query, args)): yield r - -proc getValue*(db: var DbConn, query: SqlQuery, - args: varargs[string, `$`]): string {. - tags: [ReadDbEffect, WriteDbEffect], raises: [].} = - ## Executes the query and returns the first column of the first row of the - ## result dataset. Returns "" if the dataset contains no rows or the database - ## value is NULL. - result = "" - try: - result = getRow(db, query, args)[0] - except: discard - -proc tryInsertId*(db: var DbConn, query: SqlQuery, - args: varargs[string, `$`]): int64 {. - tags: [ReadDbEffect, WriteDbEffect], raises: [].} = - ## Executes the query (typically "INSERT") and returns the - ## generated ID for the row or -1 in case of an error. - if not tryExec(db, query, args): - result = -1'i64 - else: - result = -1'i64 - try: - case sqlGetDBMS(db).toLower(): - of "postgresql": - result = getValue(db, sql"SELECT LASTVAL();", []).parseInt - of "mysql": - result = getValue(db, sql"SELECT LAST_INSERT_ID();", []).parseInt - of "sqlite": - result = getValue(db, sql"SELECT LAST_INSERT_ROWID();", []).parseInt - of "microsoft sql server": - result = getValue(db, sql"SELECT SCOPE_IDENTITY();", []).parseInt - of "oracle": - result = getValue(db, sql"SELECT id.currval FROM DUAL;", []).parseInt - else: result = -1'i64 - except: discard - -proc insertId*(db: var DbConn, query: SqlQuery, - args: varargs[string, `$`]): int64 {. - tags: [ReadDbEffect, WriteDbEffect], raises: [DbError].} = - ## Executes the query (typically "INSERT") and returns the - ## generated ID for the row. - result = tryInsertID(db, query, args) - if result < 0: dbError(db) - -proc tryInsert*(db: var DbConn, query: SqlQuery,pkName: string, - args: varargs[string, `$`]): int64 - {.tags: [ReadDbEffect, WriteDbEffect], raises: [], since: (1, 3).} = - ## same as tryInsertID - tryInsertID(db, query, args) - -proc insert*(db: var DbConn, query: SqlQuery, pkName: string, - args: varargs[string, `$`]): int64 - {.tags: [ReadDbEffect, WriteDbEffect], since: (1, 3).} = - ## same as insertId - result = tryInsert(db, query,pkName, args) - if result < 0: dbError(db) - -proc execAffectedRows*(db: var DbConn, query: SqlQuery, - args: varargs[string, `$`]): int64 {. - tags: [ReadDbEffect, WriteDbEffect], raises: [DbError].} = - ## Runs the query (typically "UPDATE") and returns the - ## number of affected rows - result = -1 - db.sqlCheck(SQLAllocHandle(SQL_HANDLE_STMT, db.hDb, db.stmt.SqlHandle)) - var q = dbFormat(query, args) - db.sqlCheck(SQLPrepare(db.stmt, q.PSQLCHAR, q.len.TSqlSmallInt)) - rawExec(db, query, args) - var rCnt:TSqlLen = -1 - db.sqlCheck(SQLRowCount(db.hDb, rCnt)) - properFreeResult(SQL_HANDLE_STMT, db.stmt) - result = rCnt.int64 - -proc close*(db: var DbConn) {. - tags: [WriteDbEffect], raises: [].} = - ## Closes the database connection. - if db.hDb != nil: - try: - var res = SQLDisconnect(db.hDb) - if db.stmt != nil: - res = SQLFreeHandle(SQL_HANDLE_STMT, db.stmt) - res = SQLFreeHandle(SQL_HANDLE_DBC, db.hDb) - res = SQLFreeHandle(SQL_HANDLE_ENV, db.env) - db = (hDb: nil, env: nil, stmt: nil) - except: - discard - -proc open*(connection, user, password, database: string): DbConn {. - tags: [ReadDbEffect, WriteDbEffect], raises: [DbError].} = - ## Opens a database connection. - ## - ## Raises `EDb` if the connection could not be established. - ## - ## Currently the database parameter is ignored, - ## but included to match `open()` in the other db_xxxxx library modules. - var - val = SQL_OV_ODBC3 - resLen = 0 - result = (hDb: nil, env: nil, stmt: nil) - # allocate environment handle - var res = SQLAllocHandle(SQL_HANDLE_ENV, result.env, result.env) - if res != SQL_SUCCESS: dbError("Error: unable to initialise ODBC environment.") - res = SQLSetEnvAttr(result.env, - SQL_ATTR_ODBC_VERSION.TSqlInteger, - cast[SqlPointer](val), resLen.TSqlInteger) - if res != SQL_SUCCESS: dbError("Error: unable to set ODBC driver version.") - # allocate hDb handle - res = SQLAllocHandle(SQL_HANDLE_DBC, result.env, result.hDb) - if res != SQL_SUCCESS: dbError("Error: unable to allocate connection handle.") - - # Connect: connection = dsn str, - res = SQLConnect(result.hDb, - connection.PSQLCHAR , connection.len.TSqlSmallInt, - user.PSQLCHAR, user.len.TSqlSmallInt, - password.PSQLCHAR, password.len.TSqlSmallInt) - if res != SQL_SUCCESS: - result.dbError() - -proc setEncoding*(connection: DbConn, encoding: string): bool {. - tags: [ReadDbEffect, WriteDbEffect], raises: [DbError].} = - ## Currently not implemented for ODBC. - ## - ## Sets the encoding of a database connection, returns true for - ## success, false for failure. - ##result = set_character_set(connection, encoding) == 0 - dbError("setEncoding() is currently not implemented by the db_odbc module") diff --git a/lib/impure/db_postgres.nim b/lib/impure/db_postgres.nim deleted file mode 100644 index 034a94852..000000000 --- a/lib/impure/db_postgres.nim +++ /dev/null @@ -1,590 +0,0 @@ -# -# -# Nim's Runtime Library -# (c) Copyright 2015 Andreas Rumpf -# -# See the file "copying.txt", included in this -# distribution, for details about the copyright. -# - -## A higher level `PostgreSQL`:idx: database wrapper. This interface -## is implemented for other databases also. -## -## See also: `db_odbc <db_odbc.html>`_, `db_sqlite <db_sqlite.html>`_, -## `db_mysql <db_mysql.html>`_. -## -## Parameter substitution -## ====================== -## -## All `db_*` modules support the same form of parameter substitution. -## That is, using the `?` (question mark) to signify the place where a -## value should be placed. For example: -## -## .. code-block:: Nim -## sql"INSERT INTO myTable (colA, colB, colC) VALUES (?, ?, ?)" -## -## **Note**: There are two approaches to parameter substitution support by -## this module. -## -## 1. `SqlQuery` using `?, ?, ?, ...` (same as all the `db_*` modules) -## -## 2. `SqlPrepared` using `$1, $2, $3, ...` -## -## .. code-block:: Nim -## prepare(db, "myExampleInsert", -## sql"""INSERT INTO myTable -## (colA, colB, colC) -## VALUES ($1, $2, $3)""", -## 3) -## -## -## Unix Socket -## =========== -## -## Using Unix sockets instead of TCP connection can -## `improve performance up to 30% ~ 175% for some operations <https://momjian.us/main/blogs/pgblog/2012.html#June_6_2012>`_. -## -## To use Unix sockets with `db_postgres`, change the server address to the socket file path: -## -## .. code-block:: Nim -## import std/db_postgres ## Change "localhost" or "127.0.0.1" to the socket file path -## let db = db_postgres.open("/run/postgresql", "user", "password", "database") -## echo db.getAllRows(sql"SELECT version();") -## db.close() -## -## The socket file path is operating system specific and distribution specific, -## additional configuration may or may not be needed on your `postgresql.conf`. -## The Postgres server must be on the same computer and only works for Unix-like operating systems. -## -## -## Examples -## ======== -## -## Opening a connection to a database -## ---------------------------------- -## -## .. code-block:: Nim -## import std/db_postgres -## let db = open("localhost", "user", "password", "dbname") -## db.close() -## -## Creating a table -## ---------------- -## -## .. code-block:: Nim -## db.exec(sql"DROP TABLE IF EXISTS myTable") -## db.exec(sql("""CREATE TABLE myTable ( -## id integer, -## name varchar(50) not null)""")) -## -## Inserting data -## -------------- -## -## .. code-block:: Nim -## db.exec(sql"INSERT INTO myTable (id, name) VALUES (0, ?)", -## "Dominik") -import strutils, postgres - -import db_common -export db_common - -import std/private/since - -type - DbConn* = PPGconn ## encapsulates a database connection - Row* = seq[string] ## a row of a dataset. NULL database values will be - ## converted to nil. - InstantRow* = object ## a handle that can be - res: PPGresult ## used to get a row's - line: int ## column text on demand - SqlPrepared* = distinct string ## a identifier for the prepared queries - -proc dbError*(db: DbConn) {.noreturn.} = - ## raises a DbError exception. - var e: ref DbError - new(e) - e.msg = $pqErrorMessage(db) - raise e - -proc dbQuote*(s: string): string = - ## DB quotes the string. - result = "'" - for c in items(s): - if c == '\'': add(result, "''") - else: add(result, c) - add(result, '\'') - -proc dbFormat(formatstr: SqlQuery, args: varargs[string]): string = - result = "" - var a = 0 - if args.len > 0 and not string(formatstr).contains("?"): - dbError("""parameter substitution expects "?" """) - if args.len == 0: - return string(formatstr) - else: - for c in items(string(formatstr)): - if c == '?': - add(result, dbQuote(args[a])) - inc(a) - else: - add(result, c) - -proc tryExec*(db: DbConn, query: SqlQuery, - args: varargs[string, `$`]): bool {.tags: [ReadDbEffect, WriteDbEffect].} = - ## tries to execute the query and returns true if successful, false otherwise. - var res = pqexecParams(db, dbFormat(query, args), 0, nil, nil, - nil, nil, 0) - result = pqresultStatus(res) == PGRES_COMMAND_OK - pqclear(res) - -proc tryExec*(db: DbConn, stmtName: SqlPrepared, - args: varargs[string, `$`]): bool {.tags: [ - ReadDbEffect, WriteDbEffect].} = - ## tries to execute the query and returns true if successful, false otherwise. - var arr = allocCStringArray(args) - var res = pqexecPrepared(db, stmtName.string, int32(args.len), arr, - nil, nil, 0) - deallocCStringArray(arr) - result = pqresultStatus(res) == PGRES_COMMAND_OK - pqclear(res) - -proc exec*(db: DbConn, query: SqlQuery, args: varargs[string, `$`]) {. - tags: [ReadDbEffect, WriteDbEffect].} = - ## executes the query and raises EDB if not successful. - var res = pqexecParams(db, dbFormat(query, args), 0, nil, nil, - nil, nil, 0) - if pqresultStatus(res) != PGRES_COMMAND_OK: dbError(db) - pqclear(res) - -proc exec*(db: DbConn, stmtName: SqlPrepared, - args: varargs[string]) {.tags: [ReadDbEffect, WriteDbEffect].} = - var arr = allocCStringArray(args) - var res = pqexecPrepared(db, stmtName.string, int32(args.len), arr, - nil, nil, 0) - deallocCStringArray(arr) - if pqResultStatus(res) != PGRES_COMMAND_OK: dbError(db) - pqclear(res) - -proc newRow(L: int): Row = - newSeq(result, L) - for i in 0..L-1: result[i] = "" - -proc setupQuery(db: DbConn, query: SqlQuery, - args: varargs[string]): PPGresult = - result = pqexec(db, dbFormat(query, args)) - if pqResultStatus(result) != PGRES_TUPLES_OK: dbError(db) - -proc setupQuery(db: DbConn, stmtName: SqlPrepared, - args: varargs[string]): PPGresult = - var arr = allocCStringArray(args) - result = pqexecPrepared(db, stmtName.string, int32(args.len), arr, - nil, nil, 0) - deallocCStringArray(arr) - if pqResultStatus(result) != PGRES_TUPLES_OK: dbError(db) - -proc prepare*(db: DbConn; stmtName: string, query: SqlQuery; - nParams: int): SqlPrepared = - ## Creates a new `SqlPrepared` statement. Parameter substitution is done - ## via `$1`, `$2`, `$3`, etc. - if nParams > 0 and not string(query).contains("$1"): - dbError("parameter substitution expects \"$1\"") - var res = pqprepare(db, stmtName, query.string, int32(nParams), nil) - if pqResultStatus(res) != PGRES_COMMAND_OK: dbError(db) - return SqlPrepared(stmtName) - -proc setRow(res: PPGresult, r: var Row, line, cols: int32) = - for col in 0'i32..cols-1: - setLen(r[col], 0) - let x = pqgetvalue(res, line, col) - if x.isNil: - r[col] = "" - else: - add(r[col], x) - -iterator fastRows*(db: DbConn, query: SqlQuery, - args: varargs[string, `$`]): Row {.tags: [ReadDbEffect].} = - ## executes the query and iterates over the result dataset. This is very - ## fast, but potentially dangerous: If the for-loop-body executes another - ## query, the results can be undefined. For Postgres it is safe though. - var res = setupQuery(db, query, args) - var L = pqnfields(res) - var result = newRow(L) - for i in 0'i32..pqntuples(res)-1: - setRow(res, result, i, L) - yield result - pqclear(res) - -iterator fastRows*(db: DbConn, stmtName: SqlPrepared, - args: varargs[string, `$`]): Row {.tags: [ReadDbEffect].} = - ## executes the prepared query and iterates over the result dataset. - var res = setupQuery(db, stmtName, args) - var L = pqNfields(res) - var result = newRow(L) - for i in 0'i32..pqNtuples(res)-1: - setRow(res, result, i, L) - yield result - pqClear(res) - -iterator instantRows*(db: DbConn, query: SqlQuery, - args: varargs[string, `$`]): InstantRow - {.tags: [ReadDbEffect].} = - ## same as fastRows but returns a handle that can be used to get column text - ## on demand using []. Returned handle is valid only within iterator body. - var res = setupQuery(db, query, args) - for i in 0'i32..pqNtuples(res)-1: - yield InstantRow(res: res, line: i) - pqClear(res) - -iterator instantRows*(db: DbConn, stmtName: SqlPrepared, - args: varargs[string, `$`]): InstantRow - {.tags: [ReadDbEffect].} = - ## same as fastRows but returns a handle that can be used to get column text - ## on demand using []. Returned handle is valid only within iterator body. - var res = setupQuery(db, stmtName, args) - for i in 0'i32..pqNtuples(res)-1: - yield InstantRow(res: res, line: i) - pqClear(res) - -proc getColumnType(res: PPGresult, col: int) : DbType = - ## returns DbType for given column in the row - ## defined in pg_type.h file in the postgres source code - ## Wire representation for types: http://www.npgsql.org/dev/types.html - var oid = pqftype(res, int32(col)) - ## The integer returned is the internal OID number of the type - case oid - of 16: return DbType(kind: DbTypeKind.dbBool, name: "bool") - of 17: return DbType(kind: DbTypeKind.dbBlob, name: "bytea") - - of 21: return DbType(kind: DbTypeKind.dbInt, name: "int2", size: 2) - of 23: return DbType(kind: DbTypeKind.dbInt, name: "int4", size: 4) - of 20: return DbType(kind: DbTypeKind.dbInt, name: "int8", size: 8) - of 1560: return DbType(kind: DbTypeKind.dbBit, name: "bit") - of 1562: return DbType(kind: DbTypeKind.dbInt, name: "varbit") - - of 18: return DbType(kind: DbTypeKind.dbFixedChar, name: "char") - of 19: return DbType(kind: DbTypeKind.dbFixedChar, name: "name") - of 1042: return DbType(kind: DbTypeKind.dbFixedChar, name: "bpchar") - - of 25: return DbType(kind: DbTypeKind.dbVarchar, name: "text") - of 1043: return DbType(kind: DbTypeKind.dbVarChar, name: "varchar") - of 2275: return DbType(kind: DbTypeKind.dbVarchar, name: "cstring") - - of 700: return DbType(kind: DbTypeKind.dbFloat, name: "float4") - of 701: return DbType(kind: DbTypeKind.dbFloat, name: "float8") - - of 790: return DbType(kind: DbTypeKind.dbDecimal, name: "money") - of 1700: return DbType(kind: DbTypeKind.dbDecimal, name: "numeric") - - of 704: return DbType(kind: DbTypeKind.dbTimeInterval, name: "tinterval") - of 702: return DbType(kind: DbTypeKind.dbTimestamp, name: "abstime") - of 703: return DbType(kind: DbTypeKind.dbTimeInterval, name: "reltime") - of 1082: return DbType(kind: DbTypeKind.dbDate, name: "date") - of 1083: return DbType(kind: DbTypeKind.dbTime, name: "time") - of 1114: return DbType(kind: DbTypeKind.dbTimestamp, name: "timestamp") - of 1184: return DbType(kind: DbTypeKind.dbTimestamp, name: "timestamptz") - of 1186: return DbType(kind: DbTypeKind.dbTimeInterval, name: "interval") - of 1266: return DbType(kind: DbTypeKind.dbTime, name: "timetz") - - of 114: return DbType(kind: DbTypeKind.dbJson, name: "json") - of 142: return DbType(kind: DbTypeKind.dbXml, name: "xml") - of 3802: return DbType(kind: DbTypeKind.dbJson, name: "jsonb") - - of 600: return DbType(kind: DbTypeKind.dbPoint, name: "point") - of 601: return DbType(kind: DbTypeKind.dbLseg, name: "lseg") - of 602: return DbType(kind: DbTypeKind.dbPath, name: "path") - of 603: return DbType(kind: DbTypeKind.dbBox, name: "box") - of 604: return DbType(kind: DbTypeKind.dbPolygon, name: "polygon") - of 628: return DbType(kind: DbTypeKind.dbLine, name: "line") - of 718: return DbType(kind: DbTypeKind.dbCircle, name: "circle") - - of 650: return DbType(kind: DbTypeKind.dbInet, name: "cidr") - of 829: return DbType(kind: DbTypeKind.dbMacAddress, name: "macaddr") - of 869: return DbType(kind: DbTypeKind.dbInet, name: "inet") - - of 2950: return DbType(kind: DbTypeKind.dbVarchar, name: "uuid") - of 3614: return DbType(kind: DbTypeKind.dbVarchar, name: "tsvector") - of 3615: return DbType(kind: DbTypeKind.dbVarchar, name: "tsquery") - of 2970: return DbType(kind: DbTypeKind.dbVarchar, name: "txid_snapshot") - - of 27: return DbType(kind: DbTypeKind.dbComposite, name: "tid") - of 1790: return DbType(kind: DbTypeKind.dbComposite, name: "refcursor") - of 2249: return DbType(kind: DbTypeKind.dbComposite, name: "record") - of 3904: return DbType(kind: DbTypeKind.dbComposite, name: "int4range") - of 3906: return DbType(kind: DbTypeKind.dbComposite, name: "numrange") - of 3908: return DbType(kind: DbTypeKind.dbComposite, name: "tsrange") - of 3910: return DbType(kind: DbTypeKind.dbComposite, name: "tstzrange") - of 3912: return DbType(kind: DbTypeKind.dbComposite, name: "daterange") - of 3926: return DbType(kind: DbTypeKind.dbComposite, name: "int8range") - - of 22: return DbType(kind: DbTypeKind.dbArray, name: "int2vector") - of 30: return DbType(kind: DbTypeKind.dbArray, name: "oidvector") - of 143: return DbType(kind: DbTypeKind.dbArray, name: "xml[]") - of 199: return DbType(kind: DbTypeKind.dbArray, name: "json[]") - of 629: return DbType(kind: DbTypeKind.dbArray, name: "line[]") - of 651: return DbType(kind: DbTypeKind.dbArray, name: "cidr[]") - of 719: return DbType(kind: DbTypeKind.dbArray, name: "circle[]") - of 791: return DbType(kind: DbTypeKind.dbArray, name: "money[]") - of 1000: return DbType(kind: DbTypeKind.dbArray, name: "bool[]") - of 1001: return DbType(kind: DbTypeKind.dbArray, name: "bytea[]") - of 1002: return DbType(kind: DbTypeKind.dbArray, name: "char[]") - of 1003: return DbType(kind: DbTypeKind.dbArray, name: "name[]") - of 1005: return DbType(kind: DbTypeKind.dbArray, name: "int2[]") - of 1006: return DbType(kind: DbTypeKind.dbArray, name: "int2vector[]") - of 1007: return DbType(kind: DbTypeKind.dbArray, name: "int4[]") - of 1008: return DbType(kind: DbTypeKind.dbArray, name: "regproc[]") - of 1009: return DbType(kind: DbTypeKind.dbArray, name: "text[]") - of 1028: return DbType(kind: DbTypeKind.dbArray, name: "oid[]") - of 1010: return DbType(kind: DbTypeKind.dbArray, name: "tid[]") - of 1011: return DbType(kind: DbTypeKind.dbArray, name: "xid[]") - of 1012: return DbType(kind: DbTypeKind.dbArray, name: "cid[]") - of 1013: return DbType(kind: DbTypeKind.dbArray, name: "oidvector[]") - of 1014: return DbType(kind: DbTypeKind.dbArray, name: "bpchar[]") - of 1015: return DbType(kind: DbTypeKind.dbArray, name: "varchar[]") - of 1016: return DbType(kind: DbTypeKind.dbArray, name: "int8[]") - of 1017: return DbType(kind: DbTypeKind.dbArray, name: "point[]") - of 1018: return DbType(kind: DbTypeKind.dbArray, name: "lseg[]") - of 1019: return DbType(kind: DbTypeKind.dbArray, name: "path[]") - of 1020: return DbType(kind: DbTypeKind.dbArray, name: "box[]") - of 1021: return DbType(kind: DbTypeKind.dbArray, name: "float4[]") - of 1022: return DbType(kind: DbTypeKind.dbArray, name: "float8[]") - of 1023: return DbType(kind: DbTypeKind.dbArray, name: "abstime[]") - of 1024: return DbType(kind: DbTypeKind.dbArray, name: "reltime[]") - of 1025: return DbType(kind: DbTypeKind.dbArray, name: "tinterval[]") - of 1027: return DbType(kind: DbTypeKind.dbArray, name: "polygon[]") - of 1040: return DbType(kind: DbTypeKind.dbArray, name: "macaddr[]") - of 1041: return DbType(kind: DbTypeKind.dbArray, name: "inet[]") - of 1263: return DbType(kind: DbTypeKind.dbArray, name: "cstring[]") - of 1115: return DbType(kind: DbTypeKind.dbArray, name: "timestamp[]") - of 1182: return DbType(kind: DbTypeKind.dbArray, name: "date[]") - of 1183: return DbType(kind: DbTypeKind.dbArray, name: "time[]") - of 1185: return DbType(kind: DbTypeKind.dbArray, name: "timestamptz[]") - of 1187: return DbType(kind: DbTypeKind.dbArray, name: "interval[]") - of 1231: return DbType(kind: DbTypeKind.dbArray, name: "numeric[]") - of 1270: return DbType(kind: DbTypeKind.dbArray, name: "timetz[]") - of 1561: return DbType(kind: DbTypeKind.dbArray, name: "bit[]") - of 1563: return DbType(kind: DbTypeKind.dbArray, name: "varbit[]") - of 2201: return DbType(kind: DbTypeKind.dbArray, name: "refcursor[]") - of 2951: return DbType(kind: DbTypeKind.dbArray, name: "uuid[]") - of 3643: return DbType(kind: DbTypeKind.dbArray, name: "tsvector[]") - of 3645: return DbType(kind: DbTypeKind.dbArray, name: "tsquery[]") - of 3807: return DbType(kind: DbTypeKind.dbArray, name: "jsonb[]") - of 2949: return DbType(kind: DbTypeKind.dbArray, name: "txid_snapshot[]") - of 3905: return DbType(kind: DbTypeKind.dbArray, name: "int4range[]") - of 3907: return DbType(kind: DbTypeKind.dbArray, name: "numrange[]") - of 3909: return DbType(kind: DbTypeKind.dbArray, name: "tsrange[]") - of 3911: return DbType(kind: DbTypeKind.dbArray, name: "tstzrange[]") - of 3913: return DbType(kind: DbTypeKind.dbArray, name: "daterange[]") - of 3927: return DbType(kind: DbTypeKind.dbArray, name: "int8range[]") - of 2287: return DbType(kind: DbTypeKind.dbArray, name: "record[]") - - of 705: return DbType(kind: DbTypeKind.dbUnknown, name: "unknown") - else: return DbType(kind: DbTypeKind.dbUnknown, name: $oid) ## Query the system table pg_type to determine exactly which type is referenced. - -proc setColumnInfo(columns: var DbColumns; res: PPGresult; L: int32) = - setLen(columns, L) - for i in 0'i32..<L: - columns[i].name = $pqfname(res, i) - columns[i].typ = getColumnType(res, i) - columns[i].tableName = $(pqftable(res, i)) ## Returns the OID of the table from which the given column was fetched. - ## Query the system table pg_class to determine exactly which table is referenced. - #columns[i].primaryKey = libpq does not have a function for that - #columns[i].foreignKey = libpq does not have a function for that - -iterator instantRows*(db: DbConn; columns: var DbColumns; query: SqlQuery; - args: varargs[string, `$`]): InstantRow - {.tags: [ReadDbEffect].} = - var res = setupQuery(db, query, args) - setColumnInfo(columns, res, pqnfields(res)) - for i in 0'i32..<pqntuples(res): - yield InstantRow(res: res, line: i) - pqClear(res) - -proc `[]`*(row: InstantRow; col: int): string {.inline.} = - ## returns text for given column of the row - $pqgetvalue(row.res, int32(row.line), int32(col)) - -proc unsafeColumnAt*(row: InstantRow, index: int): cstring {.inline.} = - ## Return cstring of given column of the row - pqgetvalue(row.res, int32(row.line), int32(index)) - -proc len*(row: InstantRow): int {.inline.} = - ## returns number of columns in the row - int(pqNfields(row.res)) - -proc getRow*(db: DbConn, query: SqlQuery, - args: varargs[string, `$`]): Row {.tags: [ReadDbEffect].} = - ## retrieves a single row. If the query doesn't return any rows, this proc - ## will return a Row with empty strings for each column. - var res = setupQuery(db, query, args) - var L = pqnfields(res) - result = newRow(L) - if pqntuples(res) > 0: - setRow(res, result, 0, L) - pqclear(res) - -proc getRow*(db: DbConn, stmtName: SqlPrepared, - args: varargs[string, `$`]): Row {.tags: [ReadDbEffect].} = - var res = setupQuery(db, stmtName, args) - var L = pqNfields(res) - result = newRow(L) - if pqntuples(res) > 0: - setRow(res, result, 0, L) - pqClear(res) - -proc getAllRows*(db: DbConn, query: SqlQuery, - args: varargs[string, `$`]): seq[Row] {. - tags: [ReadDbEffect].} = - ## executes the query and returns the whole result dataset. - result = @[] - for r in fastRows(db, query, args): - result.add(r) - -proc getAllRows*(db: DbConn, stmtName: SqlPrepared, - args: varargs[string, `$`]): seq[Row] {.tags: - [ReadDbEffect].} = - ## executes the prepared query and returns the whole result dataset. - result = @[] - for r in fastRows(db, stmtName, args): - result.add(r) - -iterator rows*(db: DbConn, query: SqlQuery, - args: varargs[string, `$`]): Row {.tags: [ReadDbEffect].} = - ## same as `fastRows`, but slower and safe. - for r in items(getAllRows(db, query, args)): yield r - -iterator rows*(db: DbConn, stmtName: SqlPrepared, - args: varargs[string, `$`]): Row {.tags: [ReadDbEffect].} = - ## same as `fastRows`, but slower and safe. - for r in items(getAllRows(db, stmtName, args)): yield r - -proc getValue*(db: DbConn, query: SqlQuery, - args: varargs[string, `$`]): string {. - tags: [ReadDbEffect].} = - ## executes the query and returns the first column of the first row of the - ## result dataset. Returns "" if the dataset contains no rows or the database - ## value is NULL. - var res = setupQuery(db, query, args) - if pqntuples(res) > 0: - var x = pqgetvalue(res, 0, 0) - result = if isNil(x): "" else: $x - else: - result = "" - -proc getValue*(db: DbConn, stmtName: SqlPrepared, - args: varargs[string, `$`]): string {. - tags: [ReadDbEffect].} = - ## executes the query and returns the first column of the first row of the - ## result dataset. Returns "" if the dataset contains no rows or the database - ## value is NULL. - var res = setupQuery(db, stmtName, args) - if pqntuples(res) > 0: - var x = pqgetvalue(res, 0, 0) - result = if isNil(x): "" else: $x - else: - result = "" - -proc tryInsertID*(db: DbConn, query: SqlQuery, - args: varargs[string, `$`]): int64 {. - tags: [WriteDbEffect].}= - ## executes the query (typically "INSERT") and returns the - ## generated ID for the row or -1 in case of an error. For Postgre this adds - ## `RETURNING id` to the query, so it only works if your primary key is - ## named `id`. - var x = pqgetvalue(setupQuery(db, SqlQuery(string(query) & " RETURNING id"), - args), 0, 0) - if not isNil(x): - result = parseBiggestInt($x) - else: - result = -1 - -proc insertID*(db: DbConn, query: SqlQuery, - args: varargs[string, `$`]): int64 {. - tags: [WriteDbEffect].} = - ## executes the query (typically "INSERT") and returns the - ## generated ID for the row. For Postgre this adds - ## `RETURNING id` to the query, so it only works if your primary key is - ## named `id`. - result = tryInsertID(db, query, args) - if result < 0: dbError(db) - -proc tryInsert*(db: DbConn, query: SqlQuery,pkName: string, - args: varargs[string, `$`]): int64 - {.tags: [WriteDbEffect], since: (1, 3).}= - ## executes the query (typically "INSERT") and returns the - ## generated ID for the row or -1 in case of an error. - var x = pqgetvalue(setupQuery(db, SqlQuery(string(query) & " RETURNING " & pkName), - args), 0, 0) - if not isNil(x): - result = parseBiggestInt($x) - else: - result = -1 - -proc insert*(db: DbConn, query: SqlQuery, pkName: string, - args: varargs[string, `$`]): int64 - {.tags: [WriteDbEffect], since: (1, 3).} = - ## executes the query (typically "INSERT") and returns the - ## generated ID - result = tryInsertID(db, query, args) - if result < 0: dbError(db) - -proc execAffectedRows*(db: DbConn, query: SqlQuery, - args: varargs[string, `$`]): int64 {.tags: [ - ReadDbEffect, WriteDbEffect].} = - ## executes the query (typically "UPDATE") and returns the - ## number of affected rows. - var q = dbFormat(query, args) - var res = pqExec(db, q) - if pqresultStatus(res) != PGRES_COMMAND_OK: dbError(db) - result = parseBiggestInt($pqcmdTuples(res)) - pqclear(res) - -proc execAffectedRows*(db: DbConn, stmtName: SqlPrepared, - args: varargs[string, `$`]): int64 {.tags: [ - ReadDbEffect, WriteDbEffect].} = - ## executes the query (typically "UPDATE") and returns the - ## number of affected rows. - var arr = allocCStringArray(args) - var res = pqexecPrepared(db, stmtName.string, int32(args.len), arr, - nil, nil, 0) - deallocCStringArray(arr) - if pqresultStatus(res) != PGRES_COMMAND_OK: dbError(db) - result = parseBiggestInt($pqcmdTuples(res)) - pqclear(res) - -proc close*(db: DbConn) {.tags: [DbEffect].} = - ## closes the database connection. - if db != nil: pqfinish(db) - -proc open*(connection, user, password, database: string): DbConn {. - tags: [DbEffect].} = - ## opens a database connection. Raises `EDb` if the connection could not - ## be established. - ## - ## Clients can also use Postgres keyword/value connection strings to - ## connect. - ## - ## Example: - ## - ## .. code-block:: nim - ## - ## con = open("", "", "", "host=localhost port=5432 dbname=mydb") - ## - ## See http://www.postgresql.org/docs/current/static/libpq-connect.html#LIBPQ-CONNSTRING - ## for more information. - let - colonPos = connection.find(':') - host = if colonPos < 0: connection - else: substr(connection, 0, colonPos-1) - port = if colonPos < 0: "" - else: substr(connection, colonPos+1) - result = pqsetdbLogin(host, port, nil, nil, database, user, password) - if pqStatus(result) != CONNECTION_OK: dbError(result) # result = nil - -proc setEncoding*(connection: DbConn, encoding: string): bool {. - tags: [DbEffect].} = - ## sets the encoding of a database connection, returns true for - ## success, false for failure. - return pqsetClientEncoding(connection, encoding) == 0 - - -# Tests are in ../../tests/untestable/tpostgres. diff --git a/lib/impure/db_sqlite.nim b/lib/impure/db_sqlite.nim deleted file mode 100644 index 832407960..000000000 --- a/lib/impure/db_sqlite.nim +++ /dev/null @@ -1,950 +0,0 @@ -# -# -# Nim's Runtime Library -# (c) Copyright 2015 Andreas Rumpf -# -# See the file "copying.txt", included in this -# distribution, for details about the copyright. -# - -## A higher level `SQLite`:idx: database wrapper. This interface -## is implemented for other databases too. -## -## Basic usage -## =========== -## -## The basic flow of using this module is: -## -## 1. Open database connection -## 2. Execute SQL query -## 3. Close database connection -## -## Parameter substitution -## ---------------------- -## -## All `db_*` modules support the same form of parameter substitution. -## That is, using the `?` (question mark) to signify the place where a -## value should be placed. For example: -## -## .. code-block:: Nim -## -## sql"INSERT INTO my_table (colA, colB, colC) VALUES (?, ?, ?)" -## -## Opening a connection to a database -## ---------------------------------- -## -## .. code-block:: Nim -## -## import std/db_sqlite -## -## # user, password, database name can be empty. -## # These params are not used on db_sqlite module. -## let db = open("mytest.db", "", "", "") -## db.close() -## -## Creating a table -## ---------------- -## -## .. code-block:: Nim -## -## db.exec(sql"DROP TABLE IF EXISTS my_table") -## db.exec(sql"""CREATE TABLE my_table ( -## id INTEGER, -## name VARCHAR(50) NOT NULL -## )""") -## -## Inserting data -## -------------- -## -## .. code-block:: Nim -## -## db.exec(sql"INSERT INTO my_table (id, name) VALUES (0, ?)", -## "Jack") -## -## Larger example -## -------------- -## -## .. code-block:: nim -## -## import std/[db_sqlite, math] -## -## let db = open("mytest.db", "", "", "") -## -## db.exec(sql"DROP TABLE IF EXISTS my_table") -## db.exec(sql"""CREATE TABLE my_table ( -## id INTEGER PRIMARY KEY, -## name VARCHAR(50) NOT NULL, -## i INT(11), -## f DECIMAL(18, 10) -## )""") -## -## db.exec(sql"BEGIN") -## for i in 1..1000: -## db.exec(sql"INSERT INTO my_table (name, i, f) VALUES (?, ?, ?)", -## "Item#" & $i, i, sqrt(i.float)) -## db.exec(sql"COMMIT") -## -## for x in db.fastRows(sql"SELECT * FROM my_table"): -## echo x -## -## let id = db.tryInsertId(sql"""INSERT INTO my_table (name, i, f) -## VALUES (?, ?, ?)""", -## "Item#1001", 1001, sqrt(1001.0)) -## echo "Inserted item: ", db.getValue(sql"SELECT name FROM my_table WHERE id=?", id) -## -## db.close() -## -## Storing binary data example -##---------------------------- -## -## .. code-block:: nim -## -## import std/random -## -## ## Generate random float datas -## var orig = newSeq[float64](150) -## randomize() -## for x in orig.mitems: -## x = rand(1.0)/10.0 -## -## let db = open("mysqlite.db", "", "", "") -## block: ## Create database -## ## Binary datas needs to be of type BLOB in SQLite -## let createTableStr = sql"""CREATE TABLE test( -## id INTEGER NOT NULL PRIMARY KEY, -## data BLOB -## ) -## """ -## db.exec(createTableStr) -## -## block: ## Insert data -## var id = 1 -## ## Data needs to be converted to seq[byte] to be interpreted as binary by bindParams -## var dbuf = newSeq[byte](orig.len*sizeof(float64)) -## copyMem(unsafeAddr(dbuf[0]), unsafeAddr(orig[0]), dbuf.len) -## -## ## Use prepared statement to insert binary data into database -## var insertStmt = db.prepare("INSERT INTO test (id, data) VALUES (?, ?)") -## insertStmt.bindParams(id, dbuf) -## let bres = db.tryExec(insertStmt) -## ## Check insert -## doAssert(bres) -## # Destroy statement -## finalize(insertStmt) -## -## block: ## Use getValue to select data -## var dataTest = db.getValue(sql"SELECT data FROM test WHERE id = ?", 1) -## ## Calculate sequence size from buffer size -## let seqSize = int(dataTest.len*sizeof(byte)/sizeof(float64)) -## ## Copy binary string data in dataTest into a seq -## var res: seq[float64] = newSeq[float64](seqSize) -## copyMem(unsafeAddr(res[0]), addr(dataTest[0]), dataTest.len) -## -## ## Check datas obtained is identical -## doAssert res == orig -## -## db.close() -## -## -## Note -## ==== -## This module does not implement any ORM features such as mapping the types from the schema. -## Instead, a `seq[string]` is returned for each row. -## -## The reasoning is as follows: -## 1. it's close to what many DBs offer natively (char**) -## 2. it hides the number of types that the DB supports -## (int? int64? decimal up to 10 places? geo coords?) -## 3. it's convenient when all you do is to forward the data to somewhere else (echo, log, put the data into a new query) -## -## See also -## ======== -## -## * `db_odbc module <db_odbc.html>`_ for ODBC database wrapper -## * `db_mysql module <db_mysql.html>`_ for MySQL database wrapper -## * `db_postgres module <db_postgres.html>`_ for PostgreSQL database wrapper - -{.experimental: "codeReordering".} - -import sqlite3, macros - -import db_common -export db_common - -import std/private/since - -type - DbConn* = PSqlite3 ## Encapsulates a database connection. - Row* = seq[string] ## A row of a dataset. `NULL` database values will be - ## converted to an empty string. - InstantRow* = PStmt ## A handle that can be used to get a row's column - ## text on demand. - SqlPrepared* = distinct PStmt ## a identifier for the prepared queries - -proc dbError*(db: DbConn) {.noreturn.} = - ## Raises a `DbError` exception. - ## - ## **Examples:** - ## - ## .. code-block:: Nim - ## - ## let db = open("mytest.db", "", "", "") - ## if not db.tryExec(sql"SELECT * FROM not_exist_table"): - ## dbError(db) - ## db.close() - var e: ref DbError - new(e) - e.msg = $sqlite3.errmsg(db) - raise e - -proc dbQuote*(s: string): string = - ## Escapes the `'` (single quote) char to `''`. - ## Because single quote is used for defining `VARCHAR` in SQL. - runnableExamples: - doAssert dbQuote("'") == "''''" - doAssert dbQuote("A Foobar's pen.") == "'A Foobar''s pen.'" - - result = "'" - for c in items(s): - if c == '\'': add(result, "''") - else: add(result, c) - add(result, '\'') - -proc dbFormat(formatstr: SqlQuery, args: varargs[string]): string = - result = "" - var a = 0 - for c in items(string(formatstr)): - if c == '?': - add(result, dbQuote(args[a])) - inc(a) - else: - add(result, c) - -proc prepare*(db: DbConn; q: string): SqlPrepared {.since: (1, 3).} = - ## Creates a new `SqlPrepared` statement. - if prepare_v2(db, q, q.len.cint,result.PStmt, nil) != SQLITE_OK: - discard finalize(result.PStmt) - dbError(db) - -proc tryExec*(db: DbConn, query: SqlQuery, - args: varargs[string, `$`]): bool {. - tags: [ReadDbEffect, WriteDbEffect].} = - ## Tries to execute the query and returns `true` if successful, `false` otherwise. - ## - ## **Examples:** - ## - ## .. code-block:: Nim - ## - ## let db = open("mytest.db", "", "", "") - ## if not db.tryExec(sql"SELECT * FROM my_table"): - ## dbError(db) - ## db.close() - assert(not db.isNil, "Database not connected.") - var q = dbFormat(query, args) - var stmt: sqlite3.PStmt - if prepare_v2(db, q, q.len.cint, stmt, nil) == SQLITE_OK: - let x = step(stmt) - if x in {SQLITE_DONE, SQLITE_ROW}: - result = finalize(stmt) == SQLITE_OK - else: - discard finalize(stmt) - result = false - -proc tryExec*(db: DbConn, stmtName: SqlPrepared): bool {. - tags: [ReadDbEffect, WriteDbEffect].} = - let x = step(stmtName.PStmt) - if x in {SQLITE_DONE, SQLITE_ROW}: - result = true - else: - discard finalize(stmtName.PStmt) - result = false - -proc exec*(db: DbConn, query: SqlQuery, args: varargs[string, `$`]) {. - tags: [ReadDbEffect, WriteDbEffect].} = - ## Executes the query and raises a `DbError` exception if not successful. - ## - ## **Examples:** - ## - ## .. code-block:: Nim - ## - ## let db = open("mytest.db", "", "", "") - ## try: - ## db.exec(sql"INSERT INTO my_table (id, name) VALUES (?, ?)", - ## 1, "item#1") - ## except: - ## stderr.writeLine(getCurrentExceptionMsg()) - ## finally: - ## db.close() - if not tryExec(db, query, args): dbError(db) - -proc newRow(L: int): Row = - newSeq(result, L) - for i in 0..L-1: result[i] = "" - -proc setupQuery(db: DbConn, query: SqlQuery, - args: varargs[string]): PStmt = - assert(not db.isNil, "Database not connected.") - var q = dbFormat(query, args) - if prepare_v2(db, q, q.len.cint, result, nil) != SQLITE_OK: dbError(db) - -proc setupQuery(db: DbConn, stmtName: SqlPrepared): SqlPrepared {.since: (1, 3).} = - assert(not db.isNil, "Database not connected.") - result = stmtName - -proc setRow(stmt: PStmt, r: var Row, cols: cint) = - for col in 0'i32..cols-1: - let cb = column_bytes(stmt, col) - setLen(r[col], cb) # set capacity - if column_type(stmt, col) == SQLITE_BLOB: - copyMem(addr(r[col][0]), column_blob(stmt, col), cb) - else: - setLen(r[col], 0) - let x = column_text(stmt, col) - if not isNil(x): add(r[col], x) - -iterator fastRows*(db: DbConn, query: SqlQuery, - args: varargs[string, `$`]): Row {.tags: [ReadDbEffect].} = - ## Executes the query and iterates over the result dataset. - ## - ## This is very fast, but potentially dangerous. Use this iterator only - ## if you require **ALL** the rows. - ## - ## **Note:** Breaking the `fastRows()` iterator during a loop will cause the - ## next database query to raise a `DbError` exception `unable to close due - ## to ...`. - ## - ## **Examples:** - ## - ## .. code-block:: Nim - ## - ## let db = open("mytest.db", "", "", "") - ## - ## # Records of my_table: - ## # | id | name | - ## # |----|----------| - ## # | 1 | item#1 | - ## # | 2 | item#2 | - ## - ## for row in db.fastRows(sql"SELECT id, name FROM my_table"): - ## echo row - ## - ## # Output: - ## # @["1", "item#1"] - ## # @["2", "item#2"] - ## - ## db.close() - var stmt = setupQuery(db, query, args) - var L = (column_count(stmt)) - var result = newRow(L) - try: - while step(stmt) == SQLITE_ROW: - setRow(stmt, result, L) - yield result - finally: - if finalize(stmt) != SQLITE_OK: dbError(db) - -iterator fastRows*(db: DbConn, stmtName: SqlPrepared): Row - {.tags: [ReadDbEffect,WriteDbEffect], since: (1, 3).} = - discard setupQuery(db, stmtName) - var L = (column_count(stmtName.PStmt)) - var result = newRow(L) - try: - while step(stmtName.PStmt) == SQLITE_ROW: - setRow(stmtName.PStmt, result, L) - yield result - except: - dbError(db) - -iterator instantRows*(db: DbConn, query: SqlQuery, - args: varargs[string, `$`]): InstantRow - {.tags: [ReadDbEffect].} = - ## Similar to `fastRows iterator <#fastRows.i,DbConn,SqlQuery,varargs[string,]>`_ - ## but returns a handle that can be used to get column text - ## on demand using `[]`. Returned handle is valid only within the iterator body. - ## - ## **Examples:** - ## - ## .. code-block:: Nim - ## - ## let db = open("mytest.db", "", "", "") - ## - ## # Records of my_table: - ## # | id | name | - ## # |----|----------| - ## # | 1 | item#1 | - ## # | 2 | item#2 | - ## - ## for row in db.instantRows(sql"SELECT * FROM my_table"): - ## echo "id:" & row[0] - ## echo "name:" & row[1] - ## echo "length:" & $len(row) - ## - ## # Output: - ## # id:1 - ## # name:item#1 - ## # length:2 - ## # id:2 - ## # name:item#2 - ## # length:2 - ## - ## db.close() - var stmt = setupQuery(db, query, args) - try: - while step(stmt) == SQLITE_ROW: - yield stmt - finally: - if finalize(stmt) != SQLITE_OK: dbError(db) - -iterator instantRows*(db: DbConn, stmtName: SqlPrepared): InstantRow - {.tags: [ReadDbEffect,WriteDbEffect], since: (1, 3).} = - var stmt = setupQuery(db, stmtName).PStmt - try: - while step(stmt) == SQLITE_ROW: - yield stmt - except: - dbError(db) - -proc toTypeKind(t: var DbType; x: int32) = - case x - of SQLITE_INTEGER: - t.kind = dbInt - t.size = 8 - of SQLITE_FLOAT: - t.kind = dbFloat - t.size = 8 - of SQLITE_BLOB: t.kind = dbBlob - of SQLITE_NULL: t.kind = dbNull - of SQLITE_TEXT: t.kind = dbVarchar - else: t.kind = dbUnknown - -proc setColumns(columns: var DbColumns; x: PStmt) = - let L = column_count(x) - setLen(columns, L) - for i in 0'i32 ..< L: - columns[i].name = $column_name(x, i) - columns[i].typ.name = $column_decltype(x, i) - toTypeKind(columns[i].typ, column_type(x, i)) - columns[i].tableName = $column_table_name(x, i) - -iterator instantRows*(db: DbConn; columns: var DbColumns; query: SqlQuery, - args: varargs[string, `$`]): InstantRow - {.tags: [ReadDbEffect].} = - ## Similar to `instantRows iterator <#instantRows.i,DbConn,SqlQuery,varargs[string,]>`_, - ## but sets information about columns to `columns`. - ## - ## **Examples:** - ## - ## .. code-block:: Nim - ## - ## let db = open("mytest.db", "", "", "") - ## - ## # Records of my_table: - ## # | id | name | - ## # |----|----------| - ## # | 1 | item#1 | - ## # | 2 | item#2 | - ## - ## var columns: DbColumns - ## for row in db.instantRows(columns, sql"SELECT * FROM my_table"): - ## discard - ## echo columns[0] - ## - ## # Output: - ## # (name: "id", tableName: "my_table", typ: (kind: dbNull, - ## # notNull: false, name: "INTEGER", size: 0, maxReprLen: 0, precision: 0, - ## # scale: 0, min: 0, max: 0, validValues: @[]), primaryKey: false, - ## # foreignKey: false) - ## - ## db.close() - var stmt = setupQuery(db, query, args) - setColumns(columns, stmt) - try: - while step(stmt) == SQLITE_ROW: - yield stmt - finally: - if finalize(stmt) != SQLITE_OK: dbError(db) - -proc `[]`*(row: InstantRow, col: int32): string {.inline.} = - ## Returns text for given column of the row. - ## - ## See also: - ## * `instantRows iterator <#instantRows.i,DbConn,SqlQuery,varargs[string,]>`_ - ## example code - $column_text(row, col) - -proc unsafeColumnAt*(row: InstantRow, index: int32): cstring {.inline.} = - ## Returns cstring for given column of the row. - ## - ## See also: - ## * `instantRows iterator <#instantRows.i,DbConn,SqlQuery,varargs[string,]>`_ - ## example code - column_text(row, index) - -proc len*(row: InstantRow): int32 {.inline.} = - ## Returns number of columns in a row. - ## - ## See also: - ## * `instantRows iterator <#instantRows.i,DbConn,SqlQuery,varargs[string,]>`_ - ## example code - column_count(row) - -proc getRow*(db: DbConn, query: SqlQuery, - args: varargs[string, `$`]): Row {.tags: [ReadDbEffect].} = - ## Retrieves a single row. If the query doesn't return any rows, this proc - ## will return a `Row` with empty strings for each column. - ## - ## **Examples:** - ## - ## .. code-block:: Nim - ## - ## let db = open("mytest.db", "", "", "") - ## - ## # Records of my_table: - ## # | id | name | - ## # |----|----------| - ## # | 1 | item#1 | - ## # | 2 | item#2 | - ## - ## doAssert db.getRow(sql"SELECT id, name FROM my_table" - ## ) == Row(@["1", "item#1"]) - ## doAssert db.getRow(sql"SELECT id, name FROM my_table WHERE id = ?", - ## 2) == Row(@["2", "item#2"]) - ## - ## # Returns empty. - ## doAssert db.getRow(sql"INSERT INTO my_table (id, name) VALUES (?, ?)", - ## 3, "item#3") == @[] - ## doAssert db.getRow(sql"DELETE FROM my_table WHERE id = ?", 3) == @[] - ## doAssert db.getRow(sql"UPDATE my_table SET name = 'ITEM#1' WHERE id = ?", - ## 1) == @[] - ## db.close() - var stmt = setupQuery(db, query, args) - var L = (column_count(stmt)) - result = newRow(L) - if step(stmt) == SQLITE_ROW: - setRow(stmt, result, L) - if finalize(stmt) != SQLITE_OK: dbError(db) - -proc getAllRows*(db: DbConn, query: SqlQuery, - args: varargs[string, `$`]): seq[Row] {.tags: [ReadDbEffect].} = - ## Executes the query and returns the whole result dataset. - ## - ## **Examples:** - ## - ## .. code-block:: Nim - ## - ## let db = open("mytest.db", "", "", "") - ## - ## # Records of my_table: - ## # | id | name | - ## # |----|----------| - ## # | 1 | item#1 | - ## # | 2 | item#2 | - ## - ## doAssert db.getAllRows(sql"SELECT id, name FROM my_table") == @[Row(@["1", "item#1"]), Row(@["2", "item#2"])] - ## db.close() - result = @[] - for r in fastRows(db, query, args): - result.add(r) - -proc getAllRows*(db: DbConn, stmtName: SqlPrepared): seq[Row] - {.tags: [ReadDbEffect,WriteDbEffect], since: (1, 3).} = - result = @[] - for r in fastRows(db, stmtName): - result.add(r) - -iterator rows*(db: DbConn, query: SqlQuery, - args: varargs[string, `$`]): Row {.tags: [ReadDbEffect].} = - ## Similar to `fastRows iterator <#fastRows.i,DbConn,SqlQuery,varargs[string,]>`_, - ## but slower and safe. - ## - ## **Examples:** - ## - ## .. code-block:: Nim - ## - ## let db = open("mytest.db", "", "", "") - ## - ## # Records of my_table: - ## # | id | name | - ## # |----|----------| - ## # | 1 | item#1 | - ## # | 2 | item#2 | - ## - ## for row in db.rows(sql"SELECT id, name FROM my_table"): - ## echo row - ## - ## ## Output: - ## ## @["1", "item#1"] - ## ## @["2", "item#2"] - ## - ## db.close() - for r in fastRows(db, query, args): yield r - -iterator rows*(db: DbConn, stmtName: SqlPrepared): Row - {.tags: [ReadDbEffect,WriteDbEffect], since: (1, 3).} = - for r in fastRows(db, stmtName): yield r - -proc getValue*(db: DbConn, query: SqlQuery, - args: varargs[string, `$`]): string {.tags: [ReadDbEffect].} = - ## Executes the query and returns the first column of the first row of the - ## result dataset. Returns `""` if the dataset contains no rows or the database - ## value is `NULL`. - ## - ## **Examples:** - ## - ## .. code-block:: Nim - ## - ## let db = open("mytest.db", "", "", "") - ## - ## # Records of my_table: - ## # | id | name | - ## # |----|----------| - ## # | 1 | item#1 | - ## # | 2 | item#2 | - ## - ## doAssert db.getValue(sql"SELECT name FROM my_table WHERE id = ?", - ## 2) == "item#2" - ## doAssert db.getValue(sql"SELECT id, name FROM my_table") == "1" - ## doAssert db.getValue(sql"SELECT name, id FROM my_table") == "item#1" - ## - ## db.close() - var stmt = setupQuery(db, query, args) - if step(stmt) == SQLITE_ROW: - let cb = column_bytes(stmt, 0) - if cb == 0: - result = "" - else: - if column_type(stmt, 0) == SQLITE_BLOB: - result.setLen(cb) - copyMem(addr(result[0]), column_blob(stmt, 0), cb) - else: - result = newStringOfCap(cb) - add(result, column_text(stmt, 0)) - else: - result = "" - if finalize(stmt) != SQLITE_OK: dbError(db) - -proc getValue*(db: DbConn, stmtName: SqlPrepared): string - {.tags: [ReadDbEffect,WriteDbEffect], since: (1, 3).} = - var stmt = setupQuery(db, stmtName).PStmt - if step(stmt) == SQLITE_ROW: - let cb = column_bytes(stmt, 0) - if cb == 0: - result = "" - else: - if column_type(stmt, 0) == SQLITE_BLOB: - result.setLen(cb) - copyMem(addr(result[0]), column_blob(stmt, 0), cb) - else: - result = newStringOfCap(cb) - add(result, column_text(stmt, 0)) - else: - result = "" - -proc tryInsertID*(db: DbConn, query: SqlQuery, - args: varargs[string, `$`]): int64 - {.tags: [WriteDbEffect], raises: [].} = - ## Executes the query (typically "INSERT") and returns the - ## generated ID for the row or -1 in case of an error. - ## - ## **Examples:** - ## - ## .. code-block:: Nim - ## - ## let db = open("mytest.db", "", "", "") - ## db.exec(sql"CREATE TABLE my_table (id INTEGER, name VARCHAR(50) NOT NULL)") - ## - ## doAssert db.tryInsertID(sql"INSERT INTO not_exist_table (id, name) VALUES (?, ?)", - ## 1, "item#1") == -1 - ## db.close() - assert(not db.isNil, "Database not connected.") - var q = dbFormat(query, args) - var stmt: sqlite3.PStmt - result = -1 - if prepare_v2(db, q, q.len.cint, stmt, nil) == SQLITE_OK: - if step(stmt) == SQLITE_DONE: - result = last_insert_rowid(db) - if finalize(stmt) != SQLITE_OK: - result = -1 - else: - discard finalize(stmt) - -proc insertID*(db: DbConn, query: SqlQuery, - args: varargs[string, `$`]): int64 {.tags: [WriteDbEffect].} = - ## Executes the query (typically "INSERT") and returns the - ## generated ID for the row. - ## - ## Raises a `DbError` exception when failed to insert row. - ## For Postgre this adds `RETURNING id` to the query, so it only works - ## if your primary key is named `id`. - ## - ## **Examples:** - ## - ## .. code-block:: Nim - ## - ## let db = open("mytest.db", "", "", "") - ## db.exec(sql"CREATE TABLE my_table (id INTEGER, name VARCHAR(50) NOT NULL)") - ## - ## for i in 0..2: - ## let id = db.insertID(sql"INSERT INTO my_table (id, name) VALUES (?, ?)", i, "item#" & $i) - ## echo "LoopIndex = ", i, ", InsertID = ", id - ## - ## # Output: - ## # LoopIndex = 0, InsertID = 1 - ## # LoopIndex = 1, InsertID = 2 - ## # LoopIndex = 2, InsertID = 3 - ## - ## db.close() - result = tryInsertID(db, query, args) - if result < 0: dbError(db) - -proc tryInsert*(db: DbConn, query: SqlQuery, pkName: string, - args: varargs[string, `$`]): int64 - {.tags: [WriteDbEffect], raises: [], since: (1, 3).} = - ## same as tryInsertID - tryInsertID(db, query, args) - -proc insert*(db: DbConn, query: SqlQuery, pkName: string, - args: varargs[string, `$`]): int64 - {.tags: [WriteDbEffect], since: (1, 3).} = - ## same as insertId - result = tryInsert(db, query,pkName, args) - if result < 0: dbError(db) - -proc execAffectedRows*(db: DbConn, query: SqlQuery, - args: varargs[string, `$`]): int64 {. - tags: [ReadDbEffect, WriteDbEffect].} = - ## Executes the query (typically "UPDATE") and returns the - ## number of affected rows. - ## - ## **Examples:** - ## - ## .. code-block:: Nim - ## - ## let db = open("mytest.db", "", "", "") - ## - ## # Records of my_table: - ## # | id | name | - ## # |----|----------| - ## # | 1 | item#1 | - ## # | 2 | item#2 | - ## - ## doAssert db.execAffectedRows(sql"UPDATE my_table SET name = 'TEST'") == 2 - ## - ## db.close() - exec(db, query, args) - result = changes(db) - -proc execAffectedRows*(db: DbConn, stmtName: SqlPrepared): int64 - {.tags: [ReadDbEffect, WriteDbEffect],since: (1, 3).} = - exec(db, stmtName) - result = changes(db) - -proc close*(db: DbConn) {.tags: [DbEffect].} = - ## Closes the database connection. - ## - ## **Examples:** - ## - ## .. code-block:: Nim - ## - ## let db = open("mytest.db", "", "", "") - ## db.close() - if sqlite3.close(db) != SQLITE_OK: dbError(db) - -proc open*(connection, user, password, database: string): DbConn {. - tags: [DbEffect].} = - ## Opens a database connection. Raises a `DbError` exception if the connection - ## could not be established. - ## - ## **Note:** Only the `connection` parameter is used for `sqlite`. - ## - ## **Examples:** - ## - ## .. code-block:: Nim - ## - ## try: - ## let db = open("mytest.db", "", "", "") - ## ## do something... - ## ## db.getAllRows(sql"SELECT * FROM my_table") - ## db.close() - ## except: - ## stderr.writeLine(getCurrentExceptionMsg()) - var db: DbConn - if sqlite3.open(connection, db) == SQLITE_OK: - result = db - else: - dbError(db) - -proc setEncoding*(connection: DbConn, encoding: string): bool {. - tags: [DbEffect].} = - ## Sets the encoding of a database connection, returns `true` for - ## success, `false` for failure. - ## - ## **Note:** The encoding cannot be changed once it's been set. - ## According to SQLite3 documentation, any attempt to change - ## the encoding after the database is created will be silently - ## ignored. - exec(connection, sql"PRAGMA encoding = ?", [encoding]) - result = connection.getValue(sql"PRAGMA encoding") == encoding - -proc finalize*(sqlPrepared:SqlPrepared) {.discardable, since: (1, 3).} = - discard finalize(sqlPrepared.PStmt) - -template dbBindParamError*(paramIdx: int, val: varargs[untyped]) = - ## Raises a `DbError` exception. - var e: ref DbError - new(e) - e.msg = "error binding param in position " & $paramIdx - raise e - -proc bindParam*(ps: SqlPrepared, paramIdx: int, val: int32) {.since: (1, 3).} = - ## Binds a int32 to the specified paramIndex. - if bind_int(ps.PStmt, paramIdx.int32, val) != SQLITE_OK: - dbBindParamError(paramIdx, val) - -proc bindParam*(ps: SqlPrepared, paramIdx: int, val: int64) {.since: (1, 3).} = - ## Binds a int64 to the specified paramIndex. - if bind_int64(ps.PStmt, paramIdx.int32, val) != SQLITE_OK: - dbBindParamError(paramIdx, val) - -proc bindParam*(ps: SqlPrepared, paramIdx: int, val: int) {.since: (1, 3).} = - ## Binds a int to the specified paramIndex. - when sizeof(int) == 8: - bindParam(ps, paramIdx, val.int64) - else: - bindParam(ps, paramIdx, val.int32) - -proc bindParam*(ps: SqlPrepared, paramIdx: int, val: float64) {.since: (1, 3).} = - ## Binds a 64bit float to the specified paramIndex. - if bind_double(ps.PStmt, paramIdx.int32, val) != SQLITE_OK: - dbBindParamError(paramIdx, val) - -proc bindNull*(ps: SqlPrepared, paramIdx: int) {.since: (1, 3).} = - ## Sets the bindparam at the specified paramIndex to null - ## (default behaviour by sqlite). - if bind_null(ps.PStmt, paramIdx.int32) != SQLITE_OK: - dbBindParamError(paramIdx) - -proc bindParam*(ps: SqlPrepared, paramIdx: int, val: string, copy = true) {.since: (1, 3).} = - ## Binds a string to the specified paramIndex. - ## if copy is true then SQLite makes its own private copy of the data immediately - if bind_text(ps.PStmt, paramIdx.int32, val.cstring, val.len.int32, if copy: SQLITE_TRANSIENT else: SQLITE_STATIC) != SQLITE_OK: - dbBindParamError(paramIdx, val) - -proc bindParam*(ps: SqlPrepared, paramIdx: int,val: openArray[byte], copy = true) {.since: (1, 3).} = - ## binds a blob to the specified paramIndex. - ## if copy is true then SQLite makes its own private copy of the data immediately - let len = val.len - if bind_blob(ps.PStmt, paramIdx.int32, val[0].unsafeAddr, len.int32, if copy: SQLITE_TRANSIENT else: SQLITE_STATIC) != SQLITE_OK: - dbBindParamError(paramIdx, val) - -macro bindParams*(ps: SqlPrepared, params: varargs[untyped]): untyped {.since: (1, 3).} = - let bindParam = bindSym("bindParam", brOpen) - let bindNull = bindSym("bindNull") - let preparedStatement = genSym() - result = newStmtList() - # Store `ps` in a temporary variable. This prevents `ps` from being evaluated every call. - result.add newNimNode(nnkLetSection).add(newIdentDefs(preparedStatement, newEmptyNode(), ps)) - for idx, param in params: - if param.kind != nnkNilLit: - result.add newCall(bindParam, preparedStatement, newIntLitNode idx + 1, param) - else: - result.add newCall(bindNull, preparedStatement, newIntLitNode idx + 1) - -macro untypedLen(args: varargs[untyped]): int = - newLit(args.len) - -template exec*(db: DbConn, stmtName: SqlPrepared, - args: varargs[typed]): untyped = - when untypedLen(args) > 0: - if reset(stmtName.PStmt) != SQLITE_OK: - dbError(db) - if clear_bindings(stmtName.PStmt) != SQLITE_OK: - dbError(db) - stmtName.bindParams(args) - if not tryExec(db, stmtName): dbError(db) - -when not defined(testing) and isMainModule: - var db = open(":memory:", "", "", "") - exec(db, sql"create table tbl1(one varchar(10), two smallint)", []) - exec(db, sql"insert into tbl1 values('hello!',10)", []) - exec(db, sql"insert into tbl1 values('goodbye', 20)", []) - var p1 = db.prepare "create table tbl2(one varchar(10), two smallint)" - exec(db, p1) - finalize(p1) - var p2 = db.prepare "insert into tbl2 values('hello!',10)" - exec(db, p2) - finalize(p2) - var p3 = db.prepare "insert into tbl2 values('goodbye', 20)" - exec(db, p3) - finalize(p3) - #db.query("create table tbl1(one varchar(10), two smallint)") - #db.query("insert into tbl1 values('hello!',10)") - #db.query("insert into tbl1 values('goodbye', 20)") - for r in db.rows(sql"select * from tbl1", []): - echo(r[0], r[1]) - for r in db.instantRows(sql"select * from tbl1", []): - echo(r[0], r[1]) - var p4 = db.prepare "select * from tbl2" - for r in db.rows(p4): - echo(r[0], r[1]) - finalize(p4) - var i5 = 0 - var p5 = db.prepare "select * from tbl2" - for r in db.instantRows(p5): - inc i5 - echo(r[0], r[1]) - assert i5 == 2 - finalize(p5) - - for r in db.rows(sql"select * from tbl2", []): - echo(r[0], r[1]) - for r in db.instantRows(sql"select * from tbl2", []): - echo(r[0], r[1]) - var p6 = db.prepare "select * from tbl2 where one = ? " - p6.bindParams("goodbye") - var rowsP3 = 0 - for r in db.rows(p6): - rowsP3 = 1 - echo(r[0], r[1]) - assert rowsP3 == 1 - finalize(p6) - - var p7 = db.prepare "select * from tbl2 where two=?" - p7.bindParams(20'i32) - when sizeof(int) == 4: - p7.bindParams(20) - var rowsP = 0 - for r in db.rows(p7): - rowsP = 1 - echo(r[0], r[1]) - assert rowsP == 1 - finalize(p7) - - exec(db, sql"CREATE TABLE photos(ID INTEGER PRIMARY KEY AUTOINCREMENT, photo BLOB)") - var p8 = db.prepare "INSERT INTO photos (ID,PHOTO) VALUES (?,?)" - var d = "abcdefghijklmnopqrstuvwxyz" - p8.bindParams(1'i32, "abcdefghijklmnopqrstuvwxyz") - exec(db, p8) - finalize(p8) - var p10 = db.prepare "INSERT INTO photos (ID,PHOTO) VALUES (?,?)" - p10.bindParams(2'i32,nil) - exec(db, p10) - exec( db, p10, 3, nil) - finalize(p10) - for r in db.rows(sql"select * from photos where ID = 1", []): - assert r[1].len == d.len - assert r[1] == d - var i6 = 0 - for r in db.rows(sql"select * from photos where ID = 3", []): - i6 = 1 - assert i6 == 1 - var p9 = db.prepare("select * from photos where PHOTO is ?") - p9.bindParams(nil) - var rowsP2 = 0 - for r in db.rows(p9): - rowsP2 = 1 - echo(r[0], repr r[1]) - assert rowsP2 == 1 - finalize(p9) - - db_sqlite.close(db) diff --git a/lib/impure/nre.nim b/lib/impure/nre.nim index 7b2d7d3ee..39d238055 100644 --- a/lib/impure/nre.nim +++ b/lib/impure/nre.nim @@ -20,15 +20,17 @@ when defined(js): ## search the internet for a wide variety of third-party documentation and ## tools. ## -## **Note**: If you love `sequtils.toSeq` we have bad news for you. This -## library doesn't work with it due to documented compiler limitations. As -## a workaround, use this: -## -## .. code-block:: nim -## -## import std/nre except toSeq -## -## +## .. warning:: If you love `sequtils.toSeq` we have bad news for you. This +## library doesn't work with it due to documented compiler limitations. As +## a workaround, use this: +runnableExamples: + # either `import std/nre except toSeq` or fully qualify `sequtils.toSeq`: + import std/sequtils + iterator iota(n: int): int = + for i in 0..<n: yield i + assert sequtils.toSeq(iota(3)) == @[0, 1, 2] +## .. note:: There are also alternative nimble packages such as [tinyre](https://github.com/khchen/tinyre) +## and [regex](https://github.com/nitely/nim-regex). ## Licencing ## --------- ## @@ -36,40 +38,38 @@ when defined(js): ## this module. ## ## .. _`some additional terms`: http://pcre.sourceforge.net/license.txt -## runnableExamples: + import std/sugar let vowels = re"[aeoui]" - - let expectedResults = [ - 1 .. 1, - 2 .. 2, - 4 .. 4, - 6 .. 6, - 7 .. 7, - ] - var i = 0 - for match in "moigagoo".findIter(vowels): - doAssert match.matchBounds == expectedResults[i] - inc i + let bounds = collect: + for match in "moiga".findIter(vowels): match.matchBounds + assert bounds == @[1 .. 1, 2 .. 2, 4 .. 4] + from std/sequtils import toSeq + let s = sequtils.toSeq("moiga".findIter(vowels)) + # fully qualified to avoid confusion with nre.toSeq + assert s.len == 3 let firstVowel = "foo".find(vowels) let hasVowel = firstVowel.isSome() - if hasVowel: - let matchBounds = firstVowel.get().captureBounds[-1] - doAssert matchBounds.a == 1 + assert hasVowel + let matchBounds = firstVowel.get().captureBounds[-1] + assert matchBounds.a == 1 - ## as with module `re`, unless specified otherwise, `start` parameter in each - ## proc indicates where the scan starts, but outputs are relative to the start - ## of the input string, not to `start`: - doAssert find("uxabc", re"(?<=x|y)ab", start = 1).get.captures[-1] == "ab" - doAssert find("uxabc", re"ab", start = 3).isNone + # as with module `re`, unless specified otherwise, `start` parameter in each + # proc indicates where the scan starts, but outputs are relative to the start + # of the input string, not to `start`: + assert find("uxabc", re"(?<=x|y)ab", start = 1).get.captures[-1] == "ab" + assert find("uxabc", re"ab", start = 3).isNone -from pcre import nil +from std/pcre import nil import nre/private/util -import tables -from strutils import `%` -import options -from unicode import runeLenAt +import std/tables +from std/strutils import `%` +import std/options +from std/unicode import runeLenAt + +when defined(nimPreviewSlimSystem): + import std/assertions export options @@ -80,16 +80,16 @@ type ## comment".` ## ## `pattern: string` - ## the string that was used to create the pattern. For details on how + ## : the string that was used to create the pattern. For details on how ## to write a pattern, please see `the official PCRE pattern ## documentation. ## <https://www.pcre.org/original/doc/html/pcrepattern.html>`_ ## ## `captureCount: int` - ## the number of captures that the pattern has. + ## : the number of captures that the pattern has. ## ## `captureNameId: Table[string, int]` - ## a table from the capture names to their numeric id. + ## : a table from the capture names to their numeric id. ## ## ## Options @@ -145,7 +145,7 @@ type ## `DOLLAR_ENDONLY`, `FIRSTLINE`, `NO_AUTO_CAPTURE`, ## `JAVASCRIPT_COMPAT`, `U`, `NO_STUDY`. In other PCRE wrappers, you ## will need to pass these as separate flags to PCRE. - pattern*: string ## not nil + pattern*: string pcreObj: ptr pcre.Pcre ## not nil pcreExtra: ptr pcre.ExtraData ## nil @@ -156,49 +156,39 @@ type ## execution. On failure, it is none, on success, it is some. ## ## `pattern: Regex` - ## the pattern that is being matched + ## : the pattern that is being matched ## ## `str: string` - ## the string that was matched against + ## : the string that was matched against ## ## `captures[]: string` - ## the string value of whatever was captured at that id. If the value + ## : the string value of whatever was captured at that id. If the value ## is invalid, then behavior is undefined. If the id is `-1`, then ## the whole match is returned. If the given capture was not matched, - ## `nil` is returned. - ## - ## - `"abc".match(re"(\w)").get.captures[0] == "a"` - ## - `"abc".match(re"(?<letter>\w)").get.captures["letter"] == "a"` - ## - `"abc".match(re"(\w)\w").get.captures[-1] == "ab"` + ## `nil` is returned. See examples for `match`. ## ## `captureBounds[]: HSlice[int, int]` - ## gets the bounds of the given capture according to the same rules as + ## : gets the bounds of the given capture according to the same rules as ## the above. If the capture is not filled, then `None` is returned. - ## The bounds are both inclusive. - ## - ## - `"abc".match(re"(\w)").get.captureBounds[0] == 0 .. 0` - ## - `0 in "abc".match(re"(\w)").get.captureBounds == true` - ## - `"abc".match(re"").get.captureBounds[-1] == 0 .. -1` - ## - `"abc".match(re"abc").get.captureBounds[-1] == 0 .. 2` + ## The bounds are both inclusive. See examples for `match`. ## ## `match: string` - ## the full text of the match. + ## : the full text of the match. ## ## `matchBounds: HSlice[int, int]` - ## the bounds of the match, as in `captureBounds[]` + ## : the bounds of the match, as in `captureBounds[]` ## ## `(captureBounds|captures).toTable` - ## returns a table with each named capture as a key. + ## : returns a table with each named capture as a key. ## ## `(captureBounds|captures).toSeq` - ## returns all the captures by their number. + ## : returns all the captures by their number. ## ## `$: string` - ## same as `match` + ## : same as `match` pattern*: Regex ## The regex doing the matching. ## Not nil. str*: string ## The string that was matched against. - ## Not nil. pcreMatchBounds: seq[HSlice[cint, cint]] ## First item is the bounds of the match ## Other items are the captures ## `a` is inclusive start, `b` is exclusive end @@ -226,22 +216,12 @@ type ## for whatever reason. The message contains the error ## code. -runnableExamples: - # This MUST be kept in sync with the examples in RegexMatch - doAssert "abc".match(re"(\w)").get.captures[0] == "a" - doAssert "abc".match(re"(?<letter>\w)").get.captures["letter"] == "a" - doAssert "abc".match(re"(\w)\w").get.captures[-1] == "ab" - - doAssert "abc".match(re"(\w)").get.captureBounds[0] == 0 .. 0 - doAssert 0 in "abc".match(re"(\w)").get.captureBounds == true - doAssert "abc".match(re"").get.captureBounds[-1] == 0 .. -1 - doAssert "abc".match(re"abc").get.captureBounds[-1] == 0 .. 2 - - proc destroyRegex(pattern: Regex) = + `=destroy`(pattern.pattern) pcre.free_substring(cast[cstring](pattern.pcreObj)) if pattern.pcreExtra != nil: pcre.free_study(pattern.pcreExtra) + `=destroy`(pattern.captureNameToId) proc getinfo[T](pattern: Regex, opt: cint): T = let retcode = pcre.fullinfo(pattern.pcreObj, pattern.pcreExtra, opt, addr result) @@ -308,7 +288,7 @@ proc matchesCrLf(pattern: Regex): bool = let newlineFlags = flags and (pcre.NEWLINE_CRLF or pcre.NEWLINE_ANY or pcre.NEWLINE_ANYCRLF) - if newLineFlags > 0u32: + if newlineFlags > 0u32: return true # get flags from build config @@ -540,24 +520,33 @@ proc matchImpl(str: string, pattern: Regex, start, endpos: int, flags: int): Opt proc match*(str: string, pattern: Regex, start = 0, endpos = int.high): Option[RegexMatch] = ## Like `find(...)<#find,string,Regex,int>`_, but anchored to the start of the ## string. - ## runnableExamples: - doAssert "foo".match(re"f").isSome - doAssert "foo".match(re"o").isNone + assert "foo".match(re"f").isSome + assert "foo".match(re"o").isNone + assert "abc".match(re"(\w)").get.captures[0] == "a" + assert "abc".match(re"(?<letter>\w)").get.captures["letter"] == "a" + assert "abc".match(re"(\w)\w").get.captures[-1] == "ab" + + assert "abc".match(re"(\w)").get.captureBounds[0] == 0 .. 0 + assert 0 in "abc".match(re"(\w)").get.captureBounds + assert "abc".match(re"").get.captureBounds[-1] == 0 .. -1 + assert "abc".match(re"abc").get.captureBounds[-1] == 0 .. 2 return str.matchImpl(pattern, start, endpos, pcre.ANCHORED) iterator findIter*(str: string, pattern: Regex, start = 0, endpos = int.high): RegexMatch = ## Works the same as `find(...)<#find,string,Regex,int>`_, but finds every - ## non-overlapping match. `"2222".find(re"22")` is `"22", "22"`, not - ## `"22", "22", "22"`. - ## + ## non-overlapping match: + runnableExamples: + import std/sugar + assert collect(for a in "2222".findIter(re"22"): a.match) == @["22", "22"] + # not @["22", "22", "22"] ## Arguments are the same as `find(...)<#find,string,Regex,int>`_ ## ## Variants: ## ## - `proc findAll(...)` returns a `seq[string]` - # see pcredemo for explanation + # see pcredemo for explanation => https://www.pcre.org/original/doc/html/pcredemo.html let matchesCrLf = pattern.matchesCrLf() let unicode = uint32(getinfo[culong](pattern, pcre.INFO_OPTIONS) and pcre.UTF8) > 0u32 @@ -578,7 +567,7 @@ iterator findIter*(str: string, pattern: Regex, start = 0, endpos = int.high): R # either the end of the input or the string # cannot be split here - we also need to bail # if we've never matched and we've already tried to... - if offset >= strlen or neverMatched: + if flags == 0 or offset >= strlen or neverMatched: # All matches found break if matchesCrLf and offset < (str.len - 1) and @@ -594,7 +583,6 @@ iterator findIter*(str: string, pattern: Regex, start = 0, endpos = int.high): R else: neverMatched = false offset = match.get.matchBounds.b + 1 - yield match.get proc find*(str: string, pattern: Regex, start = 0, endpos = int.high): Option[RegexMatch] = @@ -602,11 +590,11 @@ proc find*(str: string, pattern: Regex, start = 0, endpos = int.high): Option[Re ## positions. ## ## `start` - ## The start point at which to start matching. `|abc` is `0`; + ## : The start point at which to start matching. `|abc` is `0`; ## `a|bc` is `1` ## ## `endpos` - ## The maximum index for a match; `int.high` means the end of the + ## : The maximum index for a match; `int.high` means the end of the ## string, otherwise it’s an inclusive upper bound. return str.matchImpl(pattern, start, endpos, 0) @@ -619,11 +607,10 @@ proc contains*(str: string, pattern: Regex, start = 0, endpos = int.high): bool ## Determine if the string contains the given pattern between the end and ## start positions: ## This function is equivalent to `isSome(str.find(pattern, start, endpos))`. - ## runnableExamples: - doAssert "abc".contains(re"bc") - doAssert not "abc".contains(re"cd") - doAssert not "abc".contains(re"a", start = 1) + assert "abc".contains(re"bc") + assert not "abc".contains(re"cd") + assert not "abc".contains(re"a", start = 1) return isSome(str.find(pattern, start, endpos)) @@ -635,16 +622,16 @@ proc split*(str: string, pattern: Regex, maxSplit = -1, start = 0): seq[string] ## runnableExamples: # - If the match is zero-width, then the string is still split: - doAssert "123".split(re"") == @["1", "2", "3"] + assert "123".split(re"") == @["1", "2", "3"] # - If the pattern has a capture in it, it is added after the string # split: - doAssert "12".split(re"(\d)") == @["", "1", "", "2", ""] + assert "12".split(re"(\d)") == @["", "1", "", "2", ""] # - If `maxsplit != -1`, then the string will only be split # `maxsplit - 1` times. This means that there will be `maxsplit` # strings in the output seq. - doAssert "1.2.3".split(re"\.", maxsplit = 2) == @["1", "2.3"] + assert "1.2.3".split(re"\.", maxsplit = 2) == @["1", "2.3"] result = @[] var lastIdx = start @@ -715,8 +702,7 @@ proc replace*(str: string, pattern: Regex, ## each match and the return value is the replacement value. ## ## If `subproc` is a `proc (string): string`, then it is executed with the - ## full text of the match and and the return value is the replacement - ## value. + ## full text of the match and the return value is the replacement value. ## ## If `subproc` is a string, the syntax is as follows: ## @@ -747,9 +733,9 @@ proc escapeRe*(str: string): string {.gcsafe.} = ## ## Escaped char: `\ + * ? [ ^ ] $ ( ) { } = ! < > | : -` runnableExamples: - doAssert escapeRe("fly+wind") == "fly\\+wind" - doAssert escapeRe("!") == "\\!" - doAssert escapeRe("nim*") == "nim\\*" + assert escapeRe("fly+wind") == "fly\\+wind" + assert escapeRe("!") == "\\!" + assert escapeRe("nim*") == "nim\\*" #([\\+*?[^\]$(){}=!<>|:-]) const SpecialCharMatcher = {'\\', '+', '*', '?', '[', '^', ']', '$', '(', diff --git a/lib/impure/nre/private/util.nim b/lib/impure/nre/private/util.nim index d227dcba3..ed8420776 100644 --- a/lib/impure/nre/private/util.nim +++ b/lib/impure/nre/private/util.nim @@ -1,5 +1,5 @@ ## INTERNAL FILE FOR USE ONLY BY nre.nim. -import tables +import std/tables const Ident = {'a'..'z', 'A'..'Z', '0'..'9', '_', '\128'..'\255'} const StartIdent = Ident - {'0'..'9'} diff --git a/lib/impure/rdstdin.nim b/lib/impure/rdstdin.nim index c580b89d1..f4fc26380 100644 --- a/lib/impure/rdstdin.nim +++ b/lib/impure/rdstdin.nim @@ -22,11 +22,16 @@ runnableExamples("-r:off"): if line.len > 0: echo line echo "exiting" + when defined(windows): + when defined(nimPreviewSlimSystem): + import std/syncio + proc readLineFromStdin*(prompt: string): string {. tags: [ReadIOEffect, WriteIOEffect].} = ## Reads a line from stdin. stdout.write(prompt) + stdout.flushFile() result = readLine(stdin) proc readLineFromStdin*(prompt: string, line: var string): bool {. @@ -50,17 +55,7 @@ elif defined(genode): stdin.readLine(line) else: - import linenoise - - proc readLineFromStdin*(prompt: string): string {. - tags: [ReadIOEffect, WriteIOEffect].} = - var buffer = linenoise.readLine(prompt) - if isNil(buffer): - raise newException(IOError, "Linenoise returned nil") - result = $buffer - if result.len > 0: - historyAdd(buffer) - linenoise.free(buffer) + import std/linenoise proc readLineFromStdin*(prompt: string, line: var string): bool {. tags: [ReadIOEffect, WriteIOEffect].} = @@ -73,3 +68,7 @@ else: historyAdd(buffer) linenoise.free(buffer) result = true + + proc readLineFromStdin*(prompt: string): string {.inline.} = + if not readLineFromStdin(prompt, result): + raise newException(IOError, "Linenoise returned nil") diff --git a/lib/impure/re.nim b/lib/impure/re.nim index 0c96876b9..053c6ab55 100644 --- a/lib/impure/re.nim +++ b/lib/impure/re.nim @@ -17,6 +17,10 @@ when defined(js): ## C library. This means that your application will depend on the PCRE ## library's licence when using this module, which should not be a problem ## though. +## +## .. note:: There are also alternative nimble packages such as [tinyre](https://github.com/khchen/tinyre) +## and [regex](https://github.com/nitely/nim-regex). +## ## PCRE's licence follows: ## ## .. include:: ../../doc/regexprs.txt @@ -32,7 +36,10 @@ runnableExamples: # can't match start of string since we're starting at 1 import - pcre, strutils, rtarrays + std/[pcre, strutils, rtarrays] + +when defined(nimPreviewSlimSystem): + import std/syncio const MaxSubpatterns* = 20 @@ -41,12 +48,12 @@ const type RegexFlag* = enum ## options for regular expressions - reIgnoreCase = 0, ## do caseless matching - reMultiLine = 1, ## `^` and `$` match newlines within data - reDotAll = 2, ## `.` matches anything including NL - reExtended = 3, ## ignore whitespace and `#` comments - reStudy = 4 ## study the expression (may be omitted if the - ## expression will be used only once) + reIgnoreCase = 0, ## do caseless matching + reMultiLine = 1, ## `^` and `$` match newlines within data + reDotAll = 2, ## `.` matches anything including NL + reExtended = 3, ## ignore whitespace and `#` comments + reStudy = 4 ## study the expression (may be omitted if the + ## expression will be used only once) RegexDesc = object h: ptr Pcre @@ -58,10 +65,16 @@ type ## is raised if the pattern is no valid regular expression. when defined(gcDestructors): - proc `=destroy`(x: var RegexDesc) = - pcre.free_substring(cast[cstring](x.h)) - if not isNil(x.e): - pcre.free_study(x.e) + when defined(nimAllowNonVarDestructor): + proc `=destroy`(x: RegexDesc) = + pcre.free_substring(cast[cstring](x.h)) + if not isNil(x.e): + pcre.free_study(x.e) + else: + proc `=destroy`(x: var RegexDesc) = + pcre.free_substring(cast[cstring](x.h)) + if not isNil(x.e): + pcre.free_study(x.e) proc raiseInvalidRegex(msg: string) {.noinline, noreturn.} = var e: ref RegexError @@ -141,6 +154,10 @@ proc matchOrFind(buf: cstring, pattern: Regex, matches: var openArray[string], else: matches[i-1] = "" return rawMatches[1] - rawMatches[0] +const MaxReBufSize* = high(cint) + ## Maximum PCRE (API 1) buffer start/size equal to `high(cint)`, which even + ## for 64-bit systems can be either 2`31`:sup:-1 or 2`63`:sup:-1. + proc findBounds*(buf: cstring, pattern: Regex, matches: var openArray[string], start = 0, bufSize: int): tuple[first, last: int] = ## returns the starting position and end position of `pattern` in `buf` @@ -148,6 +165,9 @@ proc findBounds*(buf: cstring, pattern: Regex, matches: var openArray[string], ## and the captured ## substrings in the array `matches`. If it does not match, nothing ## is written into `matches` and `(-1,0)` is returned. + ## + ## Note: The memory for `matches` needs to be allocated before this function is + ## called, otherwise it will just remain empty. var rtarray = initRtArray[cint]((matches.len+1)*3) rawMatches = rtarray.getRawData @@ -167,16 +187,27 @@ proc findBounds*(s: string, pattern: Regex, matches: var openArray[string], ## and the captured substrings in the array `matches`. ## If it does not match, nothing ## is written into `matches` and `(-1,0)` is returned. - result = findBounds(cstring(s), pattern, matches, start, s.len) + ## + ## .. note:: The memory for `matches` needs to be allocated before this function is called, otherwise it will just remain empty. + runnableExamples: + var matches = newSeq[string](1) + let (first, last) = findBounds("Hello World", re"(W\w+)", matches) + doAssert first == 6 + doAssert last == 10 + doAssert matches[0] == "World" + result = findBounds(cstring(s), pattern, matches, + min(start, MaxReBufSize), min(s.len, MaxReBufSize)) proc findBounds*(buf: cstring, pattern: Regex, matches: var openArray[tuple[first, last: int]], - start = 0, bufSize = 0): tuple[first, last: int] = + start = 0, bufSize: int): tuple[first, last: int] = ## returns the starting position and end position of `pattern` in `buf` ## (where `buf` has length `bufSize` and is not necessarily `'\0'` terminated), ## and the captured substrings in the array `matches`. ## If it does not match, nothing is written into `matches` and ## `(-1,0)` is returned. + ## + ## .. note:: The memory for `matches` needs to be allocated before this function is called, otherwise it will just remain empty. var rtarray = initRtArray[cint]((matches.len+1)*3) rawMatches = rtarray.getRawData @@ -197,7 +228,28 @@ proc findBounds*(s: string, pattern: Regex, ## and the captured substrings in the array `matches`. ## If it does not match, nothing is written into `matches` and ## `(-1,0)` is returned. - result = findBounds(cstring(s), pattern, matches, start, s.len) + ## + ## .. note:: The memory for `matches` needs to be allocated before this function is called, otherwise it will just remain empty. + runnableExamples: + var matches = newSeq[tuple[first, last: int]](1) + let (first, last) = findBounds("Hello World", re"(\w+)", matches) + doAssert first == 0 + doAssert last == 4 + doAssert matches[0] == (0, 4) + result = findBounds(cstring(s), pattern, matches, + min(start, MaxReBufSize), min(s.len, MaxReBufSize)) + +proc findBoundsImpl(buf: cstring, pattern: Regex, + start = 0, bufSize = 0, flags = 0): tuple[first, last: int] = + var rtarray = initRtArray[cint](3) + let rawMatches = rtarray.getRawData + let res = pcre.exec(pattern.h, pattern.e, buf, bufSize.cint, start.cint, flags.int32, + cast[ptr cint](rawMatches), 3) + + if res < 0'i32: + result = (-1, 0) + else: + result = (int(rawMatches[0]), int(rawMatches[1]-1)) proc findBounds*(buf: cstring, pattern: Regex, start = 0, bufSize: int): tuple[first, last: int] = @@ -220,7 +272,8 @@ proc findBounds*(s: string, pattern: Regex, ## Note: there is a speed improvement if the matches do not need to be captured. runnableExamples: assert findBounds("01234abc89", re"abc") == (5,7) - result = findBounds(cstring(s), pattern, start, s.len) + result = findBounds(cstring(s), pattern, + min(start, MaxReBufSize), min(s.len, MaxReBufSize)) proc matchOrFind(buf: cstring, pattern: Regex, start, bufSize: int, flags: cint): cint = var @@ -236,6 +289,8 @@ proc matchLen*(s: string, pattern: Regex, matches: var openArray[string], ## the same as `match`, but it returns the length of the match, ## if there is no match, `-1` is returned. Note that a match length ## of zero can happen. + ## + ## .. note:: The memory for `matches` needs to be allocated before this function is called, otherwise it will just remain empty. result = matchOrFind(cstring(s), pattern, matches, start.cint, s.len.cint, pcre.ANCHORED) proc matchLen*(buf: cstring, pattern: Regex, matches: var openArray[string], @@ -243,6 +298,8 @@ proc matchLen*(buf: cstring, pattern: Regex, matches: var openArray[string], ## the same as `match`, but it returns the length of the match, ## if there is no match, `-1` is returned. Note that a match length ## of zero can happen. + ## + ## .. note:: The memory for `matches` needs to be allocated before this function is called, otherwise it will just remain empty. return matchOrFind(buf, pattern, matches, start.cint, bufSize.cint, pcre.ANCHORED) proc matchLen*(s: string, pattern: Regex, start = 0): int {.inline.} = @@ -273,6 +330,7 @@ proc match*(s: string, pattern: Regex, matches: var openArray[string], ## match, nothing is written into `matches` and `false` is ## returned. ## + ## .. note:: The memory for `matches` needs to be allocated before this function is called, otherwise it will just remain empty. runnableExamples: import std/sequtils var matches: array[2, string] @@ -287,14 +345,18 @@ proc match*(buf: cstring, pattern: Regex, matches: var openArray[string], ## match, nothing is written into `matches` and `false` is ## returned. ## `buf` has length `bufSize` (not necessarily `'\0'` terminated). + ## + ## .. note:: The memory for `matches` needs to be allocated before this function is called, otherwise it will just remain empty. result = matchLen(buf, pattern, matches, start, bufSize) != -1 proc find*(buf: cstring, pattern: Regex, matches: var openArray[string], - start = 0, bufSize = 0): int = + start = 0, bufSize: int): int = ## returns the starting position of `pattern` in `buf` and the captured ## substrings in the array `matches`. If it does not match, nothing ## is written into `matches` and `-1` is returned. ## `buf` has length `bufSize` (not necessarily `'\0'` terminated). + ## + ## .. note:: The memory for `matches` needs to be allocated before this function is called, otherwise it will just remain empty. var rtarray = initRtArray[cint]((matches.len+1)*3) rawMatches = rtarray.getRawData @@ -313,6 +375,8 @@ proc find*(s: string, pattern: Regex, matches: var openArray[string], ## returns the starting position of `pattern` in `s` and the captured ## substrings in the array `matches`. If it does not match, nothing ## is written into `matches` and `-1` is returned. + ## + ## .. note:: The memory for `matches` needs to be allocated before this function is called, otherwise it will just remain empty. result = find(cstring(s), pattern, matches, start, s.len) proc find*(buf: cstring, pattern: Regex, start = 0, bufSize: int): int = @@ -382,7 +446,7 @@ iterator findAll*(buf: cstring, pattern: Regex, start = 0, bufSize: int): string proc findAll*(s: string, pattern: Regex, start = 0): seq[string] {.inline.} = ## returns all matching `substrings` of `s` that match `pattern`. - ## If it does not match, @[] is returned. + ## If it does not match, `@[]` is returned. result = @[] for x in findAll(s, pattern, start): result.add x @@ -396,7 +460,7 @@ template `=~` *(s: string, pattern: Regex): untyped = elif line =~ re"\s*(\#.*)": # matches a comment # note that the implicit `matches` array is different from 1st branch result = $(matches[0],) - else: doAssert false + else: raiseAssert "unreachable" doAssert not declared(matches) doAssert parse("NAME = LENA") == """("NAME", "LENA")""" doAssert parse(" # comment ... ") == """("# comment ... ",)""" @@ -414,6 +478,8 @@ proc contains*(s: string, pattern: Regex, start = 0): bool {.inline.} = proc contains*(s: string, pattern: Regex, matches: var openArray[string], start = 0): bool {.inline.} = ## same as `find(s, pattern, matches, start) >= 0` + ## + ## .. note:: The memory for `matches` needs to be allocated before this function is called, otherwise it will just remain empty. return find(s, pattern, matches, start) >= 0 proc startsWith*(s: string, prefix: Regex): bool {.inline.} = @@ -433,12 +499,16 @@ proc replace*(s: string, sub: Regex, by = ""): string = doAssert "var1=key; var2=key2".replace(re"(\w+)=(\w+)", "?") == "?; ?" result = "" var prev = 0 + var flags = int32(0) while prev < s.len: - var match = findBounds(s, sub, prev) + var match = findBoundsImpl(s.cstring, sub, prev, s.len, flags) + flags = 0 if match.first < 0: break add(result, substr(s, prev, match.first-1)) add(result, by) - if match.last + 1 == prev: break + if match.first > match.last: + # 0-len match + flags = pcre.NOTEMPTY_ATSTART prev = match.last + 1 add(result, substr(s, prev)) @@ -499,19 +569,22 @@ iterator split*(s: string, sep: Regex; maxsplit = -1): string = @["", "this", "is", "an", "example", ""] var last = 0 var splits = maxsplit - var x: int + var x = -1 + if len(s) == 0: + last = 1 + if matchLen(s, sep, 0) == 0: + x = 0 while last <= len(s): var first = last var sepLen = 1 + if x == 0: + inc(last) while last < len(s): x = matchLen(s, sep, last) if x >= 0: sepLen = x break inc(last) - if x == 0: - if last >= len(s): break - inc last if splits == 0: last = len(s) yield substr(s, first, last-1) if splits == 0: break |