├── .github └── workflows │ ├── ci.yml │ └── codeql-analysis.yml ├── .gitignore ├── LICENSE ├── README.md ├── clutch-1.3.2-2.rockspec ├── clutch.c └── test.lua /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | on: [push, pull_request] 3 | 4 | jobs: 5 | test: 6 | strategy: 7 | matrix: 8 | luaVersion: ["5.2", "5.3", "5.4"] 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@master 12 | - uses: leafo/gh-actions-lua@v8 13 | with: 14 | luaVersion: ${{ matrix.luaVersion }} 15 | - uses: leafo/gh-actions-luarocks@v4 16 | - name: build 17 | run: luarocks build 18 | - name: test 19 | run: luarocks test 20 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [master] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [master] 20 | schedule: 21 | - cron: "33 18 * * 3" 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: ["cpp"] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Learn more about CodeQL language support at https://git.io/codeql-language-support 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v2 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v1 46 | with: 47 | languages: ${{ matrix.language }} 48 | # If you wish to specify custom queries, you can do so here or in a config file. 49 | # By default, queries listed here will override any specified in a config file. 50 | # Prefix the list here with "+" to use these queries and those in the config file. 51 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 52 | 53 | - uses: leafo/gh-actions-lua@v8 54 | with: 55 | luaVersion: "5.4" 56 | - uses: leafo/gh-actions-luarocks@v4 57 | - name: build 58 | run: luarocks build 59 | 60 | - name: Perform CodeQL Analysis 61 | uses: github/codeql-action/analyze@v1 62 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.o 2 | *.so 3 | *.rock 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017-2021 Atte Kojo 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so, 8 | subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 15 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 16 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![tests](https://github.com/akojo/clutch/actions/workflows/ci.yml/badge.svg) 2 | 3 | # Clutch Sqlite3 API 4 | 5 | Clutch is a simple and straightforward sqlite3 API for Lua. 6 | Its primary design goal is to offer a effective sqlite3 interface while 7 | staying out of the way as much as possible. 8 | 9 | NB. The following examples use 10 | [suppliers and parts](http://wiki.c2.com/?SupplierPartsDatabase) 11 | as a sample database. 12 | 13 | ## Opening a database connection 14 | 15 | ```lua 16 | clutch = require 'clutch' 17 | db = clutch.open('mydatabase.db') 18 | ``` 19 | 20 | Clutch passes the name of the database file directly to the underlying sqlite3 21 | API, so you can use `':memory:'` as a file name to open an in-memory database, 22 | or an empty string to open a temporary on-disk database. Clutch's unit tests 23 | use the latter mechanism, for example, to create a temporary database for 24 | each test case. 25 | 26 | ## Querying the database 27 | 28 | The primary interface for issuing queries is the `query()` method of the 29 | database connection. It returns an iterator function, returning the query 30 | results one row at a time. A simple query looks like 31 | 32 | ```lua 33 | for r in db:query("select * from p") do 34 | print(r.pname, r.weight) 35 | end 36 | ``` 37 | 38 | Clutch maps the result into Lua tables with the column names as keys. 39 | 40 | As a convenience, `clutch` provides two query shorthands: 41 | 42 | - `queryone()` checks that the query results into exactly one row and returns 43 | that row as a single table. Otherwise it throws an error. 44 | - `queryall()` returns all resulting rows in a Lua array. In case the query 45 | returns an empty result set, the method returns an empty table. 46 | 47 | ## Binding parameters to queries 48 | 49 | ### Named parameters 50 | 51 | The most staightforward way is to provide query parameters in a table: 52 | 53 | ```lua 54 | db:query("select * from p where color = :color", {color = 'Red'}) 55 | ``` 56 | 57 | Since Clutch uses sqlite3's prepare/bind functions internally, named parameters 58 | can be prefixed by `:`, `$` and `@`, which means that all of the following are 59 | equivalent: 60 | 61 | ```sql 62 | select * from p where color = :color 63 | select * from p where color = $color 64 | select * from p where color = @color 65 | ``` 66 | 67 | ### Anonymous and positional parameters 68 | 69 | Since anonymous parameters are indexed from 1 onwards just like Lua arrays, 70 | they're simple to use 71 | 72 | ```lua 73 | db:query("select * from p where color = ?", {'Red'}) 74 | ``` 75 | 76 | Positional parameters can be supplied by suffixing the `?` with a number: 77 | 78 | ```lua 79 | db:query("select * from p where weight = ?2 color = ?1", {'Red', 12}) 80 | ``` 81 | 82 | NB. Even though it is entirely possible to mix named, anonymous and positional 83 | parameters in the same query, I wouldn't recommend trying to do that unless you 84 | really want to confuse your readers. 85 | 86 | For a small number of parameters it is a bit inconvenient to always write 87 | the extra braces, so Clutch supports binding anonymous and positional 88 | parameters also as varargs: 89 | 90 | ```lua 91 | db:query("select * from p where weight < ? and color = ?", 15, 'Red') 92 | ``` 93 | 94 | ### Interpolated parameters 95 | 96 | This feature was inspired by the original SQLite 97 | [Tcl extension](https://sqlite.org/tclsqlite.html). 98 | If there are no extra arguments after the query string Clutch tries to look up 99 | for variables with the same name as the query parameters. This comes in handy 100 | when you have e.g. wrapper functions around common queries: 101 | 102 | ```lua 103 | function getPartByPnum(pnum) 104 | return db:queryone('select * from p where pnum = $pnum') 105 | end 106 | ``` 107 | 108 | ## Issuing updates to the database 109 | 110 | For writing into the database, whether it be DDL statements, inserts or updates, 111 | Clutch offers a single method `update()`. It checks that the query it ran 112 | returns no results and throws an error otherwise. For _INSERT_, _UPDATE_ and 113 | _DELETE_ operations `update()` returns the number of modified rows. 114 | 115 | For example: 116 | 117 | ```lua 118 | local dbsetup = { 119 | [[ 120 | CREATE TABLE p ( 121 | pnum INTEGER NOT NULL PRIMARY KEY, 122 | pname TEXT NOT NULL, 123 | color TEXT NOT NULL, 124 | weight REAL NOT NULL, 125 | city TEXT NOT NULL, 126 | UNIQUE (pname, color, city) 127 | ) 128 | ]], 129 | "INSERT INTO p VALUES (1, 'Nut', 'Red', 12, 'London')", 130 | "INSERT INTO p VALUES (2, 'Bolt', 'Green', 17, 'Paris')", 131 | "INSERT INTO p VALUES (3, 'Screw', 'Blue', 17, 'Oslo')", 132 | "INSERT INTO p VALUES (4, 'Screw', 'Red', 14, 'London')", 133 | "INSERT INTO p VALUES (5, 'Cam', 'Blue', 12, 'Paris')", 134 | "INSERT INTO p VALUES (6, 'Cog', 'Red', 19, 'London')", 135 | } 136 | for _, query in ipairs(dbsetup) do 137 | db:update(query) 138 | end 139 | ``` 140 | 141 | `update()` uses the same code for preparing queries as `query()` and its 142 | friends so you can use all the same mechanisms for parameter binding. 143 | 144 | ## Data types 145 | 146 | ### Query parameters 147 | 148 | When running queries, Clutch maps Lua types given as query parameters to SQLite types as follows: 149 | 150 | - `nil`: _NULL_ 151 | - _number_ represented as an integer: _INTEGER_ (from Lua 5.3 onwards) 152 | - _number_: _REAL_ 153 | - _string_: _TEXT_ 154 | 155 | Using any other Lua type (_function_, _userdata_, _thread_, _table_) as a parameter will result in an error. 156 | 157 | As a convenience, and to follow SQLite [convention on boolean values](https://www.sqlite.org/datatype3.html#boolean_datatype), Lua values _true_ and _false_ are mapped inside Clutch to integers 1 and 0, respectively. 158 | 159 | ### Query results 160 | 161 | When mapping query results into Lua values, SQLite types are mapped to Lua types as follows: 162 | 163 | - _INTEGER_: _number_ (as an integer from Lua 5.3 onwards) 164 | - _REAL_: _number_ 165 | - _TEXT_ and _BLOB_: _string_ 166 | - _NULL_: `nil` 167 | 168 | ## Pragmas 169 | 170 | Since `PRAGMA` statements in SQLite are like any other SQL statements, you can 171 | use them via update and query functions. For example after running 172 | 173 | ```lua 174 | db:update("PRAGMA encoding = 'UTF-16'") 175 | encoding = db:queryone("PRAGMA encoding").encoding 176 | ``` 177 | 178 | variable _encoding_ will have value `UTF-16le`. 179 | 180 | ## Prepared statements 181 | 182 | Clutch supports a straightforward way to use prepared statements. You create a 183 | prepared statement using database `prepare()` method; then bind parameters and 184 | run the statement using its `update()`, `query()`, `queryone()` or `queryall()` 185 | methods. These methods correspond exactly to the database methods of same name. 186 | 187 | For example, to iterate through all red parts: 188 | 189 | ```lua 190 | local stmt = db:prepare("select * from p where color = :color") 191 | for p in stmt:query({color = "Red"}) do 192 | print(p.name) 193 | end 194 | ``` 195 | 196 | Since the statement methods support all the same mechanisms for parameter 197 | binding as the database query methods, this can also be written e.g.: 198 | 199 | ```lua 200 | local stmt = db:prepare("select * from p where color = ?") 201 | for p in stmt:query("Red") do 202 | print(p.name) 203 | end 204 | ``` 205 | 206 | NB. Even though prepared statements support also interpolated parameters, using 207 | them will most likely lead to code that's very hard to decipher. 208 | 209 | As another example, to insert some values into table `p`, and demonstrate yet 210 | another way of binding parameters: 211 | 212 | ```lua 213 | local stmt = db:prepare("insert into p values (?, ?, ?, ?, ?)") 214 | 215 | stmt:update({1, "Nut", "Red", 12.0, "London"}) 216 | stmt:update({2, "Bolt", "Green", 17.0, "Paris"}) 217 | stmt:update({3, "Screw", "Blue", 17.0, "Oslo"}) 218 | ``` 219 | 220 | Calling any of the statement methods will cause the statement to be 221 | reset. This design has two notable implications: 222 | 223 | - It is perfectly safe to not iterate through all resulting rows when using 224 | `query()` 225 | - Mixing calls to an iterator obtained via `query()` and any of the statement 226 | methods will produce unpredictable results 227 | 228 | ## Transactions 229 | 230 | Clutch support transactions using the `transaction()` method. The method takes 231 | as a single parameter a function which will be run inside the transaction. Any 232 | error (be it from sqlite or Lua code) inside a transaction causes it to be 233 | aborted and rolled back. This will also cause the error to be thrown from the 234 | transaction call. 235 | 236 | For example: 237 | 238 | ```lua 239 | db:transaction(function (t) 240 | t:update("insert into p values (7, 'Washer', 'Grey', 5, 'Helsinki')") 241 | t:update("insert into p values (8, 'Washer', 'Black', 7, 'Helsinki')") 242 | end) 243 | ``` 244 | 245 | Since transactions have been implemented using sqlite3 savepoints, they can be 246 | freely nested. In addition, a rollback in an inner transaction doesn't 247 | automatically cause a rollback of the outer transaction. 248 | 249 | ## Error handling 250 | 251 | Whenever the underlying sqlite3 API returns anything else than success for 252 | a call, Clutch throws an error with the sqlite3 error message as message. 253 | 254 | ## Missing values, *NULL*s and nils 255 | 256 | There any many ways to handle mapping SQL *NULL*s into host language and vice 257 | versa. Clutch takes the approach that whenever `nil` would mean "missing value" 258 | in Lua, it is mapped to SQL _NULL_. 259 | 260 | This means that missing parameter values in all different methods of parameter 261 | binding are converted to SQL *NULL*s. So, if you omit or misspell a table key, 262 | misspell an interpolated variable name or omit some of the arguments from a 263 | vararg call, a _NULL_ is bound to the parameter in question. 264 | 265 | Also if an SQL query returns _NULL_ for some column in a row, the resulting 266 | table won't have a value for a key with that name. 267 | 268 | The result of all this is that any row returned by a query is valid parameter 269 | mapping for a corresponding insert or update. It also means that you don't 270 | have to write awkward code mapping special NULL values to nil and vice versa in 271 | your sqlite3 interface code. 272 | 273 | As a sidenote, if you follow the practice of using _NOT NULL_ by default for 274 | SQL table columns, database constraint checks will catch the aforementioned 275 | errors. And it does so more reliably than any library code could. 276 | 277 | ## Building, installing and running tests 278 | 279 | Clutch is distributed as a Luarock, so the easiest way to install it is: 280 | 281 | ```sh 282 | $ luarocks install clutch 283 | ``` 284 | 285 | The Sqlite3 library is always dynamically linked, which means that you have to 286 | have it installed somewhere where the Lua dynamic loader can find it. 287 | 288 | Additionally, since Clutch consists of a single C file you can link it 289 | statically into your custom Lua application by including `clutch.c` into your 290 | project and calling `luaopen_clutch()` from your `main()`, for example. 291 | 292 | Clutch uses luarocks "builtin" build mechanism, so you can also build it from 293 | source easily: 294 | 295 | ```sh 296 | $ luarocks make 297 | ``` 298 | 299 | To run Clutch unit tests you need `luaunit` rock. The test can be run with: 300 | 301 | ```sh 302 | $ lua test.lua 303 | ``` 304 | -------------------------------------------------------------------------------- /clutch-1.3.2-2.rockspec: -------------------------------------------------------------------------------- 1 | rockspec_format = "3.0" 2 | package = "clutch" 3 | version = "1.3.2-2" 4 | source = { 5 | url = "git://github.com/akojo/clutch", 6 | tag = "v1.3.2" 7 | } 8 | description = { 9 | summary = "A simple API for sqlite3", 10 | detailed = [[ 11 | clutch is an API for sqlite3 designed for simplicity and ease of use. 12 | It hides all the complexities of binding parameters to SQL queries and 13 | parsing the results returned by queries, allowing you to concentrate 14 | on writing application code. 15 | ]], 16 | homepage = "https://github.com/akojo/clutch", 17 | license = "MIT" 18 | } 19 | dependencies = { 20 | "lua >= 5.2" 21 | } 22 | external_dependencies = { 23 | LIBSQLITE3 = { 24 | header = "sqlite3.h" 25 | } 26 | } 27 | build = { 28 | type = "builtin", 29 | modules = { 30 | clutch = { 31 | incdirs = { 32 | "$(LIBSQLITE3_INCDIR)" 33 | }, 34 | libdirs = { 35 | "$(LIBSQLITE3_LIBDIR)" 36 | }, 37 | libraries = { 38 | "sqlite3" 39 | }, 40 | sources = "clutch.c" 41 | } 42 | } 43 | } 44 | test_dependencies = { 45 | "luaunit" 46 | } 47 | -------------------------------------------------------------------------------- /clutch.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | static void init_db_metatable(lua_State *L); 9 | static void init_statement_metatable(lua_State *L); 10 | 11 | static int clutch_open(lua_State *L); 12 | 13 | static int db_close(lua_State *L); 14 | static int db_prepare(lua_State *L); 15 | static int db_query_all(lua_State *L); 16 | static int db_query_one(lua_State *L); 17 | static int db_query(lua_State *L); 18 | static int db_tostring(lua_State *L); 19 | static int db_transaction(lua_State *L); 20 | static int db_update(lua_State *L); 21 | 22 | static int prep_stmt_all(lua_State *L); 23 | static int prep_stmt_close(lua_State *L); 24 | static int prep_stmt_iter(lua_State *L); 25 | static int prep_stmt_one(lua_State *L); 26 | static int prep_stmt_tostring(lua_State *L); 27 | static int prep_stmt_update(lua_State *L); 28 | 29 | static sqlite3_stmt *rebind_stmt(lua_State *L); 30 | static sqlite3_stmt *prepare_query(lua_State *L); 31 | static sqlite3_stmt *prepare_stmt(lua_State *L, sqlite3 *db); 32 | static int bind_stmt(lua_State *L, sqlite3_stmt *stmt, int nargs); 33 | static int bind_params(lua_State *L, sqlite3_stmt *stmt); 34 | static int bind_varargs(lua_State *L, int nargs, sqlite3_stmt *stmt); 35 | static int bind_lua_vars(lua_State *L, sqlite3_stmt *stmt); 36 | static int bind_one_param(lua_State *L, sqlite3_stmt *stmt, int index); 37 | static int bind_number(lua_State *L, sqlite3_stmt *stmt, int index); 38 | static int bind_string(lua_State *L, sqlite3_stmt *stmt, int index); 39 | static int is_named_parameter(const char *name); 40 | static void find_var(lua_State *L, const char *name); 41 | 42 | static int iter(lua_State *L); 43 | static int step(lua_State *L, sqlite3_stmt *stmt); 44 | static int step_one(lua_State *L, sqlite3_stmt *stmt); 45 | static int step_all(lua_State *L, sqlite3_stmt *stmt); 46 | static void handle_row(lua_State *L, sqlite3_stmt *stmt); 47 | static int update(lua_State *L, sqlite3_stmt *stmt); 48 | 49 | static void close_sqlite(sqlite3 **db); 50 | static void close_sqlite_stmt(sqlite3_stmt **stmt); 51 | 52 | static const char *DB_REGISTRY_KEY = "clutch.sqlite3.db"; 53 | static const char *STMT_REGISTRY_KEY = "clutch.sqlite3.stmt"; 54 | 55 | static const struct luaL_Reg clutch_funcs[] = {{"open", clutch_open}, 56 | {NULL, NULL}}; 57 | 58 | static const struct luaL_Reg clutch_db_methods[] = { 59 | {"close", db_close}, {"prepare", db_prepare}, {"query", db_query}, {"queryall", db_query_all}, {"queryone", db_query_one}, {"transaction", db_transaction}, {"update", db_update}, {"__gc", db_close}, {"__tostring", db_tostring}, {NULL, NULL}}; 60 | 61 | static const struct luaL_Reg clutch_stmt_methods[] = { 62 | {"query", prep_stmt_iter}, 63 | {"queryall", prep_stmt_all}, 64 | {"queryone", prep_stmt_one}, 65 | {"update", prep_stmt_update}, 66 | {"__gc", prep_stmt_close}, 67 | {"__tostring", prep_stmt_tostring}, 68 | {NULL, NULL}}; 69 | 70 | int luaopen_clutch(lua_State *L) 71 | { 72 | init_db_metatable(L); 73 | init_statement_metatable(L); 74 | 75 | luaL_newlib(L, clutch_funcs); 76 | return 1; 77 | } 78 | 79 | static void init_db_metatable(lua_State *L) 80 | { 81 | luaL_newmetatable(L, DB_REGISTRY_KEY); 82 | 83 | lua_pushvalue(L, -1); 84 | lua_setfield(L, -2, "__index"); 85 | 86 | luaL_setfuncs(L, clutch_db_methods, 0); 87 | } 88 | 89 | static void init_statement_metatable(lua_State *L) 90 | { 91 | luaL_newmetatable(L, STMT_REGISTRY_KEY); 92 | 93 | lua_pushvalue(L, -1); 94 | lua_setfield(L, -2, "__index"); 95 | 96 | luaL_setfuncs(L, clutch_stmt_methods, 0); 97 | } 98 | 99 | static int clutch_open(lua_State *L) 100 | { 101 | const char *filename = luaL_checkstring(L, 1); 102 | 103 | sqlite3 **db = (sqlite3 **)lua_newuserdata(L, sizeof(sqlite3 *)); 104 | *db = NULL; 105 | 106 | luaL_getmetatable(L, DB_REGISTRY_KEY); 107 | lua_setmetatable(L, -2); 108 | 109 | if (sqlite3_open(filename, db) != SQLITE_OK) 110 | { 111 | lua_pushfstring(L, "%s: %s", filename, sqlite3_errmsg(*db)); 112 | close_sqlite(db); 113 | return lua_error(L); 114 | } 115 | return 1; 116 | } 117 | 118 | static int db_close(lua_State *L) 119 | { 120 | close_sqlite((sqlite3 **)luaL_checkudata(L, 1, DB_REGISTRY_KEY)); 121 | return 0; 122 | } 123 | 124 | static int db_prepare(lua_State *L) 125 | { 126 | prepare_stmt(L, *(sqlite3 **)luaL_checkudata(L, 1, DB_REGISTRY_KEY)); 127 | return 1; 128 | } 129 | 130 | static int db_query_all(lua_State *L) { return step_all(L, prepare_query(L)); } 131 | 132 | static int db_query_one(lua_State *L) { return step_one(L, prepare_query(L)); } 133 | 134 | static int db_query(lua_State *L) 135 | { 136 | prepare_query(L); 137 | lua_pushcclosure(L, iter, 1); 138 | return 1; 139 | } 140 | 141 | static int db_tostring(lua_State *L) 142 | { 143 | sqlite3 **db = (sqlite3 **)luaL_checkudata(L, 1, DB_REGISTRY_KEY); 144 | const char *name = sqlite3_db_filename(*db, "main"); 145 | lua_pushfstring(L, "sqlite3: %s", name); 146 | return 1; 147 | } 148 | 149 | static int db_transaction(lua_State *L) 150 | { 151 | sqlite3 *db = *(sqlite3 **)luaL_checkudata(L, 1, DB_REGISTRY_KEY); 152 | luaL_argcheck(L, lua_type(L, 2) == LUA_TFUNCTION, 2, 153 | "argument 2 is not a function"); 154 | 155 | int status = sqlite3_exec(db, "SAVEPOINT clutch_savepoint", NULL, NULL, NULL); 156 | if (status != SQLITE_OK) 157 | return luaL_error(L, "%s", sqlite3_errmsg(db)); 158 | 159 | lua_settop(L, 2); 160 | lua_insert(L, -2); 161 | status = lua_pcall(L, 1, LUA_MULTRET, 0); 162 | 163 | if (status == LUA_OK) 164 | sqlite3_exec(db, "RELEASE clutch_savepoint", NULL, NULL, NULL); 165 | else 166 | sqlite3_exec(db, "ROLLBACK TO clutch_savepoint", NULL, NULL, NULL); 167 | 168 | lua_pushboolean(L, status == LUA_OK); 169 | 170 | lua_insert(L, 1); 171 | return lua_gettop(L); 172 | } 173 | 174 | static int db_update(lua_State *L) { return update(L, prepare_query(L)); } 175 | 176 | static int prep_stmt_all(lua_State *L) { return step_all(L, rebind_stmt(L)); } 177 | 178 | static int prep_stmt_close(lua_State *L) 179 | { 180 | close_sqlite_stmt((sqlite3_stmt **)luaL_checkudata(L, 1, STMT_REGISTRY_KEY)); 181 | return 0; 182 | } 183 | 184 | static int prep_stmt_iter(lua_State *L) 185 | { 186 | rebind_stmt(L); 187 | lua_pushcclosure(L, iter, 1); 188 | return 1; 189 | } 190 | 191 | static int prep_stmt_one(lua_State *L) { return step_one(L, rebind_stmt(L)); } 192 | 193 | static int prep_stmt_tostring(lua_State *L) 194 | { 195 | sqlite3_stmt *stmt = *(sqlite3_stmt **)luaL_checkudata(L, 1, STMT_REGISTRY_KEY); 196 | lua_pushstring(L, sqlite3_sql(stmt)); 197 | return 1; 198 | } 199 | 200 | static int prep_stmt_update(lua_State *L) { return update(L, rebind_stmt(L)); } 201 | 202 | static sqlite3_stmt *rebind_stmt(lua_State *L) 203 | { 204 | sqlite3_stmt *stmt = *(sqlite3_stmt **)luaL_checkudata(L, 1, STMT_REGISTRY_KEY); 205 | sqlite3_reset(stmt); 206 | bind_stmt(L, stmt, 1); 207 | return stmt; 208 | } 209 | 210 | static sqlite3_stmt *prepare_query(lua_State *L) 211 | { 212 | sqlite3 *db = *(sqlite3 **)luaL_checkudata(L, 1, DB_REGISTRY_KEY); 213 | sqlite3_stmt *stmt = prepare_stmt(L, db); 214 | 215 | int status = bind_stmt(L, stmt, 3); 216 | if (status != SQLITE_OK) 217 | luaL_error(L, "%s", sqlite3_errmsg(db)); 218 | 219 | return stmt; 220 | } 221 | 222 | static sqlite3_stmt *prepare_stmt(lua_State *L, sqlite3 *db) 223 | { 224 | const char *sql = luaL_checkstring(L, 2); 225 | 226 | sqlite3_stmt **stmt = 227 | (sqlite3_stmt **)lua_newuserdata(L, sizeof(sqlite3_stmt **)); 228 | *stmt = NULL; 229 | 230 | luaL_getmetatable(L, STMT_REGISTRY_KEY); 231 | lua_setmetatable(L, -2); 232 | 233 | lua_insert(L, 3); 234 | 235 | int status = sqlite3_prepare_v2(db, sql, strlen(sql), stmt, NULL); 236 | if (status != SQLITE_OK) 237 | luaL_error(L, "%s", sqlite3_errmsg(db)); 238 | 239 | return *stmt; 240 | } 241 | 242 | static int bind_stmt(lua_State *L, sqlite3_stmt *stmt, int nargs) 243 | { 244 | int top = lua_gettop(L); 245 | if (top < nargs + 1) 246 | return bind_lua_vars(L, stmt); 247 | else if (lua_istable(L, nargs + 1)) 248 | return bind_params(L, stmt); 249 | else 250 | return bind_varargs(L, top - nargs, stmt); 251 | } 252 | 253 | static int bind_params(lua_State *L, sqlite3_stmt *stmt) 254 | { 255 | int count = sqlite3_bind_parameter_count(stmt); 256 | int status = SQLITE_OK; 257 | 258 | for (int i = 1; i <= count; ++i) 259 | { 260 | const char *name = sqlite3_bind_parameter_name(stmt, i); 261 | if (!name || name[0] == '?') 262 | { 263 | #if LUA_VERSION_NUM >= 503 264 | lua_geti(L, -1, i); 265 | #else 266 | lua_pushinteger(L, i); 267 | lua_gettable(L, -2); 268 | #endif 269 | } 270 | else 271 | { 272 | lua_getfield(L, -1, name + 1); 273 | } 274 | status = bind_one_param(L, stmt, i); 275 | if (status != SQLITE_OK) 276 | break; 277 | } 278 | lua_pop(L, 1); 279 | return status; 280 | } 281 | 282 | static int bind_one_param(lua_State *L, sqlite3_stmt *stmt, int index) 283 | { 284 | int status = SQLITE_OK; 285 | 286 | switch (lua_type(L, -1)) 287 | { 288 | case LUA_TBOOLEAN: 289 | status = sqlite3_bind_int64(stmt, index, lua_toboolean(L, -1)); 290 | break; 291 | case LUA_TNUMBER: 292 | status = bind_number(L, stmt, index); 293 | break; 294 | case LUA_TSTRING: 295 | status = bind_string(L, stmt, index); 296 | break; 297 | case LUA_TNIL: 298 | status = sqlite3_bind_null(stmt, index); 299 | break; 300 | default: 301 | return luaL_error(L, "unsupported lua type '%s' at position %d", 302 | lua_typename(L, lua_type(L, -1)), index); 303 | } 304 | 305 | lua_pop(L, 1); 306 | return status; 307 | } 308 | 309 | static int bind_number(lua_State *L, sqlite3_stmt *stmt, int index) 310 | { 311 | #if LUA_VERSION_NUM >= 503 312 | if (lua_isinteger(L, -1)) 313 | return sqlite3_bind_int64(stmt, index, lua_tointeger(L, -1)); 314 | #endif 315 | return sqlite3_bind_double(stmt, index, lua_tonumber(L, -1)); 316 | } 317 | 318 | static int bind_string(lua_State *L, sqlite3_stmt *stmt, int index) 319 | { 320 | size_t len; 321 | const char *text = lua_tolstring(L, -1, &len); 322 | return sqlite3_bind_text(stmt, index, text, len, SQLITE_TRANSIENT); 323 | } 324 | 325 | static int bind_varargs(lua_State *L, int nparams, sqlite3_stmt *stmt) 326 | { 327 | int count = sqlite3_bind_parameter_count(stmt); 328 | 329 | lua_settop(L, lua_gettop(L) + (count - nparams)); 330 | 331 | int status = SQLITE_OK; 332 | while (count > 0) 333 | { 334 | status = bind_one_param(L, stmt, count--); 335 | if (status != SQLITE_OK) 336 | break; 337 | } 338 | return status; 339 | } 340 | 341 | static int bind_lua_vars(lua_State *L, sqlite3_stmt *stmt) 342 | { 343 | int count = sqlite3_bind_parameter_count(stmt); 344 | int status = SQLITE_OK; 345 | 346 | for (int i = 1; i <= count; ++i) 347 | { 348 | const char *name = sqlite3_bind_parameter_name(stmt, i); 349 | if (!name || !is_named_parameter(name)) 350 | return luaL_error(L, "anonymous and numbered parameters not supported"); 351 | 352 | find_var(L, name + 1); 353 | status = bind_one_param(L, stmt, i); 354 | if (status != SQLITE_OK) 355 | break; 356 | } 357 | return status; 358 | } 359 | 360 | static int is_named_parameter(const char *name) 361 | { 362 | return name[0] == ':' || name[0] == '@' || name[0] == '$'; 363 | } 364 | 365 | static void find_var(lua_State *L, const char *name) 366 | { 367 | lua_Debug debug; 368 | for (int level = 1; lua_getstack(L, level, &debug); ++level) 369 | { 370 | int index = 1; 371 | const char *lname; 372 | while ((lname = lua_getlocal(L, &debug, index++))) 373 | { 374 | if (!strcmp(name, lname)) 375 | return; 376 | lua_pop(L, 1); 377 | } 378 | } 379 | lua_getglobal(L, name); 380 | } 381 | 382 | static int iter(lua_State *L) 383 | { 384 | sqlite3_stmt *stmt = *(sqlite3_stmt **)lua_touserdata(L, lua_upvalueindex(1)); 385 | return step(L, stmt); 386 | } 387 | 388 | static int step_one(lua_State *L, sqlite3_stmt *stmt) 389 | { 390 | if (step(L, stmt) == 0) 391 | luaL_error(L, "no results"); 392 | if (step(L, stmt) != 0) 393 | luaL_error(L, "too many results"); 394 | return 1; 395 | } 396 | 397 | static int step_all(lua_State *L, sqlite3_stmt *stmt) 398 | { 399 | lua_newtable(L); 400 | for (int i = 1; step(L, stmt); ++i) 401 | lua_rawseti(L, -2, i); 402 | return 1; 403 | } 404 | 405 | static int step(lua_State *L, sqlite3_stmt *stmt) 406 | { 407 | int status = sqlite3_step(stmt); 408 | if (status == SQLITE_DONE) 409 | return 0; 410 | else if (status != SQLITE_ROW) 411 | luaL_error(L, "step: %s", sqlite3_errstr(status)); 412 | 413 | handle_row(L, stmt); 414 | return 1; 415 | } 416 | 417 | static void handle_row(lua_State *L, sqlite3_stmt *stmt) 418 | { 419 | int count = sqlite3_data_count(stmt); 420 | 421 | lua_createtable(L, 0, count); 422 | for (int i = 0; i < count; ++i) 423 | { 424 | lua_pushstring(L, sqlite3_column_name(stmt, i)); 425 | switch (sqlite3_column_type(stmt, i)) 426 | { 427 | case SQLITE_INTEGER: 428 | lua_pushinteger(L, sqlite3_column_int64(stmt, i)); 429 | break; 430 | case SQLITE_FLOAT: 431 | lua_pushnumber(L, sqlite3_column_double(stmt, i)); 432 | break; 433 | case SQLITE_TEXT: 434 | case SQLITE_BLOB: 435 | lua_pushlstring(L, (const char *)sqlite3_column_blob(stmt, i), 436 | sqlite3_column_bytes(stmt, i)); 437 | break; 438 | case SQLITE_NULL: 439 | default: 440 | lua_pushnil(L); 441 | break; 442 | } 443 | lua_rawset(L, -3); 444 | } 445 | } 446 | 447 | static int update(lua_State *L, sqlite3_stmt *stmt) 448 | { 449 | sqlite3 *db = sqlite3_db_handle(stmt); 450 | 451 | int status = sqlite3_step(stmt); 452 | if (status != SQLITE_DONE) 453 | return luaL_error(L, "%s", sqlite3_errmsg(db)); 454 | 455 | lua_pushinteger(L, sqlite3_changes(db)); 456 | return 1; 457 | } 458 | 459 | static void close_sqlite(sqlite3 **db) 460 | { 461 | if (*db) 462 | { 463 | sqlite3_close_v2(*db); 464 | *db = NULL; 465 | } 466 | } 467 | 468 | static void close_sqlite_stmt(sqlite3_stmt **stmt) 469 | { 470 | if (*stmt) 471 | { 472 | sqlite3_finalize(*stmt); 473 | *stmt = NULL; 474 | } 475 | } 476 | -------------------------------------------------------------------------------- /test.lua: -------------------------------------------------------------------------------- 1 | local luaunit = require 'luaunit' 2 | local clutch = require 'clutch' 3 | 4 | local dbsetup = { 5 | [[ 6 | CREATE TABLE p ( 7 | pnum INTEGER NOT NULL PRIMARY KEY, 8 | pname TEXT NOT NULL, 9 | color TEXT NOT NULL, 10 | weight REAL NOT NULL, 11 | city TEXT NOT NULL, 12 | UNIQUE (pname, color, city) 13 | ) 14 | ]], 15 | "INSERT INTO p VALUES (1, 'Nut', 'Red', 12, 'London')", 16 | "INSERT INTO p VALUES (2, 'Bolt', 'Green', 17, 'Paris')", 17 | "INSERT INTO p VALUES (3, 'Screw', 'Blue', 17, 'Oslo')", 18 | "INSERT INTO p VALUES (4, 'Screw', 'Red', 14, 'London')", 19 | "INSERT INTO p VALUES (5, 'Cam', 'Blue', 12, 'Paris')", 20 | "INSERT INTO p VALUES (6, 'Cog', 'Red', 19, 'London')", 21 | } 22 | 23 | TestClutch = {} 24 | 25 | function TestClutch:setup() 26 | self.db = clutch.open("") 27 | for _, sql in ipairs(dbsetup) do 28 | self.db:update(sql) 29 | end 30 | end 31 | 32 | function TestClutch:teardown() 33 | self.db:close() 34 | end 35 | 36 | function TestClutch:testSimpleQueryReturnsCorrectNumberOfRows() 37 | assertResultCount(self.db:query('select * from p'), 6) 38 | end 39 | 40 | function TestClutch:testResultsAreBoundToCorrectKeys() 41 | assertSingleResult( 42 | self.db:query('select * from p where pnum = 1'), 43 | {pnum = 1, pname = 'Nut', color = 'Red', weight = 12.0, city = 'London'}) 44 | end 45 | 46 | function TestClutch:testAnonymousParameterBinding() 47 | assertSingleResult( 48 | self.db:query('select pname from p where pnum = ?', {1}), 49 | {pname = 'Nut'}) 50 | end 51 | 52 | function TestClutch:testAnonymousParameterBindingWithMultipleParameters() 53 | assertResultCount( 54 | self.db:query('select * from p where pnum > ? and pnum < ?', {1, 4}), 55 | 2) 56 | end 57 | 58 | function TestClutch:testAnonymousParameterBindingWithVarargs() 59 | assertResultCount( 60 | self.db:query('select * from p where pnum > ? and pnum < ?', 1, 4), 61 | 2) 62 | end 63 | 64 | function TestClutch:testVarargBindingIgnoresExtraArguments() 65 | assertResultCount( 66 | self.db:query('select * from p where pnum > ? and pnum < ?', 1, 4, 2, 3), 67 | 2) 68 | end 69 | 70 | function TestClutch:testPositionalParameterBinding() 71 | assertSingleResult( 72 | self.db:query('select pname from p where pnum = ?1', {1}), 73 | {pname = 'Nut'}) 74 | end 75 | 76 | function TestClutch:testPositionalParameterBindingWithMultipleParameters() 77 | assertResultCount( 78 | self.db:query('select * from p where pnum > ?2 and pnum < ?1', {4, 1}), 79 | 2) 80 | end 81 | 82 | function TestClutch:testNamedParameterBinding() 83 | assertSingleResult( 84 | self.db:query('select pname from p where pnum = :pnum', {pnum = 1}), 85 | {pname = 'Nut'}) 86 | end 87 | 88 | function TestClutch:testNamedParameterBindingWithAt() 89 | assertSingleResult( 90 | self.db:query('select pname from p where pnum = @pnum', {pnum = 1}), 91 | {pname = 'Nut'}) 92 | end 93 | 94 | function TestClutch:testNamedParameterBindingWithDollar() 95 | assertSingleResult( 96 | self.db:query('select pname from p where pnum = $pnum', {pnum = 1}), 97 | {pname = 'Nut'}) 98 | end 99 | 100 | function TestClutch:testNamedParameterBindingWithMultipleParameters() 101 | assertSingleResult( 102 | self.db:query('select count(1) from p where pnum > :min and pnum < :max', {min = 1, max = 4}), 103 | {count = 2}) 104 | end 105 | 106 | function TestClutch:testInterpolatedParameterBindingWithLocal() 107 | local pnum = 1 108 | assertSingleResult( 109 | self.db:query('select pname from p where pnum = $pnum'), 110 | {pname = 'Nut'}) 111 | end 112 | 113 | function TestClutch:testInterpolatedParameterBindingWithArgument() 114 | local f = function(pnum) 115 | assertSingleResult( 116 | self.db:query('select pname from p where pnum = $pnum'), 117 | {pname = 'Nut'}) 118 | end 119 | f(1) 120 | end 121 | 122 | function TestClutch:testInterpolatedParameterBindingWithClosure() 123 | local pnum = 1 124 | local f = function() 125 | assertSingleResult( 126 | self.db:query('select pname from p where pnum = $pnum'), 127 | {pname = 'Nut'}) 128 | end 129 | f() 130 | end 131 | 132 | function TestClutch:testInterpolatedParameterBindingWithGlobal() 133 | globalNum = 1 134 | local f = function() 135 | assertSingleResult( 136 | self.db:query('select pname from p where pnum = $globalNum'), 137 | {pname = 'Nut'}) 138 | end 139 | f() 140 | end 141 | 142 | function TestClutch:testStringParameterBinding() 143 | assertSingleResult( 144 | self.db:query('select pnum from p where color = $color', {color = 'Green'}), 145 | {pnum = 2}) 146 | end 147 | 148 | function TestClutch:testDoubleParameterBinding() 149 | assertSingleResult( 150 | self.db:query('select pname from p where weight = @weight', {weight = 19.0}), 151 | {pname = 'Cog'}) 152 | end 153 | 154 | function TestClutch:testUpdateWithParameters() 155 | self.db:update('insert into p values (:pnum, :pname, :color, :weight, :city)', 156 | {pnum = 7, pname = 'Washer', color = 'Grey', weight = 5.0, city = 'Helsinki'}) 157 | assertSingleResult( 158 | self.db:query('select pname from p where pnum = 7'), 159 | { pname = 'Washer'}) 160 | end 161 | 162 | function TestClutch:testParametersAreQuotedProperlyInUpdate() 163 | self.db:update("insert into p values (:pnum, :pname, :color, :weight, :city)", 164 | {pnum = 7, pname = "'); delete from p; -- ", color = 'Grey', weight = 5.0, city = 'Helsinki'}) 165 | assertSingleResult( 166 | self.db:query('select color from p where pnum = 7'), 167 | { pname = 'Grey'}) 168 | end 169 | 170 | function TestClutch:testParametersAreQuotedProperyInQuery() 171 | assertResultCount( 172 | self.db:query('select 1 from p where pname = :pname', {pname = "' or '1'='1' -- "}), 173 | 0 174 | ) 175 | end 176 | 177 | function TestClutch:testParametersAreInterpolatedProperyInQuery() 178 | local pname = "' or '1'='1' -- " 179 | assertResultCount(self.db:query('select 1 from p where pname = :pname'), 0) 180 | end 181 | 182 | function TestClutch:testSupportsBooleanParameter() 183 | self.db:update('CREATE TABLE t (pnum INTEGER PRIMARY KEY, avail INTEGER NOT NULL)') 184 | self.db:update('insert into t values (?, ?)', 1, true) 185 | assertSingleResult( 186 | self.db:query('select avail from t where pnum = 1'), 187 | { avail = 1}) 188 | end 189 | 190 | function TestClutch:testQueryOneReturnsSingleResultAsTable() 191 | luaunit.assertItemsEquals( 192 | self.db:queryone('select pname from p where pnum = ?', 1), 193 | {pname = 'Nut'}) 194 | end 195 | 196 | function TestClutch:testQueryAllReturnsAllResultsInAnArray() 197 | local results = self.db:queryall('select pnum from p order by pnum asc') 198 | luaunit.assertEquals(#results, 6) 199 | for i = 1, 6 do 200 | luaunit.assertItemsEquals(results[i], {pnum = i}) 201 | end 202 | end 203 | 204 | function TestClutch:testQueryAllReturnsEmptyTableForNoResults() 205 | local results = self.db:queryall('select pnum from p where pnum = -1') 206 | luaunit.assertItemsEquals(results, {}) 207 | end 208 | 209 | function TestClutch:testInsertReturnsOneForNewRow() 210 | local n = self.db:update("insert into p values (7, 'Washer', 'Grey', 5, 'Helsinki')") 211 | luaunit.assertEquals(n, 1) 212 | end 213 | 214 | function TestClutch:testUpdateReturnsNumberOfModifiedRows() 215 | local n = self.db:update("update p set weight = weight + 1 where color = 'Red'") 216 | luaunit.assertEquals(n, 3) 217 | end 218 | 219 | function TestClutch:testUpdateWithNoMatchingRowsReturnsZero() 220 | local n = self.db:update("update p set weight = weight + 1 where color = 'Pink'") 221 | luaunit.assertEquals(n, 0) 222 | end 223 | 224 | function TestClutch:testDeleteReturnsNumberOfModifiedRows() 225 | local n = self.db:update("delete from p where city = 'Paris'") 226 | luaunit.assertEquals(n, 2) 227 | end 228 | 229 | function TestClutch:testDeleteWithNoMatchingRowsReturnsZero() 230 | local n = self.db:update("delete from p where city = 'Vienna'") 231 | luaunit.assertEquals(n, 0) 232 | end 233 | 234 | function TestClutch:testPreparedStatement() 235 | local stmt = self.db:prepare("select city from p where pnum = :pnum") 236 | local iter = stmt:query({pnum = 1}) 237 | luaunit.assertItemsEquals(iter(), {city = "London"}) 238 | end 239 | 240 | function TestClutch:testPreparedStatementCanBeRebound() 241 | local stmt = self.db:prepare("select pnum, city from p where pnum = :pnum") 242 | for pnum, city in ipairs({"London", "Paris"}) do 243 | local iter = stmt:query({pnum = pnum}) 244 | luaunit.assertItemsEquals(iter(), {pnum = pnum, city = city}) 245 | end 246 | end 247 | 248 | function TestClutch:testPreparedStatementIterReturnsNilAfterLastResult() 249 | local stmt = self.db:prepare("select city from p where pnum = :pnum") 250 | local iter = stmt:query({pnum = 2}) 251 | luaunit.assertItemsEquals(iter(), {city = "Paris"}) 252 | luaunit.assertNil(iter()) 253 | end 254 | 255 | function TestClutch:testPreparedStatementIterReturnsNilForNoResults() 256 | local stmt = self.db:prepare("select city from p where pnum = :pnum") 257 | local iter = stmt:query({pnum = 100}) 258 | luaunit.assertNil(iter()) 259 | end 260 | 261 | function TestClutch:testPreparedStatementIterWorksWithTableArguments() 262 | local stmt = self.db:prepare("select city from p where pnum = ?") 263 | local iter = stmt:query({3}) 264 | luaunit.assertItemsEquals(iter(), {city = "Oslo"}) 265 | end 266 | 267 | function TestClutch:testPreparedStatementIterWorksWithVarargs() 268 | local stmt = self.db:prepare("select city from p where pnum = ?") 269 | local iter = stmt:query(3) 270 | luaunit.assertItemsEquals(iter(), {city = "Oslo"}) 271 | end 272 | 273 | function TestClutch:testPreparedStatementReturnsOneResult() 274 | local stmt = self.db:prepare("select city from p where pnum = :pnum") 275 | luaunit.assertItemsEquals(stmt:queryone({pnum = 1}), {city = "London"}) 276 | end 277 | 278 | function TestClutch:testPreparedStatementOneFailsWithNoResults() 279 | local stmt = self.db:prepare("select city from p where pnum = :pnum") 280 | luaunit.assertErrorMsgContains("no results", 281 | function() stmt:queryone({pnum = 100}) end) 282 | end 283 | 284 | function TestClutch:testPreparedStatementOneFailsWithTooManyResults() 285 | local stmt = self.db:prepare("select city from p where color = :color") 286 | luaunit.assertErrorMsgContains("too many results", 287 | function() stmt:queryone({color = "Red"}) end) 288 | end 289 | 290 | function TestClutch:testPreparedStatementOneWorksWithTableArguments() 291 | local stmt = self.db:prepare("select city from p where pnum = ?") 292 | luaunit.assertItemsEquals(stmt:queryone({4}), {city = "London"}) 293 | end 294 | 295 | function TestClutch:testPreparedStatementOneWorksWithVarargs() 296 | local stmt = self.db:prepare("select city from p where pnum = ?") 297 | luaunit.assertItemsEquals(stmt:queryone(4), {city = "London"}) 298 | end 299 | 300 | function TestClutch:testPreparedStatementReturnsAllResults() 301 | local stmt = self.db:prepare("select pname from p where color = :color") 302 | local results = stmt:queryall({color = "Red"}) 303 | for i, name in ipairs({"Nut", "Screw", "Cog"}) do 304 | luaunit.assertItemsEquals(results[i], {pname = name}) 305 | end 306 | end 307 | 308 | function TestClutch:testPreparedStatementAllResultsEmptyTableForNoResults() 309 | local stmt = self.db:prepare("select pname from p where color = :color") 310 | luaunit.assertEquals(stmt:queryall({color = "Pink"}), {}) 311 | end 312 | 313 | function TestClutch:testPreparedStatementAllWorksWithTableArguments() 314 | local stmt = self.db:prepare("select city from p where pnum = ?") 315 | luaunit.assertItemsEquals(stmt:queryall({5})[1], {city = "Paris"}) 316 | end 317 | 318 | function TestClutch:testPreparedStatementAllWorksWithVarargs() 319 | local stmt = self.db:prepare("select city from p where pnum = ?") 320 | luaunit.assertItemsEquals(stmt:queryall(5)[1], {city = "Paris"}) 321 | end 322 | 323 | function TestClutch:testPreparedStatementUpdate() 324 | local stmt = self.db:prepare("insert into p values (?, ?, ?, ?, ?)") 325 | stmt:update({7, "Washer", "Grey", 5.0, "Helsinki"}) 326 | 327 | local result = self.db:queryone("select pname from p where pnum = 7") 328 | luaunit.assertEquals(result.pname, "Washer") 329 | end 330 | 331 | function TestClutch:testUpdateInTransactionSucceeds() 332 | self.db:transaction(function (t) 333 | t:update("insert into p values (7, 'Washer', 'Grey', 5, 'Helsinki')") 334 | end) 335 | luaunit.assertItemsEquals( 336 | self.db:queryone('select city from p where pnum = 7'), 337 | {city = "Helsinki"} 338 | ) 339 | end 340 | 341 | function TestClutch:testTransactionReturnsTheValuesFromTransactionFunction() 342 | local success, result = self.db:transaction(function (t) 343 | return t:update("insert into p values (7, 'Washer', 'Grey', 5, 'Helsinki')") 344 | end) 345 | luaunit.assertTrue(success) 346 | luaunit.assertEquals(result, 1) 347 | end 348 | 349 | function TestClutch:testTransactionRollsBackInCaseOfConstrainFailure() 350 | local success, result = self.db:transaction(function (t) 351 | t:update("insert into p values (7, 'Washer', 'Grey', 5, 'Helsinki')") 352 | t:update("insert into p values (7, 'Washer', 'Grey', 5, 'Helsinki')") 353 | end) 354 | luaunit.assertFalse(success) 355 | luaunit.assertStrContains(result, "UNIQUE constraint failed") 356 | luaunit.assertEquals(#self.db:queryall("select * from p where pnum = 7"), 0) 357 | end 358 | 359 | function TestClutch:testTransactionRollsBackInCaseOfLuaError() 360 | local success, result = self.db:transaction(function (t) 361 | t:update("insert into p values (7, 'Washer', 'Grey', 5, 'Helsinki')") 362 | error("Lua error") 363 | end) 364 | luaunit.assertFalse(success) 365 | luaunit.assertStrContains(result, "Lua error") 366 | luaunit.assertEquals(#self.db:queryall("select * from p where pnum = 7"), 0) 367 | end 368 | 369 | function TestClutch:testNestedTransactionWritesToDatabase() 370 | self.db:transaction(function (t) 371 | t:update("insert into p values (7, 'Washer', 'Grey', 5, 'Helsinki')") 372 | t:transaction(function (t2) 373 | t2:update("insert into p values (8, 'Washer', 'Black', 7, 'Helsinki')") 374 | end) 375 | end) 376 | luaunit.assertItemsEquals( 377 | #self.db:queryall("select city from p where city = 'Helsinki'"), 2) 378 | end 379 | 380 | function TestClutch:testErrorInNestedTransactionRollsBackOnlyInnerTransaction() 381 | local success, result = self.db:transaction(function (t) 382 | t:update("insert into p values (7, 'Washer', 'Grey', 5, 'Helsinki')") 383 | return t:transaction(function (t2) 384 | t2:update("insert into p values (8, 'Washer', 'Black', 7, 'Helsinki')") 385 | error("Inner transaction") 386 | end) 387 | end) 388 | luaunit.assertTrue(success) 389 | luaunit.assertItemsEquals( 390 | #self.db:queryall("select city from p where city = 'Helsinki'"), 1) 391 | end 392 | 393 | function TestClutch:testErrorInOuterTransactionRollsBackAlsoInnerTransaction() 394 | local success, result = self.db:transaction(function (t) 395 | t:transaction(function (t2) 396 | t2:update("insert into p values (8, 'Washer', 'Black', 7, 'Helsinki')") 397 | end) 398 | return t:update("insert into p values (8, 'Washer', 'Grey', 5, 'Helsinki')") 399 | end) 400 | luaunit.assertFalse(success) 401 | luaunit.assertStrContains(result, "UNIQUE constraint failed") 402 | luaunit.assertItemsEquals( 403 | #self.db:queryall("select city from p where city = 'Helsinki'"), 0) 404 | end 405 | 406 | function TestClutch:testQueryOneReportsErrorWithTooManyResults() 407 | luaunit.assertErrorMsgContains( 408 | "too many results", 409 | function() self.db:queryone('select * from p') end) 410 | end 411 | 412 | function TestClutch:testQueryOneReportsErrorWithZeroResults() 413 | luaunit.assertErrorMsgContains( 414 | "no results", 415 | function() self.db:queryone('select * from p where pnum = -1') end) 416 | end 417 | 418 | function TestClutch:testCanUseNilAsAnonymousParameter() 419 | luaunit.assertErrorMsgContains("NOT NULL constraint failed: p.city", function () 420 | self.db:update("insert into p values (7, 'Washer', 'Grey', 5, ?)", nil) 421 | end) 422 | end 423 | 424 | function TestClutch:testCanUseNilAsNamedParameter() 425 | luaunit.assertErrorMsgContains("NOT NULL constraint failed: p.city", function () 426 | self.db:update("insert into p values (7, 'Washer', 'Grey', 5, :city)", {city = nil}) 427 | end) 428 | end 429 | 430 | function TestClutch:testMissingAnonymousParametersAreTreatedAsNil() 431 | luaunit.assertErrorMsgContains("NOT NULL constraint failed: p.city", function (city) 432 | self.db:update("insert into p values (7, 'Washer', 'Grey', ?, ?)", 5.0) 433 | end, nil) 434 | end 435 | 436 | function TestClutch:testMissingInterpolatedVariableIsTreatedAsNil() 437 | luaunit.assertErrorMsgContains("NOT NULL constraint failed: p.city", function () 438 | self.db:update("insert into p values (7, 'Washer', 'Grey', 5, :city)") 439 | end) 440 | end 441 | 442 | function TestClutch:testSQLIntegrityViolationIsReportedAsError() 443 | luaunit.assertErrorMsgContains("UNIQUE constraint failed", function () 444 | self.db:update('insert into p values (:pnum, :pname, :color, :weight, :city)', 445 | {pnum = 1, pname = 'Washer', color = 'Grey', weight = 5.0, city = 'Helsinki'}) 446 | end) 447 | end 448 | 449 | function TestClutch:testSQLSyntaxErrorIsReportedAsError() 450 | luaunit.assertErrorMsgContains("syntax error", function () 451 | self.db:query('insert values') 452 | end) 453 | end 454 | 455 | function assertResultCount(iter, count) 456 | local i = 0 457 | for _ in iter do 458 | i = i + 1 459 | end 460 | luaunit.assertEquals(i, count) 461 | end 462 | 463 | function assertSingleResult(iter, expected) 464 | luaunit.assertItemsEquals(iter(), expected) 465 | end 466 | 467 | os.exit(luaunit.LuaUnit.run()) 468 | --------------------------------------------------------------------------------