├── .gitignore ├── LICENSE ├── README.md ├── docs └── images │ ├── AfterDelete.png │ ├── AfterExecute.png │ ├── AfterExecuteFile.png │ ├── AfterInsert.png │ ├── AfterInsertFile.png │ ├── AfterReplace.png │ ├── AfterSelect.png │ ├── AfterUpdate.png │ ├── BeginTransaction.png │ ├── CloseDatabase.png │ ├── CommitTransation.png │ ├── DatabaseClosed.png │ ├── DatabaseCreated.png │ ├── DatabaseDowngrade.png │ ├── DatabaseExists.png │ ├── DatabaseOpened.png │ ├── DatabasePath.png │ ├── DatabaseUpgrade.png │ ├── Delete.png │ ├── DeleteAsync.png │ ├── DeleteDatabase.png │ ├── Execute.png │ ├── ExecuteAsync.png │ ├── ExecuteFile.png │ ├── ExecuteFileAsync.png │ ├── ExportDatabase.png │ ├── GetDBName.png │ ├── GetDBVersion.png │ ├── GetDebugDialog.png │ ├── GetDebugToast.png │ ├── GetReturnColumnNames.png │ ├── ImportDatabase.png │ ├── Insert.png │ ├── InsertAsync.png │ ├── InsertFile.png │ ├── InsertFileAsync.png │ ├── IsDatabaseOpen.png │ ├── OpenDatabase.png │ ├── Replace.png │ ├── ReplaceAsync.png │ ├── RollbackTransaction.png │ ├── SQLError.png │ ├── Select.png │ ├── SelectAsync.png │ ├── SelectSQL.png │ ├── SelectSQLAsync.png │ ├── SetDBName.png │ ├── SetDBVersion.png │ ├── SetDebugDialog.png │ ├── SetDebugToast.png │ ├── SetReturnColumnNames.png │ ├── TableCount.png │ ├── TableExists.png │ ├── TableNames.png │ ├── TableRowCount.png │ ├── Update.png │ └── UpdateAsync.png ├── org.bennedum.SQLite.aix ├── src ├── SQLite.java └── aiwebres │ └── small-icon.png └── test ├── README.md ├── SQLiteTest.aia ├── exec.sql ├── insert.csv └── monthNames.txt /.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frdfsnlght/aix-SQLite/63ad1bb80a23592c15e2e46a0324d9c19513e51d/.gitignore -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Thomas Bennedum 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SQLite 2 | 3 | [App Inventor](http://appinventor.mit.edu/), 4 | [Thunkable](https://thunkable.com/), 5 | [AppyBuilder](http://appybuilder.com/), 6 | [Makeroid](https://www.makeroid.io/), 7 | [Blockly Studio](https://www.blocklystudio.xyz), 8 | etc. extension for [SQLite](https://www.sqlite.org/) 9 | 10 | [SQLite](https://www.sqlite.org/) is a small, fast, self-contained SQL (Structured Query Language) database engine 11 | built into Android. 12 | [SQL](https://www.w3schools.com/sql/) statements are used to create, select, update, and delete data in one or 13 | more tables. SQL allows for complex relationships between tables and provides an expressive means to 14 | find data stored in a database. 15 | 16 | ## Contents 17 | 18 | * [Features](#features) 19 | * [Download](#download) 20 | * [Donate](#donate) 21 | * [Background](#background) 22 | * [Properties](#properties) 23 | * [Events](#events) 24 | * [Methods](#methods) 25 | * [General](#general) 26 | * [Transactions](#transactions) 27 | * [Data Manipulation](#data-manipulation) 28 | * [Bind Parameters](#bind-parameters) 29 | * [Samples](#samples) 30 | 31 | ## Features 32 | 33 | * Control over database name 34 | * Import/export entire database 35 | * Convenience methods for common CRUD operations 36 | * In-line and asynchronous versions of data manipulation methods 37 | * Can use parameterized SQL statements (prevents SQL injection) 38 | * Query results returned as proper lists 39 | * Database life cycle events 40 | * Debug messages as Toast messages and/or dialogs 41 | * Begin, commit, and rollback transactions (nested too!) 42 | * Database versioning 43 | * and more! 44 | 45 | If you don't see a feature you'd like or need, tell me about it. I can't promise I'll add it but you never know! 46 | 47 | ## Download 48 | 49 | [Download AIX](https://github.com/frdfsnlght/aix-SQLite/raw/master/org.bennedum.SQLite.aix) 50 | 51 | ## Installation 52 | 53 | Installation may depend on which App Inventor flavor you're using, but I think they're all pretty similar. 54 | 55 | 1. Open the "Designer" tab of your application. 56 | 2. At the bottom of the Palette on the left side, open the "Extensions" category. 57 | 3. Click the "Import extension" link. 58 | 4. Select the aix file you downloaded above and click the "Import" button. 59 | 5. Wait a few seconds and you should see a message saying the extension was imported. You should also see a SQLite 60 | extension appear in the "Extensions" category. 61 | 6. Drag the SQLite extension into your app and carry on! 62 | 63 | ## Donate 64 | 65 | If you find this extension useful, please consider donating by clicking the button below. 66 | If you're using this extension in an app you're making money from, please STRONGLY consider donating even more. 67 | The recommended donation is $10 USD, but I'll accept anything you think the extension is worth to you. 68 | 69 | [![PayPal](https://www.paypalobjects.com/webstatic/en_US/i/buttons/pp-acceptance-medium.png)](https://paypal.me/frdfsnlght/10) 70 | 71 | ## Background 72 | 73 | Before writing this extension, I'd never done any Android programming. I came upon a need 74 | for a small app that would be a perfect learning experience. Starting small, I came across 75 | MIT's App Inventor and all the variations of that platform. After doing some research, I came 76 | to the conclusion that would be a nice way to get my feet wet. 77 | 78 | Without going into details, my small app would benefit greatly from a proper database rather than 79 | the TinyDB or other key/value stores included with App Inventor and it's fellows. Since I have a 80 | deep programming background, SQL doesn't bother me and I was already familiar with SQLite. 81 | I was pleasantly surprised to find SQLite is part of the Android platform. 82 | 83 | During my research into App Inventor I discovered the ability to create extensions and all the 84 | various free and paid extensions available. There are already a handful of SQLite extensions 85 | available for the App Inventor platform but the various options didn't meet my needs. 86 | I took the opportunity to learn how to build an App Inventor extension in addition to building my first Android app. 87 | 88 | ## Properties 89 | 90 | ![Image](https://github.com/frdfsnlght/aix-SQLite/raw/master/docs/images/GetDBName.png) ![Image](https://github.com/frdfsnlght/aix-SQLite/raw/master/docs/images/SetDBName.png) 91 | 92 | This property defines the name of the database file. It's default value is "db.sqlite". Changing the name after 93 | opening a database has no effect on the database. Use this property to change the name before the database 94 | is opened. 95 | 96 | ![Image](https://github.com/frdfsnlght/aix-SQLite/raw/master/docs/images/GetDBVersion.png) ![Image](https://github.com/frdfsnlght/aix-SQLite/raw/master/docs/images/SetDBVersion.png) 97 | 98 | This is the "version" of the database. An app will define what version of the database it's compatible with. 99 | When a database is opened and its version doesn't match the this version, either the "DatabaseUpgrade" or 100 | "DatabaseDowngrade" event will be fired to allow you to modify the database to make it compatible. 101 | 102 | ![Image](https://github.com/frdfsnlght/aix-SQLite/raw/master/docs/images/GetReturnColumnNames.png) ![Image](https://github.com/frdfsnlght/aix-SQLite/raw/master/docs/images/SetReturnColumnNames.png) 103 | 104 | This setting effects how result rows are returned by the various Select methods. When this property is false, 105 | each element of the list returned by the Select methods will be a simple list of values which represent the 106 | values of the columns selected for each matched row. When this property is true, each element of the 107 | list returned by the Select methods will be a list of pairs which represent the 108 | name and values of the columns selected for each matched row. 109 | 110 | ![Image](https://github.com/frdfsnlght/aix-SQLite/raw/master/docs/images/GetDebugToast.png) ![Image](https://github.com/frdfsnlght/aix-SQLite/raw/master/docs/images/SetDebugToast.png) 111 | 112 | This property turns on or off simple debug messages. These messages are displayed as Toast messages and disappear 113 | after a couple of seconds. Turn this on while debugging your application to see what the database is doing. 114 | 115 | ![Image](https://github.com/frdfsnlght/aix-SQLite/raw/master/docs/images/GetDebugDialog.png) ![Image](https://github.com/frdfsnlght/aix-SQLite/raw/master/docs/images/SetDebugDialog.png) 116 | 117 | This property turns on or off simple debug messages. These messages are displayed as dialogs with an OK button. 118 | The messages don't disappear until the dialog is dismissed. Turn this on while debugging your application to see what 119 | the database is doing. 120 | 121 | *NOTE:* It has been demonstrated that dialogs may be displayed in an order contrary to the actual 122 | order of the operations indicated by the dialogs. For this reason, it's recommended that the DebugToast 123 | be the preferred method used for debugging. 124 | 125 | ## Events 126 | 127 | The events in this section don't include events related to asynchronous methods, which are 128 | included as part of the description for each of those methods. 129 | 130 | ![Image](https://github.com/frdfsnlght/aix-SQLite/raw/master/docs/images/DatabaseOpened.png) 131 | 132 | This event fires after a database has been opened, and possibly created. 133 | 134 | ![Image](https://github.com/frdfsnlght/aix-SQLite/raw/master/docs/images/DatabaseCreated.png) 135 | 136 | This event fires after a database has been created because the file didn't exist when it 137 | was opened. This event fires before the DatabaseOpen event. 138 | 139 | *NOTE:* There is a case when this event will fire event if the database file exists. See the NOTE 140 | in the description of the ImportDatabase method. 141 | 142 | ![Image](https://github.com/frdfsnlght/aix-SQLite/raw/master/docs/images/DatabaseUpgrade.png) 143 | 144 | This event fires when the DBVersion property value is greater than the existing database version 145 | when it's opened. After the event finishes, the database version is set to match the DBVersion 146 | property. 147 | 148 | Use this method to make whatever changes to your database are necessary to make it match the 149 | version your application expects. 150 | 151 | ![Image](https://github.com/frdfsnlght/aix-SQLite/raw/master/docs/images/DatabaseDowngrade.png) 152 | 153 | This event fires when the DBVersion property value is less than the existing database version 154 | when it's opened. After the event finishes, the database version is set to match the DBVersion 155 | property. This event will usually never be used except for the rare times when a user 156 | downgrades to an older version of your app. 157 | 158 | Use this method to make whatever changes to your database are necessary to make it match the 159 | version your application expects. 160 | 161 | ![Image](https://github.com/frdfsnlght/aix-SQLite/raw/master/docs/images/DatabaseClosed.png) 162 | 163 | This event fires when the database is closed. 164 | 165 | ![Image](https://github.com/frdfsnlght/aix-SQLite/raw/master/docs/images/SQLError.png) 166 | 167 | This event fires whenever there is a SQL error. This usually happens when you do something 168 | wrong like trying to select from a table or column that doesn't exist, or commit a transaction 169 | when you haven't started one. 170 | 171 | ## Methods 172 | 173 | ### General 174 | 175 | ![Image](https://github.com/frdfsnlght/aix-SQLite/raw/master/docs/images/DatabasePath.png) 176 | 177 | Returns the path to the database file, whether or not it exists. 178 | 179 | ![Image](https://github.com/frdfsnlght/aix-SQLite/raw/master/docs/images/ExportDatabase.png) 180 | 181 | Makes a byte-for-byte copy of an unopened database file to the specified file. 182 | A prefix of "/" specifies a file on the external SD card. 183 | No prefix specifies a path relative to the app's private storage. 184 | 185 | ![Image](https://github.com/frdfsnlght/aix-SQLite/raw/master/docs/images/ImportDatabase.png) 186 | 187 | Makes a byte-for-byte copy of a specified SQLite database file to the file named by the 188 | DBName property. The imported file must be a fully formed binary SQLite database file, *NOT* 189 | raw SQL statements (use the ExecuteFile method for that). 190 | A prefix of "//" specifies a file in the app's assets. 191 | A prefix of "/" specifies a file on the external SD card. 192 | No prefix specifies a path relative to the app's private storage. 193 | 194 | This method is useful for initializing a database on an app's first run. Simply upload a fully 195 | formed SQLite database file into your app's assets and this function will copy it into place. 196 | 197 | *NOTE:* If you import a database that does not already contain a fully formed "android_metadata" 198 | table, the DatabaseCreated event will fire when the database is first opened. The "android_metadata" 199 | table is where Android keeps track of the database version, so when it doesn't exist, it 200 | believes the database is new, even if it already contains other tables. Any other existing tables 201 | will not be modified. 202 | 203 | ![Image](https://github.com/frdfsnlght/aix-SQLite/raw/master/docs/images/DeleteDatabase.png) 204 | 205 | Deletes the unopened database file named by the DBName property. Use this method to completely 206 | destroy the database and start over. 207 | 208 | ![Image](https://github.com/frdfsnlght/aix-SQLite/raw/master/docs/images/DatabaseExists.png) 209 | 210 | Returns true if the database file named by the DBName property exists, false otherwise. 211 | 212 | ![Image](https://github.com/frdfsnlght/aix-SQLite/raw/master/docs/images/OpenDatabase.png) 213 | 214 | Opens the database named by the DBName property. If the file doesn't exist, it will be 215 | created and the DatabaseCreated event will be fired. 216 | If the file has a version different than the DBVersion property, either the 217 | DatabaseUpgrade or DatabaseDowngrade events will be fired. 218 | After any of these events are fired, the DatabaseOpened event fill fire last. 219 | Opening an already open database has no effect. 220 | 221 | ![Image](https://github.com/frdfsnlght/aix-SQLite/raw/master/docs/images/CloseDatabase.png) 222 | 223 | Closes a previously opened database, rolling back any uncommitted transactions, 224 | and fires the DatabaseClosed event. Closing an already closed database has no effect. 225 | 226 | ![Image](https://github.com/frdfsnlght/aix-SQLite/raw/master/docs/images/IsDatabaseOpen.png) 227 | 228 | Returns true if the database is open, false otherwise. 229 | 230 | ![Image](https://github.com/frdfsnlght/aix-SQLite/raw/master/docs/images/TableCount.png) 231 | 232 | Returns the number of tables in the open database. 233 | 234 | ![Image](https://github.com/frdfsnlght/aix-SQLite/raw/master/docs/images/TableExists.png) 235 | 236 | Returns true if the named table exists in the open database, false otherwise. 237 | 238 | ![Image](https://github.com/frdfsnlght/aix-SQLite/raw/master/docs/images/TableNames.png) 239 | 240 | Returns a list of table names in the open database. 241 | 242 | ![Image](https://github.com/frdfsnlght/aix-SQLite/raw/master/docs/images/TableRowCount.png) 243 | 244 | Returns the number of rows in a table in the open database. 245 | 246 | ### Transactions 247 | 248 | Transactions allow an "all or nothing" approach to manipulating data. After a transaction has 249 | been started, all the operations that change data are "remembered" by the database until 250 | they are either "committed" or "rolled back". Committing a transaction tells the database, 251 | "I really meant to do all those things, so save the results now", while rolling back 252 | a transaction means, "forget all those things I told you to do." This, of course, is a greatly 253 | simplified description. Google around for more information. Use of transactions is optional 254 | but is an important tool for ensuring data integrity in your database. 255 | 256 | ![Image](https://github.com/frdfsnlght/aix-SQLite/raw/master/docs/images/BeginTransaction.png) 257 | 258 | Begins a transaction. Transactions can be nested (transactions inside transactions). 259 | Make sure to call CommitTransation or RollbackTransaction for each opened transaction. 260 | 261 | ![Image](https://github.com/frdfsnlght/aix-SQLite/raw/master/docs/images/CommitTransation.png) 262 | 263 | Commits (saves) changes made during the transaction to the database. 264 | 265 | ![Image](https://github.com/frdfsnlght/aix-SQLite/raw/master/docs/images/RollbackTransaction.png) 266 | 267 | Rolls back (discards) changes made during the transaction to the database. 268 | 269 | ### Data Manipulation 270 | 271 | The methods in this section deal with creating, deleting and otherwise manipulating 272 | data in the database. They all have asynchronous versions and corresponding events. Asynchronous methods 273 | perform their work on a background thread so they don't block the main UI thread. This is 274 | important for a good user experience during operations that might take more than a 100 milliseconds 275 | or so. Blocking the main UI thread will make your application appear to "freeze" and become 276 | unresponsive. Users don't like that. Use the asynchronous methods when you need to perform 277 | database operations that would take a while, like selecting, updating, deleting, inserting hundreds 278 | of rows. 279 | 280 | When an asynchronous method is available, it will accept the same arguments as the synchronous 281 | version with the addition of a "tag" parameter. The "tag" is an arbitrary string argument you provide 282 | and will be passed to the "After" event that corresponds to the asynchronous method. You can use the 283 | tag to differentiate between multiple results in the event handler. 284 | 285 | ![Image](https://github.com/frdfsnlght/aix-SQLite/raw/master/docs/images/Execute.png) ![Image](https://github.com/frdfsnlght/aix-SQLite/raw/master/docs/images/ExecuteAsync.png) ![Image](https://github.com/frdfsnlght/aix-SQLite/raw/master/docs/images/AfterExecute.png) 286 | 287 | These methods execute any arbitrary, non-SELECT SQL statement, optionally binding parameters. 288 | See the section below about [bind parameters](#bind-parameters) for more information. 289 | 290 | ![Image](https://github.com/frdfsnlght/aix-SQLite/raw/master/docs/images/ExecuteFile.png) ![Image](https://github.com/frdfsnlght/aix-SQLite/raw/master/docs/images/ExecuteFileAsync.png) ![Image](https://github.com/frdfsnlght/aix-SQLite/raw/master/docs/images/AfterExecuteFile.png) 291 | 292 | These methods execute one or more SQL statements contained in a file. The file can contain both SQL statements, 293 | blank lines, and comments. In-line comments start with "--" and end at the end of the line. Multi-line comments 294 | start with "\\*" and end with "*/". Line continuation is also supported by using "\\" as the last character in a broken 295 | line. Each statement can optionally end in a semicolon. 296 | The [literal string](https://www.scaler.com/topics/sql/string-literal/) "\n" will be replaced with an actual newline character in any SQL statement. 297 | Execution stops at the first error. The methods return the number of statements successfully executed. 298 | 299 | A file name prefix of "//" specifies a file in the app's assets. 300 | A file name prefix of "/" specifies a file on the external SD card. 301 | No prefix specifies a path relative to the app's private storage. 302 | 303 | It is recommended these methods should be used inside a transaction since they can result in 304 | partial execution in the event of an error. Typically, you want all the statements in the file to 305 | work, or none at all. 306 | 307 | ![Image](https://github.com/frdfsnlght/aix-SQLite/raw/master/docs/images/SelectSQL.png) ![Image](https://github.com/frdfsnlght/aix-SQLite/raw/master/docs/images/SelectSQLAsync.png) 308 | 309 | These methods execute a SQL SELECT statement, with optional bind parameters, that returns a list with 310 | zero or more rows of data. 311 | See the section below about [bind parameters](#bind-parameters) for more information. 312 | 313 | Although not shown here, the AfterSelect event is fired from the SelectSQLAsync method when the query is complete. 314 | 315 | The list returned by these methods has a row element list for each row matched by the query. 316 | The elements in the row element list depend on the ReturnColumnNames property. 317 | When this property is true, the elements in the row element list are themselves lists, each with two elements; 318 | a column name and a column value. When the property is false, the elements in the row element list 319 | are the column values in the same order as the requested columns in the SELECT query. 320 | 321 | ![Image](https://github.com/frdfsnlght/aix-SQLite/raw/master/docs/images/Select.png) ![Image](https://github.com/frdfsnlght/aix-SQLite/raw/master/docs/images/SelectAsync.png) ![Image](https://github.com/frdfsnlght/aix-SQLite/raw/master/docs/images/AfterSelect.png) 322 | 323 | These methods execute a SQL SELECT statement, with optional bind parameters, that returns a list with 324 | zero or more rows of data. 325 | See the section below about [bind parameters](#bind-parameters) for more information. 326 | 327 | This is a convenience method that avoids the need to construct the entire SQL statement. A list if column names 328 | and various query clauses can be provided to simplify the call. 329 | 330 | See the SelectSQL method for a description of the returned list. 331 | 332 | ![Image](https://github.com/frdfsnlght/aix-SQLite/raw/master/docs/images/Insert.png) ![Image](https://github.com/frdfsnlght/aix-SQLite/raw/master/docs/images/InsertAsync.png) ![Image](https://github.com/frdfsnlght/aix-SQLite/raw/master/docs/images/AfterInsert.png) 333 | 334 | These methods execute a SQL INSERT statement that returns the unique row ID of the inserted row. 335 | 336 | ![Image](https://github.com/frdfsnlght/aix-SQLite/raw/master/docs/images/InsertFile.png) ![Image](https://github.com/frdfsnlght/aix-SQLite/raw/master/docs/images/InsertFileAsync.png) ![Image](https://github.com/frdfsnlght/aix-SQLite/raw/master/docs/images/AfterInsertFile.png) 337 | 338 | These methods perform a bulk insert of CSV formatted data from a file. 339 | The first non-empty line in the file should be a comma separated list of column names from the target table to insert 340 | values into. The second and subsequent non-empty lines should each be a comma separated list of values. 341 | Each of these lines will result in a new row inserted into the target table. 342 | Line continuation is also supported by using "\\" as the last character in a broken 343 | line. The literal string "\n" will be replaced with an actual newline character. 344 | Execution stops at the first error. The methods return the number of rows successfully inserted. 345 | 346 | A file name prefix of "//" specifies a file in the app's assets. 347 | A file name prefix of "/" specifies a file on the external SD card. 348 | No prefix specifies a path relative to the app's private storage. 349 | 350 | It is recommended these methods should be used inside a transaction since they can result in 351 | partial execution in the event of an error. Typically, you want all the statements in the file to 352 | work, or none at all. 353 | 354 | ![Image](https://github.com/frdfsnlght/aix-SQLite/raw/master/docs/images/Replace.png) ![Image](https://github.com/frdfsnlght/aix-SQLite/raw/master/docs/images/ReplaceAsync.png) ![Image](https://github.com/frdfsnlght/aix-SQLite/raw/master/docs/images/AfterReplace.png) 355 | 356 | These methods execute a SQL REPLACE statement that returns the unique row ID of the inserted or updated row. 357 | A SQL REPLACE statement means "insert if it doesn't exist, update it if it does". 358 | 359 | ![Image](https://github.com/frdfsnlght/aix-SQLite/raw/master/docs/images/Update.png) ![Image](https://github.com/frdfsnlght/aix-SQLite/raw/master/docs/images/UpdateAsync.png) ![Image](https://github.com/frdfsnlght/aix-SQLite/raw/master/docs/images/AfterUpdate.png) 360 | 361 | These methods execute a SQL UPDATE statement, with optional bind parameters, that returns the number of 362 | rows that are updated. 363 | See the section below about [bind parameters](#bind-parameters) for more information. 364 | 365 | ![Image](https://github.com/frdfsnlght/aix-SQLite/raw/master/docs/images/Delete.png) ![Image](https://github.com/frdfsnlght/aix-SQLite/raw/master/docs/images/DeleteAsync.png) ![Image](https://github.com/frdfsnlght/aix-SQLite/raw/master/docs/images/AfterDelete.png) 366 | 367 | These methods execute a SQL DELETE statement, with optional bind parameters, that returns the number of 368 | rows that are deleted. If no whereClause is provided, which means "delete all the rows", a zero is returned. 369 | See the section below about [bind parameters](#bind-parameters) for more information. 370 | 371 | #### Bind Parameters 372 | 373 | Many of the methods above include whereClause and bindParams arguments. Together, the arguments provide a 374 | simple way to limit the rows operated on and prevent SQL injection attacks. The idea is simple. The whereClause 375 | is a string in the form of a SQL WHERE clause, without the word "WHERE". Any question marks in the whereClause 376 | are automatically replaced by the corresponding value from the bindParams list before the statement is 377 | executed by the database engine. The database engine deals with escaping the values as necessary. This 378 | allows the developer to skip building a complex whereClause string using concatenation. 379 | 380 | As an example, suppose we want to select rows from a table like this: 381 | 382 | ```sql 383 | SELECT * FROM myTable WHERE name = 'Unknown' AND catCount > 10 384 | ``` 385 | 386 | The WHERE clause in this SQL statement is: 387 | 388 | ```sql 389 | name = 'Unknown' AND catCount > 10 390 | ``` 391 | 392 | We can accomplish the same thing by passing in a whereClause like this: 393 | 394 | ```sql 395 | name = ? AND catCount > ? 396 | ``` 397 | 398 | And a bindParams list like this: 399 | 400 | ("Unknown", 10) 401 | 402 | ## Samples 403 | 404 | Coming soon... 405 | -------------------------------------------------------------------------------- /docs/images/AfterDelete.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frdfsnlght/aix-SQLite/63ad1bb80a23592c15e2e46a0324d9c19513e51d/docs/images/AfterDelete.png -------------------------------------------------------------------------------- /docs/images/AfterExecute.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frdfsnlght/aix-SQLite/63ad1bb80a23592c15e2e46a0324d9c19513e51d/docs/images/AfterExecute.png -------------------------------------------------------------------------------- /docs/images/AfterExecuteFile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frdfsnlght/aix-SQLite/63ad1bb80a23592c15e2e46a0324d9c19513e51d/docs/images/AfterExecuteFile.png -------------------------------------------------------------------------------- /docs/images/AfterInsert.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frdfsnlght/aix-SQLite/63ad1bb80a23592c15e2e46a0324d9c19513e51d/docs/images/AfterInsert.png -------------------------------------------------------------------------------- /docs/images/AfterInsertFile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frdfsnlght/aix-SQLite/63ad1bb80a23592c15e2e46a0324d9c19513e51d/docs/images/AfterInsertFile.png -------------------------------------------------------------------------------- /docs/images/AfterReplace.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frdfsnlght/aix-SQLite/63ad1bb80a23592c15e2e46a0324d9c19513e51d/docs/images/AfterReplace.png -------------------------------------------------------------------------------- /docs/images/AfterSelect.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frdfsnlght/aix-SQLite/63ad1bb80a23592c15e2e46a0324d9c19513e51d/docs/images/AfterSelect.png -------------------------------------------------------------------------------- /docs/images/AfterUpdate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frdfsnlght/aix-SQLite/63ad1bb80a23592c15e2e46a0324d9c19513e51d/docs/images/AfterUpdate.png -------------------------------------------------------------------------------- /docs/images/BeginTransaction.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frdfsnlght/aix-SQLite/63ad1bb80a23592c15e2e46a0324d9c19513e51d/docs/images/BeginTransaction.png -------------------------------------------------------------------------------- /docs/images/CloseDatabase.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frdfsnlght/aix-SQLite/63ad1bb80a23592c15e2e46a0324d9c19513e51d/docs/images/CloseDatabase.png -------------------------------------------------------------------------------- /docs/images/CommitTransation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frdfsnlght/aix-SQLite/63ad1bb80a23592c15e2e46a0324d9c19513e51d/docs/images/CommitTransation.png -------------------------------------------------------------------------------- /docs/images/DatabaseClosed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frdfsnlght/aix-SQLite/63ad1bb80a23592c15e2e46a0324d9c19513e51d/docs/images/DatabaseClosed.png -------------------------------------------------------------------------------- /docs/images/DatabaseCreated.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frdfsnlght/aix-SQLite/63ad1bb80a23592c15e2e46a0324d9c19513e51d/docs/images/DatabaseCreated.png -------------------------------------------------------------------------------- /docs/images/DatabaseDowngrade.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frdfsnlght/aix-SQLite/63ad1bb80a23592c15e2e46a0324d9c19513e51d/docs/images/DatabaseDowngrade.png -------------------------------------------------------------------------------- /docs/images/DatabaseExists.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frdfsnlght/aix-SQLite/63ad1bb80a23592c15e2e46a0324d9c19513e51d/docs/images/DatabaseExists.png -------------------------------------------------------------------------------- /docs/images/DatabaseOpened.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frdfsnlght/aix-SQLite/63ad1bb80a23592c15e2e46a0324d9c19513e51d/docs/images/DatabaseOpened.png -------------------------------------------------------------------------------- /docs/images/DatabasePath.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frdfsnlght/aix-SQLite/63ad1bb80a23592c15e2e46a0324d9c19513e51d/docs/images/DatabasePath.png -------------------------------------------------------------------------------- /docs/images/DatabaseUpgrade.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frdfsnlght/aix-SQLite/63ad1bb80a23592c15e2e46a0324d9c19513e51d/docs/images/DatabaseUpgrade.png -------------------------------------------------------------------------------- /docs/images/Delete.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frdfsnlght/aix-SQLite/63ad1bb80a23592c15e2e46a0324d9c19513e51d/docs/images/Delete.png -------------------------------------------------------------------------------- /docs/images/DeleteAsync.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frdfsnlght/aix-SQLite/63ad1bb80a23592c15e2e46a0324d9c19513e51d/docs/images/DeleteAsync.png -------------------------------------------------------------------------------- /docs/images/DeleteDatabase.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frdfsnlght/aix-SQLite/63ad1bb80a23592c15e2e46a0324d9c19513e51d/docs/images/DeleteDatabase.png -------------------------------------------------------------------------------- /docs/images/Execute.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frdfsnlght/aix-SQLite/63ad1bb80a23592c15e2e46a0324d9c19513e51d/docs/images/Execute.png -------------------------------------------------------------------------------- /docs/images/ExecuteAsync.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frdfsnlght/aix-SQLite/63ad1bb80a23592c15e2e46a0324d9c19513e51d/docs/images/ExecuteAsync.png -------------------------------------------------------------------------------- /docs/images/ExecuteFile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frdfsnlght/aix-SQLite/63ad1bb80a23592c15e2e46a0324d9c19513e51d/docs/images/ExecuteFile.png -------------------------------------------------------------------------------- /docs/images/ExecuteFileAsync.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frdfsnlght/aix-SQLite/63ad1bb80a23592c15e2e46a0324d9c19513e51d/docs/images/ExecuteFileAsync.png -------------------------------------------------------------------------------- /docs/images/ExportDatabase.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frdfsnlght/aix-SQLite/63ad1bb80a23592c15e2e46a0324d9c19513e51d/docs/images/ExportDatabase.png -------------------------------------------------------------------------------- /docs/images/GetDBName.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frdfsnlght/aix-SQLite/63ad1bb80a23592c15e2e46a0324d9c19513e51d/docs/images/GetDBName.png -------------------------------------------------------------------------------- /docs/images/GetDBVersion.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frdfsnlght/aix-SQLite/63ad1bb80a23592c15e2e46a0324d9c19513e51d/docs/images/GetDBVersion.png -------------------------------------------------------------------------------- /docs/images/GetDebugDialog.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frdfsnlght/aix-SQLite/63ad1bb80a23592c15e2e46a0324d9c19513e51d/docs/images/GetDebugDialog.png -------------------------------------------------------------------------------- /docs/images/GetDebugToast.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frdfsnlght/aix-SQLite/63ad1bb80a23592c15e2e46a0324d9c19513e51d/docs/images/GetDebugToast.png -------------------------------------------------------------------------------- /docs/images/GetReturnColumnNames.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frdfsnlght/aix-SQLite/63ad1bb80a23592c15e2e46a0324d9c19513e51d/docs/images/GetReturnColumnNames.png -------------------------------------------------------------------------------- /docs/images/ImportDatabase.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frdfsnlght/aix-SQLite/63ad1bb80a23592c15e2e46a0324d9c19513e51d/docs/images/ImportDatabase.png -------------------------------------------------------------------------------- /docs/images/Insert.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frdfsnlght/aix-SQLite/63ad1bb80a23592c15e2e46a0324d9c19513e51d/docs/images/Insert.png -------------------------------------------------------------------------------- /docs/images/InsertAsync.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frdfsnlght/aix-SQLite/63ad1bb80a23592c15e2e46a0324d9c19513e51d/docs/images/InsertAsync.png -------------------------------------------------------------------------------- /docs/images/InsertFile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frdfsnlght/aix-SQLite/63ad1bb80a23592c15e2e46a0324d9c19513e51d/docs/images/InsertFile.png -------------------------------------------------------------------------------- /docs/images/InsertFileAsync.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frdfsnlght/aix-SQLite/63ad1bb80a23592c15e2e46a0324d9c19513e51d/docs/images/InsertFileAsync.png -------------------------------------------------------------------------------- /docs/images/IsDatabaseOpen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frdfsnlght/aix-SQLite/63ad1bb80a23592c15e2e46a0324d9c19513e51d/docs/images/IsDatabaseOpen.png -------------------------------------------------------------------------------- /docs/images/OpenDatabase.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frdfsnlght/aix-SQLite/63ad1bb80a23592c15e2e46a0324d9c19513e51d/docs/images/OpenDatabase.png -------------------------------------------------------------------------------- /docs/images/Replace.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frdfsnlght/aix-SQLite/63ad1bb80a23592c15e2e46a0324d9c19513e51d/docs/images/Replace.png -------------------------------------------------------------------------------- /docs/images/ReplaceAsync.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frdfsnlght/aix-SQLite/63ad1bb80a23592c15e2e46a0324d9c19513e51d/docs/images/ReplaceAsync.png -------------------------------------------------------------------------------- /docs/images/RollbackTransaction.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frdfsnlght/aix-SQLite/63ad1bb80a23592c15e2e46a0324d9c19513e51d/docs/images/RollbackTransaction.png -------------------------------------------------------------------------------- /docs/images/SQLError.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frdfsnlght/aix-SQLite/63ad1bb80a23592c15e2e46a0324d9c19513e51d/docs/images/SQLError.png -------------------------------------------------------------------------------- /docs/images/Select.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frdfsnlght/aix-SQLite/63ad1bb80a23592c15e2e46a0324d9c19513e51d/docs/images/Select.png -------------------------------------------------------------------------------- /docs/images/SelectAsync.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frdfsnlght/aix-SQLite/63ad1bb80a23592c15e2e46a0324d9c19513e51d/docs/images/SelectAsync.png -------------------------------------------------------------------------------- /docs/images/SelectSQL.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frdfsnlght/aix-SQLite/63ad1bb80a23592c15e2e46a0324d9c19513e51d/docs/images/SelectSQL.png -------------------------------------------------------------------------------- /docs/images/SelectSQLAsync.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frdfsnlght/aix-SQLite/63ad1bb80a23592c15e2e46a0324d9c19513e51d/docs/images/SelectSQLAsync.png -------------------------------------------------------------------------------- /docs/images/SetDBName.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frdfsnlght/aix-SQLite/63ad1bb80a23592c15e2e46a0324d9c19513e51d/docs/images/SetDBName.png -------------------------------------------------------------------------------- /docs/images/SetDBVersion.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frdfsnlght/aix-SQLite/63ad1bb80a23592c15e2e46a0324d9c19513e51d/docs/images/SetDBVersion.png -------------------------------------------------------------------------------- /docs/images/SetDebugDialog.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frdfsnlght/aix-SQLite/63ad1bb80a23592c15e2e46a0324d9c19513e51d/docs/images/SetDebugDialog.png -------------------------------------------------------------------------------- /docs/images/SetDebugToast.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frdfsnlght/aix-SQLite/63ad1bb80a23592c15e2e46a0324d9c19513e51d/docs/images/SetDebugToast.png -------------------------------------------------------------------------------- /docs/images/SetReturnColumnNames.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frdfsnlght/aix-SQLite/63ad1bb80a23592c15e2e46a0324d9c19513e51d/docs/images/SetReturnColumnNames.png -------------------------------------------------------------------------------- /docs/images/TableCount.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frdfsnlght/aix-SQLite/63ad1bb80a23592c15e2e46a0324d9c19513e51d/docs/images/TableCount.png -------------------------------------------------------------------------------- /docs/images/TableExists.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frdfsnlght/aix-SQLite/63ad1bb80a23592c15e2e46a0324d9c19513e51d/docs/images/TableExists.png -------------------------------------------------------------------------------- /docs/images/TableNames.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frdfsnlght/aix-SQLite/63ad1bb80a23592c15e2e46a0324d9c19513e51d/docs/images/TableNames.png -------------------------------------------------------------------------------- /docs/images/TableRowCount.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frdfsnlght/aix-SQLite/63ad1bb80a23592c15e2e46a0324d9c19513e51d/docs/images/TableRowCount.png -------------------------------------------------------------------------------- /docs/images/Update.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frdfsnlght/aix-SQLite/63ad1bb80a23592c15e2e46a0324d9c19513e51d/docs/images/Update.png -------------------------------------------------------------------------------- /docs/images/UpdateAsync.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frdfsnlght/aix-SQLite/63ad1bb80a23592c15e2e46a0324d9c19513e51d/docs/images/UpdateAsync.png -------------------------------------------------------------------------------- /org.bennedum.SQLite.aix: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frdfsnlght/aix-SQLite/63ad1bb80a23592c15e2e46a0324d9c19513e51d/org.bennedum.SQLite.aix -------------------------------------------------------------------------------- /src/SQLite.java: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | MIT License 4 | 5 | Copyright (c) 2018 Thomas Bennedum 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. 24 | 25 | */ 26 | 27 | package org.bennedum.SQLite; 28 | 29 | import android.content.Context; 30 | import android.content.ContentValues; 31 | import android.database.Cursor; 32 | import android.database.sqlite.SQLiteDatabase; 33 | import android.database.sqlite.SQLiteCursor; 34 | import android.database.sqlite.SQLiteOpenHelper; 35 | import android.database.SQLException; 36 | import android.os.AsyncTask; 37 | import android.os.Environment; 38 | import android.widget.Toast; 39 | 40 | import com.google.appinventor.components.annotations.*; 41 | import com.google.appinventor.components.runtime.*; 42 | import com.google.appinventor.components.common.ComponentCategory; 43 | import com.google.appinventor.components.common.PropertyTypeConstants; 44 | import com.google.appinventor.components.runtime.util.AsynchUtil; 45 | import com.google.appinventor.components.runtime.util.YailList; 46 | 47 | import java.util.ArrayList; 48 | import java.util.concurrent.ExecutionException; 49 | import java.io.File; 50 | import java.io.FileInputStream; 51 | import java.io.FileOutputStream; 52 | import java.io.InputStream; 53 | import java.io.OutputStream; 54 | import java.io.InputStreamReader; 55 | import java.io.BufferedReader; 56 | import java.io.FileNotFoundException; 57 | import java.io.IOException; 58 | 59 | 60 | @DesignerComponent( 61 | version = SQLite.VERSION, 62 | description = "SQLite database interface.", 63 | category = ComponentCategory.EXTENSION, 64 | nonVisible = true, 65 | iconName = "aiwebres/small-icon.png" 66 | ) 67 | @UsesPermissions(permissionNames = "android.permission.READ_EXTERNAL_STORAGE, android.permission.WRITE_EXTERNAL_STORAGE") 68 | 69 | 70 | @SimpleObject(external = true) 71 | public class SQLite extends AndroidNonvisibleComponent implements Component { 72 | 73 | public static final int VERSION = 1; 74 | 75 | private static final String NAME = "SQLite"; 76 | 77 | // Extension properties 78 | private boolean debugToast = false; 79 | private boolean debugDialog = false; 80 | private String dbName = "db.sqlite"; 81 | private int dbVersion = 1; 82 | private boolean returnColumnNames = false; 83 | 84 | private ComponentContainer container; 85 | private Context context; 86 | private boolean isRepl; 87 | 88 | private DBHelper dbHelper = null; 89 | private SQLiteDatabase db = null; 90 | 91 | /** 92 | * Helper class for handling database life cycle events. 93 | */ 94 | private class DBHelper extends SQLiteOpenHelper { 95 | 96 | public DBHelper(Context context) { 97 | super(context, dbName, null, dbVersion); 98 | } 99 | 100 | @Override 101 | public void onOpen(SQLiteDatabase db) { 102 | debug("Database opened"); 103 | SQLite.this.db = db; 104 | form.runOnUiThread(new Runnable() { 105 | @Override 106 | public void run() { 107 | DatabaseOpened(); 108 | } 109 | }); 110 | } 111 | 112 | @Override 113 | public void onCreate(SQLiteDatabase db) { 114 | debug("Database created"); 115 | SQLite.this.db = db; 116 | form.runOnUiThread(new Runnable() { 117 | @Override 118 | public void run() { 119 | DatabaseCreated(); 120 | } 121 | }); 122 | } 123 | 124 | @Override 125 | public void onUpgrade(SQLiteDatabase db, final int oldVersion, final int newVersion) { 126 | debug("Database upgraded"); 127 | SQLite.this.db = db; 128 | form.runOnUiThread(new Runnable() { 129 | @Override 130 | public void run() { 131 | DatabaseUpgrade(oldVersion, newVersion); 132 | } 133 | }); 134 | } 135 | 136 | @Override 137 | public void onDowngrade(SQLiteDatabase db, final int oldVersion, final int newVersion) { 138 | debug("Database downgraded"); 139 | SQLite.this.db = db; 140 | form.runOnUiThread(new Runnable() { 141 | @Override 142 | public void run() { 143 | DatabaseDowngrade(oldVersion, newVersion); 144 | } 145 | }); 146 | } 147 | 148 | } 149 | 150 | /** 151 | * Helper class to pass into DBAsyncTask. 152 | * This is necessary because using a Runnable causes the static execute method to run and that's 153 | * how you get ants. 154 | */ 155 | private abstract class DBRunnable { 156 | public abstract void run(); 157 | } 158 | 159 | /** 160 | * Helper class to ensure all DB calls are serialized on a single thread. 161 | * Also provides a method to wait on the task completing. 162 | */ 163 | private class DBAsyncTask extends AsyncTask { 164 | 165 | public ArrayList rows = null; 166 | public long id = -1; 167 | public int count = -1; 168 | public boolean success = false; 169 | 170 | @Override 171 | protected Void doInBackground(DBRunnable... runnables) { 172 | if (runnables.length != 1) 173 | throw new RuntimeException("One runnable is required."); 174 | runnables[0].run(); 175 | return null; 176 | } 177 | 178 | public boolean waitUntilDone() { 179 | try { 180 | get(); 181 | return true; 182 | } catch (ExecutionException e) { 183 | Throwable t = e.getCause(); 184 | if (t instanceof Exception) 185 | debugException((Exception)t); 186 | return false; 187 | } catch (Exception e) { 188 | return false; 189 | } finally { 190 | if (rows == null) rows = new ArrayList(); 191 | } 192 | } 193 | 194 | public boolean executeAndWait(DBRunnable... runnables) { 195 | executeOnExecutor(AsyncTask.SERIAL_EXECUTOR, runnables); 196 | return waitUntilDone(); 197 | } 198 | 199 | } 200 | 201 | 202 | /** 203 | * Constructor 204 | */ 205 | public SQLite(ComponentContainer container) { 206 | super(container.$form()); 207 | isRepl = form instanceof ReplForm; // Note: form is defined in our superclass 208 | this.container = container; 209 | context = (Context)container.$context(); 210 | } 211 | 212 | private void debug(final String message) { 213 | if (debugToast || debugDialog) { 214 | form.runOnUiThread(new Runnable() { 215 | @Override 216 | public void run() { 217 | if (debugToast) 218 | Toast.makeText(context, NAME + ": " + message, Toast.LENGTH_SHORT).show(); 219 | if (debugDialog) 220 | //new Notifier(form).ShowAlert(NAME + ": " + message); 221 | new Notifier(form).ShowMessageDialog(NAME + ": " + message, NAME, "OK"); 222 | } 223 | }); 224 | } 225 | } 226 | 227 | private void debugException(final Exception e) { 228 | if (e != null) { 229 | debug(e.getMessage()); 230 | if (e instanceof SQLException) 231 | SQLError(e.getMessage()); 232 | } 233 | } 234 | 235 | private void toast(final String message) { 236 | Toast.makeText(context, NAME + ": " + message, Toast.LENGTH_SHORT).show(); 237 | } 238 | 239 | private boolean checkDB(final String action) { 240 | if (db == null) { 241 | debug("Database is not open: " + action); 242 | return false; 243 | } 244 | return true; 245 | } 246 | 247 | /** 248 | * Display debugging messages as toast messages. 249 | */ 250 | @SimpleProperty(category = PropertyCategory.BEHAVIOR, 251 | description = "Specifies whether debug messages should be displayed as Toast messages." 252 | ) 253 | public boolean DebugToast() { 254 | return debugToast; 255 | } 256 | 257 | /** 258 | * Display debugging messages as toast messages. 259 | */ 260 | @DesignerProperty(editorType = PropertyTypeConstants.PROPERTY_TYPE_BOOLEAN, 261 | defaultValue = "false" 262 | ) 263 | @SimpleProperty 264 | public void DebugToast(boolean debugToast) { 265 | this.debugToast = debugToast; 266 | } 267 | 268 | /** 269 | * Display debugging messages as alerts. 270 | */ 271 | @SimpleProperty(category = PropertyCategory.BEHAVIOR, 272 | description = "Specifies whether debug messages should be displayed in dialogs." 273 | ) 274 | public boolean DebugDialog() { 275 | return debugDialog; 276 | } 277 | 278 | /** 279 | * Display debugging messages as alerts. 280 | */ 281 | @DesignerProperty(editorType = PropertyTypeConstants.PROPERTY_TYPE_BOOLEAN, 282 | defaultValue = "false" 283 | ) 284 | @SimpleProperty 285 | public void DebugDialog(boolean debugDialog) { 286 | this.debugDialog = debugDialog; 287 | } 288 | 289 | /** 290 | * Name of the database. 291 | */ 292 | @SimpleProperty(category = PropertyCategory.BEHAVIOR, 293 | description = "Specifies the name of the database." 294 | ) 295 | public String DBName() { 296 | return dbName; 297 | } 298 | 299 | /** 300 | * Name of the database. 301 | */ 302 | @DesignerProperty(editorType = PropertyTypeConstants.PROPERTY_TYPE_STRING, 303 | defaultValue = "db.sqlite" 304 | ) 305 | @SimpleProperty 306 | public void DBName(String dbName) { 307 | this.dbName = dbName; 308 | } 309 | 310 | /** 311 | * Version of the database. 312 | */ 313 | @SimpleProperty(category = PropertyCategory.BEHAVIOR, 314 | description = "Specified the version of the database." 315 | ) 316 | public int DBVersion() { 317 | return dbVersion; 318 | } 319 | 320 | /** 321 | * Version of the database. 322 | */ 323 | @DesignerProperty(editorType = PropertyTypeConstants.PROPERTY_TYPE_INTEGER, 324 | defaultValue = "1" 325 | ) 326 | @SimpleProperty 327 | public void DBVersion(int dbVersion) { 328 | this.dbVersion = dbVersion; 329 | } 330 | 331 | /** 332 | * Should result lists contain column names. 333 | */ 334 | @SimpleProperty(category = PropertyCategory.BEHAVIOR, 335 | description = "Specifies whether lists of results will contain column names. " 336 | + "See the query blocks for more information." 337 | ) 338 | public boolean ReturnColumnNames() { 339 | return returnColumnNames; 340 | } 341 | 342 | /** 343 | * Should result lists contain column names. 344 | */ 345 | @DesignerProperty(editorType = PropertyTypeConstants.PROPERTY_TYPE_BOOLEAN, 346 | defaultValue = "false") 347 | @SimpleProperty 348 | public void ReturnColumnNames(boolean returnColumnNames) { 349 | this.returnColumnNames = returnColumnNames; 350 | } 351 | 352 | 353 | //======================================================== 354 | // Utility methods 355 | // 356 | 357 | /** 358 | * Returns the path to the database. 359 | */ 360 | @SimpleFunction(description = "Returns the full path to the database, even if it doesn't exist yet.") 361 | public String DatabasePath() { 362 | return context.getDatabasePath(dbName).getPath(); 363 | } 364 | 365 | /** 366 | * Returns true if the database file exists, false otherwise. 367 | */ 368 | @SimpleFunction(description = "Returns true if the database file exists, false otherwise.") 369 | public boolean DatabaseExists() { 370 | return context.getDatabasePath(dbName).exists(); 371 | } 372 | 373 | /** 374 | * Delete the database. 375 | */ 376 | @SimpleFunction(description = "Deletes a closed database. " 377 | + "This deletes the database file permanently." 378 | ) 379 | public void DeleteDatabase() { 380 | if (db != null) { 381 | debugException(new Exception("Unable to delete when the database is open.")); 382 | return; 383 | } 384 | context.deleteDatabase(dbName); 385 | debug("Database deleted"); 386 | } 387 | 388 | /** 389 | * Import a SQLite database file. 390 | * This is probably not best done on the main thread. 391 | */ 392 | @SimpleFunction(description = "Imports a SQLite database completely replacing the currently closed database. " 393 | + "Returns true if the import was successful, false otherwise." 394 | ) 395 | public boolean ImportDatabase(String fileName) { 396 | if (db != null) { 397 | debugException(new Exception("Unable to import when the database is open.")); 398 | return false; 399 | } 400 | InputStream is = null; 401 | OutputStream os = null; 402 | try { 403 | is = openInputStream(fileName); 404 | os = new FileOutputStream(context.getDatabasePath(dbName)); 405 | byte[] buf = new byte[1024]; 406 | int len; 407 | while ((len = is.read(buf)) > 0) 408 | os.write(buf, 0, len); 409 | debug("Database imported"); 410 | return true; 411 | } catch (IOException e) { 412 | debugException(e); 413 | return false; 414 | } finally { 415 | try { 416 | if (is != null) is.close(); 417 | if (os != null) os.close(); 418 | } catch (IOException e) {} 419 | } 420 | } 421 | 422 | /** 423 | * Exports the database. 424 | * This is probably not best done on the main thread. 425 | */ 426 | @SimpleFunction(description = "Exports the currently closed database to the specified file. " 427 | + "The resulting file is a complete SQLite database." 428 | + "Returns true if the import was successful, false otherwise." 429 | ) 430 | public boolean ExportDatabase(String fileName) { 431 | if (db != null) { 432 | debugException(new Exception("Unable to export when the database is open.")); 433 | return false; 434 | } 435 | InputStream is = null; 436 | OutputStream os = null; 437 | try { 438 | is = new FileInputStream(context.getDatabasePath(dbName)); 439 | os = new FileOutputStream(resolveFileName(fileName)); 440 | byte[] buf = new byte[1024]; 441 | int len; 442 | while ((len = is.read(buf)) > 0) 443 | os.write(buf, 0, len); 444 | debug("Database exported"); 445 | return true; 446 | } catch (IOException e) { 447 | debugException(e); 448 | return false; 449 | } finally { 450 | try { 451 | if (is != null) is.close(); 452 | if (os != null) os.close(); 453 | } catch (IOException e) {} 454 | } 455 | } 456 | 457 | //======================================================== 458 | // Database methods 459 | // 460 | 461 | /** 462 | * Opens the database. 463 | */ 464 | @SimpleFunction(description = "Opens the database. " 465 | + "If the database is already open, nothing happens." 466 | ) 467 | public void OpenDatabase() { 468 | if (db == null) { 469 | final DBAsyncTask task = new DBAsyncTask(); 470 | task.executeAndWait(new DBRunnable() { 471 | @Override 472 | public void run() { 473 | if (db != null) { 474 | task.success = true; 475 | return; 476 | } 477 | try { 478 | dbHelper = new DBHelper(context); 479 | db = dbHelper.getWritableDatabase(); 480 | task.success = true; 481 | } catch (SQLException e) { 482 | db = null; 483 | dbHelper = null; 484 | debugException(e); 485 | } 486 | } 487 | }); 488 | } 489 | } 490 | 491 | /** 492 | * Closes the database. 493 | */ 494 | @SimpleFunction(description = "Closes the database. If the database is already closed, nothing happens. " 495 | + "Any uncommited transactions will be rolled back." 496 | ) 497 | public void CloseDatabase() { 498 | if (db != null) { 499 | final DBAsyncTask task = new DBAsyncTask(); 500 | task.executeAndWait(new DBRunnable() { 501 | @Override 502 | public void run() { 503 | if (db == null) return; 504 | db.close(); 505 | db = null; 506 | dbHelper = null; 507 | task.success = true; 508 | } 509 | }); 510 | if (task.success) { 511 | debug("Database closed"); 512 | DatabaseClosed(); 513 | } 514 | } 515 | } 516 | 517 | /** 518 | * Indicates if the database has been opened or not. 519 | */ 520 | @SimpleFunction(description = "Returns true if the database is open, false otherwise.") 521 | public boolean IsDatabaseOpen() { 522 | return db != null; 523 | } 524 | 525 | /** 526 | * Returns the number of tables in the database. 527 | */ 528 | @SimpleFunction(description = "Returns the number of tables in the database, or -1 if an error occurs or the database is not open.") 529 | public int TableCount() { 530 | if (! checkDB("TableCount")) return -1; 531 | final DBAsyncTask task = new DBAsyncTask(); 532 | task.executeAndWait(new DBRunnable() { 533 | @Override 534 | public void run() { 535 | try { 536 | String sql = "SELECT count(1) FROM sqlite_master WHERE type='table'"; 537 | Cursor cursor = db.rawQuery(sql, null); 538 | cursor.moveToNext(); 539 | task.count = cursor.getInt(0); 540 | cursor.close(); 541 | } catch (SQLException e) { 542 | debugException(e); 543 | } 544 | } 545 | }); 546 | return task.count; 547 | } 548 | 549 | /** 550 | * Returns a list of names of the tables in the the database. 551 | */ 552 | @SimpleFunction(description = "Returns a list of names of the tables in the database, or an empty list if an error occurs or the database is not open.") 553 | public YailList TableNames() { 554 | if (! checkDB("TableNames")) return YailList.makeEmptyList(); 555 | final DBAsyncTask task = new DBAsyncTask(); 556 | task.executeAndWait(new DBRunnable() { 557 | @Override 558 | public void run() { 559 | try { 560 | String sql = "SELECT name FROM sqlite_master WHERE type='table'"; 561 | Cursor cursor = db.rawQuery(sql, null); 562 | task.rows = new ArrayList(); 563 | while (cursor.moveToNext()) 564 | task.rows.add(cursor.getString(0)); 565 | cursor.close(); 566 | } catch (SQLException e) { 567 | debugException(e); 568 | } 569 | } 570 | }); 571 | return YailList.makeList(task.rows); 572 | } 573 | 574 | /** 575 | * Returns true if the table exists in the database, false otherwise. 576 | */ 577 | @SimpleFunction(description = "Returns true if the table exists in the database, or false if the table does not exist or an error occurs or the database is not open.") 578 | public boolean TableExists(final String table) { 579 | if (! checkDB("TableExists")) return false; 580 | final DBAsyncTask task = new DBAsyncTask(); 581 | task.executeAndWait(new DBRunnable() { 582 | @Override 583 | public void run() { 584 | try { 585 | String sql = "SELECT count(1) FROM sqlite_master WHERE type='table' AND name=?"; 586 | Cursor cursor = db.rawQuery(sql, new String[] {table}); 587 | cursor.moveToNext(); 588 | task.count = cursor.getInt(0); 589 | cursor.close(); 590 | } catch (SQLException e) { 591 | debugException(e); 592 | } 593 | } 594 | }); 595 | return task.count == 1; 596 | } 597 | 598 | /** 599 | * Returns the number of records in a table. 600 | */ 601 | @SimpleFunction(description = "Returns the number of rows in a table, or -1 if an error occurs or the database is not open.") 602 | public int TableRowCount(final String table) { 603 | if (! checkDB("TableRowCount")) return -1; 604 | final DBAsyncTask task = new DBAsyncTask(); 605 | task.executeAndWait(new DBRunnable() { 606 | @Override 607 | public void run() { 608 | try { 609 | String sql = "SELECT count(1) FROM '" + table + "'"; 610 | Cursor cursor = db.rawQuery(sql, null); 611 | cursor.moveToNext(); 612 | task.count = cursor.getInt(0); 613 | cursor.close(); 614 | } catch (SQLException e) { 615 | debugException(e); 616 | } 617 | } 618 | }); 619 | return task.count; 620 | } 621 | 622 | //======================================================== 623 | // Transaction methods 624 | // 625 | 626 | /** 627 | * Begin a transaction. 628 | */ 629 | @SimpleFunction(description = "Begins a transaction on an open database. " 630 | + "Nested transactions are supported." 631 | ) 632 | public void BeginTransaction() { 633 | if (! checkDB("BeginTransaction")) return; 634 | final DBAsyncTask task = new DBAsyncTask(); 635 | task.executeAndWait(new DBRunnable() { 636 | @Override 637 | public void run() { 638 | try { 639 | db.beginTransaction(); 640 | task.success = true; 641 | } catch (Exception e) { 642 | debugException(e); 643 | } 644 | } 645 | }); 646 | if (task.success) 647 | debug("Transaction started"); 648 | } 649 | 650 | /** 651 | * Commit a transaction. 652 | */ 653 | @SimpleFunction(description = "Commits the last open transaction.") 654 | public void CommitTransaction() { 655 | if (! checkDB("CommitTransaction")) return; 656 | final DBAsyncTask task = new DBAsyncTask(); 657 | task.executeAndWait(new DBRunnable() { 658 | @Override 659 | public void run() { 660 | try { 661 | db.setTransactionSuccessful(); 662 | db.endTransaction(); 663 | task.success = true; 664 | } catch (Exception e) { 665 | debugException(e); 666 | } 667 | } 668 | }); 669 | if (task.success) 670 | debug("Transaction committed"); 671 | } 672 | 673 | /** 674 | * Rollback a transaction. 675 | */ 676 | @SimpleFunction(description = "Rolls back the last open transaction.") 677 | public void RollbackTransaction() { 678 | if (! checkDB("RollbackTransaction")) return; 679 | final DBAsyncTask task = new DBAsyncTask(); 680 | task.executeAndWait(new DBRunnable() { 681 | @Override 682 | public void run() { 683 | try { 684 | db.endTransaction(); 685 | } catch (Exception e) { 686 | debugException(e); 687 | } 688 | } 689 | }); 690 | if (task.success) 691 | debug("Transaction rolled back"); 692 | } 693 | 694 | //======================================================== 695 | // Data manipulation methods 696 | // 697 | 698 | /** 699 | * Execute a single, parameterized SQL statement that is NOT a SELECT. 700 | * @param sql: The SQL statement 701 | * @param bindParams: The list of parameter values to bind 702 | * @return true if the statements was executed successfully, false otherwise 703 | */ 704 | @SimpleFunction(description = "Execute a single, parameterized SQL statement that is NOT a SELECT and returns whether or not it succeeded. " 705 | + "Each bind parameter replaces the corresponding '?' in WHERE clause in the query. " 706 | + "If the database is not open, false is returned." 707 | ) 708 | public boolean Execute(final String sql, final YailList bindParams) { 709 | if (! checkDB("Execute")) return false; 710 | final DBAsyncTask task = new DBAsyncTask(); 711 | task.executeAndWait(new DBRunnable() { 712 | @Override 713 | public void run() { 714 | try { 715 | db.execSQL(sql, (bindParams == null) ? null : bindParams.toStringArray()); 716 | task.success = true; 717 | } catch (SQLException e) { 718 | debugException(e); 719 | } 720 | } 721 | }); 722 | if (task.success) 723 | debug("Execute: " + sql); 724 | return task.success; 725 | } 726 | 727 | /** 728 | * Execute a single, parameterized SQL statement that is NOT a SELECT, asynchronously. 729 | * @param tag: The identifier for the result of this operation 730 | * @param sql: The SQL statement 731 | * @param bindParams: The list of parameter values to bind 732 | */ 733 | @SimpleFunction(description = "Execute a single, parameterized SQL statement that is NOT a SELECT and returns whether or not it succeeded. " 734 | + "The tag identifies the result of this call in the AfterExecute event. " 735 | + "See ExecParamSQL for more information." 736 | ) 737 | public void ExecuteAsync(final String tag, final String sql, final YailList bindParams) { 738 | AsynchUtil.runAsynchronously(new Runnable() { 739 | @Override 740 | public void run() { 741 | final boolean res = Execute(sql, bindParams); 742 | form.runOnUiThread(new Runnable() { 743 | @Override 744 | public void run() { 745 | AfterExecute(tag, res); 746 | } 747 | }); 748 | } 749 | }); 750 | } 751 | 752 | /** 753 | * Execute multiple SQL statements from a file. 754 | * @param fileName The name of the file 755 | * @return the number of SQL statements executed, -1 if the database is not open 756 | */ 757 | @SimpleFunction(description = "Executes multiple SQL statements from a file and returns the count of statements successfully executed. " 758 | + "Each line of the file should be a complete non-SELECT SQL statement, optionally followed by a semicolon. " 759 | + "Single line (--) and multiline (/*...*/) comments are ignored. " 760 | + "Line continuation using '\\' is supported. " 761 | + "'\\n' are replaced with actual newlines. " 762 | + "Execution stops at the first error. " 763 | + "If the database is not open, -1 is returned. " 764 | + "Prefix the filename with / to read from a specific file on the SD card. " 765 | + "To read assets packaged with an application (also works for the Companion) start " 766 | + "the filename with // (two slashes). " 767 | + "If a filename does not start with a slash, it will be read from the applications private storage (for packaged " 768 | + "apps) and from /sdcard/AppInventor/data for the Companion." 769 | ) 770 | public int ExecuteFile(final String fileName) { 771 | if (! checkDB("ExecuteFile")) return -1; 772 | final DBAsyncTask task = new DBAsyncTask(); 773 | task.executeAndWait(new DBRunnable() { 774 | @Override 775 | public void run() { 776 | BufferedReader file = null; 777 | task.count = 0; 778 | try { 779 | InputStream is = openInputStream(fileName); 780 | file = new BufferedReader(new InputStreamReader(is)); 781 | boolean inComment = false; 782 | boolean inLine = false; 783 | String fullLine = ""; 784 | 785 | for (String line = file.readLine(); line != null; line = file.readLine()) { 786 | if (inComment) { 787 | if (line.matches("\\*\\/")) { // end of multiline comment 788 | line = line.replaceFirst(".*?\\*\\/", ""); 789 | inComment = false; 790 | } else 791 | continue; 792 | } else { 793 | line = line.replaceAll("\\/\\*.*?\\*\\/", ""); // remove single line comments 794 | line = line.replaceFirst("--.*$", ""); // remove single line comment 795 | if (line.matches("\\/\\*")) { // start of multiline comment 796 | line = line.replaceFirst("\\/\\*.*$", ""); 797 | inComment = true; 798 | } 799 | } 800 | 801 | line = line.trim(); 802 | if (line.endsWith("\\")) { 803 | inLine = true; 804 | fullLine += line.substring(0, line.length() - 1).trim(); 805 | continue; 806 | } 807 | inLine = false; 808 | 809 | fullLine += line; 810 | 811 | if (fullLine.endsWith(";")) 812 | fullLine = fullLine.substring(0, fullLine.length() - 1).trim(); 813 | 814 | if (fullLine.length() != 0) { 815 | fullLine = fullLine.replace("\\n", "\n"); // replace \n with actual newline 816 | db.execSQL(fullLine); 817 | task.count++; 818 | fullLine = ""; 819 | } 820 | } 821 | } catch (Exception e) { 822 | debugException(e); 823 | } finally { 824 | if (file != null) 825 | try { 826 | file.close(); 827 | } catch (IOException e) {} 828 | } 829 | } 830 | }); 831 | debug("ExecuteFile: " + task.count + " statements executed"); 832 | return task.count; 833 | } 834 | 835 | /** 836 | * Execute multiple SQL statements from a file, asynchronously. 837 | * @param tag: The identifier for the result of this operation 838 | * @param fileName The name of the file 839 | */ 840 | @SimpleFunction(description = "Executes multiple SQL statements from a file, asynchronously. " 841 | + "The tag identifies the result of this call in the AfterExecuteFile event. " 842 | + "See ExecFile for more information." 843 | ) 844 | public void ExecuteFileAsync(final String tag, final String fileName) { 845 | AsynchUtil.runAsynchronously(new Runnable() { 846 | @Override 847 | public void run() { 848 | final int res = ExecuteFile(fileName); 849 | form.runOnUiThread(new Runnable() { 850 | @Override 851 | public void run() { 852 | AfterExecuteFile(tag, res); 853 | } 854 | }); 855 | } 856 | }); 857 | } 858 | 859 | /** 860 | * Execute a single, parameterized SQL SELECT statement and returns a list of records. 861 | * @param sql: The SQL statement 862 | * @param bindParams: The list of parameter values to bind 863 | * @return the list of result rows 864 | */ 865 | @SimpleFunction(description = "Execute a single, parameterized SQL SELECT statement and returns a list of records. " 866 | + "Each bind parameter replaces the corresponding '?' in WHERE clause in the query. " 867 | + "If selecting only a single column, each element in the returned list is the value of that column for each result row. " 868 | + "If selecting multiple columns, each element of the returned list is itself a list of values for each selected column. " 869 | + "If the ReturnColumnNames option is true, each column value will be a two element list where the first element is the column name and the second element is the column value. " 870 | + "If the database is not open, an empty list is returned." 871 | ) 872 | public YailList SelectSQL(final String sql, final YailList bindParams) { 873 | if (! checkDB("SelectSQL")) return YailList.makeEmptyList(); 874 | final DBAsyncTask task = new DBAsyncTask(); 875 | task.executeAndWait(new DBRunnable() { 876 | @Override 877 | public void run() { 878 | try { 879 | Cursor cursor = db.rawQuery(sql, (bindParams == null) ? null : bindParams.toStringArray()); 880 | task.rows = cursorToList(cursor); 881 | } catch (SQLException e) { 882 | debugException(e); 883 | } 884 | } 885 | }); 886 | debug("SelectSQL: " + task.rows.size() + " rows"); 887 | return YailList.makeList(task.rows); 888 | } 889 | 890 | /** 891 | * Execute a single, parameterized SQL SELECT statement, asynchronously. 892 | * @param tag: The identifier for the result of this operation 893 | * @param sql: The SQL statement 894 | * @param bindParams: The list of parameter values to bind 895 | */ 896 | @SimpleFunction(description = "Execute a single, parameterized SQL SELECT statement, asynchronously. " 897 | + "The tag identifies the result of this call in the AfterSelect event. " 898 | + "See SelectSQL for more information." 899 | ) 900 | public void SelectSQLAsync(final String tag, final String sql, final YailList bindParams) { 901 | AsynchUtil.runAsynchronously(new Runnable() { 902 | @Override 903 | public void run() { 904 | final YailList res = SelectSQL(sql, bindParams); 905 | form.runOnUiThread(new Runnable() { 906 | @Override 907 | public void run() { 908 | AfterSelect(tag, res.size(), res); 909 | } 910 | }); 911 | } 912 | }); 913 | } 914 | 915 | /** 916 | * Executes a SQL SELECT statement. 917 | * @param table: The table 918 | * @param distinct: Should the query return distinct rows 919 | * @param columns: List of column names to return 920 | * @param whereClause: The optional WHERE clause. Passing null or an empty string will return all rows. 921 | * @param bindParams: The list of parameter values to bind 922 | * @param groupBy: How to group rows, formatted as a SQL GROUP BY clause. Passing an empty string will cause the row to not be grouped. 923 | * @param having: Which row groups to include if row grouping is being used. Passing an empty string will cause all row groups to be included. 924 | * @param orderBy: How to order the rows, formatted as a SQL ORDER BY clause. Passing an empty string will return rows unordered. 925 | * @param limit: Limit how many rows to return, formatted as SQL LIMIT clause. Passing an empty string returns all matching rows. 926 | */ 927 | @SimpleFunction(description = "Executes a SQL SELECT statement. " 928 | + "There whereClause is optional. " 929 | + "All rows in the table will be returned if no whereClause is specified. " 930 | + "Each bind parameter replaces the corresponding '?' in whereClause. " 931 | + "groupBy is optional. If not specified, no grouping will be performed. " 932 | + "having is optional. If not specified, all row groups will be returned. " 933 | + "orderBy is optional. If not specified, rows will be returned unordered. " 934 | + "limit is optional. If not specified, all matching rows will be returned. " 935 | + "If selecting only a single column, each element in the returned list is the value of that column for each result row. " 936 | + "If selecting multiple columns, each element of the returned list is itself a list of values for each selected column. " 937 | + "If the ReturnColumnNames option is true, each column value will be a two element list where the first element is the column name and the second element is the column value. " 938 | + "See SelectRawSQL for more information on the returned list." 939 | ) 940 | public YailList Select(final String table, 941 | final boolean distinct, 942 | final YailList columns, 943 | final String whereClause, 944 | final YailList bindParams, 945 | final String groupBy, 946 | final String having, 947 | final String orderBy, 948 | final String limit) { 949 | if (! checkDB("Select")) return YailList.makeEmptyList(); 950 | final DBAsyncTask task = new DBAsyncTask(); 951 | task.executeAndWait(new DBRunnable() { 952 | @Override 953 | public void run() { 954 | try { 955 | Cursor cursor = db.query(distinct, table, 956 | columns.toStringArray(), 957 | (whereClause == "") ? null : whereClause, 958 | bindParams.toStringArray(), 959 | (groupBy == "") ? null : groupBy, 960 | (having == "") ? null : having, 961 | (orderBy == "") ? null : orderBy, 962 | (limit == "") ? null : limit 963 | ); 964 | task.rows = cursorToList(cursor); 965 | } catch (SQLException e) { 966 | debugException(e); 967 | task.rows = new ArrayList(); 968 | } 969 | } 970 | }); 971 | debug("Select " + task.rows.size() + " rows from " + table); 972 | return YailList.makeList(task.rows); 973 | } 974 | 975 | 976 | /** 977 | * Executes a SQL SELECT statement, asynchronously. 978 | * @param tag: The identifier for the result of this operation 979 | * @param table: The table 980 | * @param distinct: Should the query return distinct rows 981 | * @param columns: List of column names to return 982 | * @param whereClause: The optional WHERE clause. Passing null or an empty string will return all rows. 983 | * @param bindParams: The list of parameter values to bind 984 | * @param groupBy: How to group rows, formatted as a SQL GROUP BY clause. Passing an empty string will cause the row to not be grouped. 985 | * @param having: Which row groups to include if row grouping is being used. Passing an empty string will cause all row groups to be included. 986 | * @param orderBy: How to order the rows, formatted as a SQL ORDER BY clause. Passing an empty string will return rows unordered. 987 | * @param limit: Limit how many rows to return, formatted as SQL LIMIT clause. Passing an empty string returns all matching rows. 988 | * @return the list of result rows 989 | */ 990 | @SimpleFunction(description = "Executes a SQL SELECT statement, asynchronously. " 991 | + "The tag identifies the result of this call in the AfterSelect event. " 992 | + "See Select for more information." 993 | ) 994 | public void SelectAsync(final String tag, 995 | final String table, 996 | final boolean distinct, 997 | final YailList columns, 998 | final String whereClause, 999 | final YailList bindParams, 1000 | final String groupBy, 1001 | final String having, 1002 | final String orderBy, 1003 | final String limit) { 1004 | AsynchUtil.runAsynchronously(new Runnable() { 1005 | @Override 1006 | public void run() { 1007 | final YailList res = Select(table, distinct, columns, whereClause, bindParams, groupBy, having, orderBy, limit); 1008 | form.runOnUiThread(new Runnable() { 1009 | @Override 1010 | public void run() { 1011 | AfterSelect(tag, res.size(), res); 1012 | } 1013 | }); 1014 | } 1015 | }); 1016 | } 1017 | 1018 | /** 1019 | * Executes a SQL INSERT statement. 1020 | * @param table: Table name 1021 | * @param columns: List of column names 1022 | * @param values: List of column values 1023 | * @return the row ID of the newly inserted row, or -1 if an error occurred 1024 | */ 1025 | @SimpleFunction(description = "Executes a SQL INSERT statement. " 1026 | + "columns contains a list of column names. " 1027 | + "values contains a list of column values. " 1028 | + "Returns the row ID of the newly inserted row. " 1029 | + "If the error occurs or the database is not open, -1 is returned." 1030 | ) 1031 | public long Insert(final String table, final YailList columns, final YailList values) { 1032 | if (! checkDB("Insert")) return -1; 1033 | final DBAsyncTask task = new DBAsyncTask(); 1034 | task.executeAndWait(new DBRunnable() { 1035 | @Override 1036 | public void run() { 1037 | try { 1038 | task.id = db.insert(table, null, makeContentValues(columns, values)); 1039 | } catch (SQLException e) { 1040 | debugException(e); 1041 | } 1042 | } 1043 | }); 1044 | debug("Insert: " + table + " id = " + task.id); 1045 | return task.id; 1046 | } 1047 | 1048 | /** 1049 | * Executes a SQL INSERT statement, asynchronously. 1050 | * @param tag: The identifier for the result of this operation 1051 | * @param sql: The SQL statement 1052 | * @param bindParams: The list of parameter values to bind 1053 | */ 1054 | @SimpleFunction(description = "Executes a SQL INSERT statement, asynchronously. " 1055 | + "The tag identifies the result of this call in the AfterInsert event. " 1056 | + "See Insert for more information." 1057 | ) 1058 | public void InsertAsync(final String tag, final String table, final YailList columns, final YailList values) { 1059 | AsynchUtil.runAsynchronously(new Runnable() { 1060 | @Override 1061 | public void run() { 1062 | final long res = Insert(table, columns, values); 1063 | form.runOnUiThread(new Runnable() { 1064 | @Override 1065 | public void run() { 1066 | AfterInsert(tag, res); 1067 | } 1068 | }); 1069 | } 1070 | }); 1071 | } 1072 | 1073 | /** 1074 | * Inserts rows from a file. 1075 | * @param table Table name 1076 | * @param fileName The name of the file 1077 | * @return the number of rows inserted, -1 if the database is not open 1078 | */ 1079 | @SimpleFunction(description = "Inserts multiple rows of data from a file into a table. " 1080 | + "The first line of the file should be a CSV list of the column names. " 1081 | + "Each of the remaining lines should be a CSV list of values for each new row. " 1082 | + "Empty lines are ignored. " 1083 | + "Line continuation using '\\' is supported. " 1084 | + "'\\n' are replaced with actual newlines. " 1085 | + "Insertion stops at the first error. " 1086 | + "If the database is not open, -1 is returned. " 1087 | + "Prefix the filename with / to read from a specific file on the SD card. " 1088 | + "To read assets packaged with an application (also works for the Companion) start " 1089 | + "the filename with // (two slashes). " 1090 | + "If a filename does not start with a slash, it will be read from the applications private storage (for packaged " 1091 | + "apps) and from /sdcard/AppInventor/data for the Companion." 1092 | ) 1093 | public int InsertFile(final String table, final String fileName) { 1094 | if (! checkDB("InsertFile")) return -1; 1095 | final DBAsyncTask task = new DBAsyncTask(); 1096 | task.executeAndWait(new DBRunnable() { 1097 | @Override 1098 | public void run() { 1099 | BufferedReader file = null; 1100 | task.count = 0; 1101 | try { 1102 | InputStream is = openInputStream(fileName); 1103 | file = new BufferedReader(new InputStreamReader(is)); 1104 | boolean inLine = false; 1105 | String fullLine = ""; 1106 | String[] columnNames = null; 1107 | 1108 | for (String line = file.readLine(); line != null; line = file.readLine()) { 1109 | line = line.trim(); 1110 | if (line.endsWith("\\")) { 1111 | inLine = true; 1112 | fullLine += line.substring(0, line.length() - 1).trim(); 1113 | continue; 1114 | } 1115 | inLine = false; 1116 | 1117 | fullLine += line; 1118 | 1119 | if (fullLine.length() != 0) { 1120 | 1121 | if (columnNames == null) 1122 | columnNames = fullLine.split("\\s*,\\s*"); 1123 | else { 1124 | fullLine = fullLine.replace("\\n", "\n"); // replace \n with actual newline 1125 | String[] values = fullLine.split("\\s*,\\s*"); 1126 | ContentValues contentValues = new ContentValues(); 1127 | for (int i = 0; i < columnNames.length; i++) { 1128 | contentValues.put(columnNames[i], values[i]); 1129 | } 1130 | db.insert(table, null, contentValues); 1131 | task.count++; 1132 | } 1133 | fullLine = ""; 1134 | } 1135 | } 1136 | } catch (Exception e) { 1137 | debugException(e); 1138 | } finally { 1139 | if (file != null) 1140 | try { 1141 | file.close(); 1142 | } catch (IOException e) {} 1143 | } 1144 | } 1145 | }); 1146 | debug("InsertFile: " + task.count + " rows inserted"); 1147 | return task.count; 1148 | } 1149 | 1150 | /** 1151 | * Inserts rows from a file, asynchronously. 1152 | * @param tag: The identifier for the result of this operation 1153 | * @param table Table name 1154 | * @param fileName The name of the file 1155 | */ 1156 | @SimpleFunction(description = "Inserts multiple rows of data from a file into a table, asynchronously. " 1157 | + "The tag identifies the result of this call in the AfterInsertFile event. " 1158 | + "See InsertFile for more information." 1159 | ) 1160 | public void InsertFileAsync(final String tag, final String table, final String fileName) { 1161 | AsynchUtil.runAsynchronously(new Runnable() { 1162 | @Override 1163 | public void run() { 1164 | final int res = InsertFile(table, fileName); 1165 | form.runOnUiThread(new Runnable() { 1166 | @Override 1167 | public void run() { 1168 | AfterInsertFile(tag, res); 1169 | } 1170 | }); 1171 | } 1172 | }); 1173 | } 1174 | 1175 | /** 1176 | * Executes a SQL REPLACE statement. 1177 | * @param table: Table name 1178 | * @param columns: List of column names 1179 | * @param values: List of column values 1180 | * @return the row ID of the newly inserted or updated row, or -1 if an error occurred 1181 | */ 1182 | @SimpleFunction(description = "Executes a SQL REPLACE statement. " 1183 | + "columns contains a list of column names. " 1184 | + "values contains a list of column values. " 1185 | + "Returns the row ID of the newly inserted or updated row. " 1186 | + "If the error occurs or the database is not open, -1 is returned." 1187 | ) 1188 | public long Replace(final String table, final YailList columns, final YailList values) { 1189 | if (! checkDB("Replace")) return -1; 1190 | final DBAsyncTask task = new DBAsyncTask(); 1191 | task.executeAndWait(new DBRunnable() { 1192 | @Override 1193 | public void run() { 1194 | try { 1195 | task.id = db.replace(table, null, makeContentValues(columns, values)); 1196 | } catch (SQLException e) { 1197 | debugException(e); 1198 | } 1199 | } 1200 | }); 1201 | debug("Replace: " + table + " id = " + task.id); 1202 | return task.id; 1203 | } 1204 | 1205 | /** 1206 | * Executes a SQL REPLACE statement, asynchronously. 1207 | * @param tag: The identifier for the result of this operation 1208 | * @param table: Table name 1209 | * @param columns: List of column names 1210 | * @param values: List of column values 1211 | * @return the row ID of the newly inserted or updated row, or -1 if an error occurred 1212 | */ 1213 | @SimpleFunction(description = "Executes a SQL REPLACE statement, asynchronously. " 1214 | + "The tag identifies the result of this call in the AfterReplace event. " 1215 | + "See Replace for more information." 1216 | ) 1217 | public void ReplaceAsync(final String tag, final String table, final YailList columns, final YailList values) { 1218 | AsynchUtil.runAsynchronously(new Runnable() { 1219 | @Override 1220 | public void run() { 1221 | final long res = Replace(table, columns, values); 1222 | form.runOnUiThread(new Runnable() { 1223 | @Override 1224 | public void run() { 1225 | AfterReplace(tag, res); 1226 | } 1227 | }); 1228 | } 1229 | }); 1230 | } 1231 | 1232 | /** 1233 | * Executes a SQL UPDATE statement. 1234 | * @param table: Table name 1235 | * @param columns: List of column names 1236 | * @param values: List of column values 1237 | * @param whereClause: The optional WHERE clause. Passing null or an empty string will update all rows. 1238 | * @param bindParams: The list of parameter values to bind 1239 | * @return the number of rows affected 1240 | */ 1241 | @SimpleFunction(description = "Executes a SQL UPDATE statement. " 1242 | + "columns contains a list of column names. " 1243 | + "values contains a list of column values. " 1244 | + "There whereClause is optional. " 1245 | + "All rows in the table will be updated if no whereClause is specified. " 1246 | + "Each bind parameter replaces the corresponding '?' in whereClause. " 1247 | + "Returns the number of rows affected." 1248 | + "If an error occurs or the database is not open, -1 is returned." 1249 | ) 1250 | public int Update(final String table, final YailList columns, final YailList values, final String whereClause, final YailList bindParams) { 1251 | if (! checkDB("Update")) return -1; 1252 | final DBAsyncTask task = new DBAsyncTask(); 1253 | task.executeAndWait(new DBRunnable() { 1254 | @Override 1255 | public void run() { 1256 | try { 1257 | task.count = db.update(table, makeContentValues(columns, values), (whereClause == "") ? null : whereClause, bindParams.toStringArray()); 1258 | } catch (SQLException e) { 1259 | debugException(e); 1260 | } 1261 | } 1262 | }); 1263 | debug("Update: " + table + " " + task.count + " rows"); 1264 | return task.count; 1265 | } 1266 | 1267 | /** 1268 | * Executes a SQL UPDATE statement, asynchronously. 1269 | * @param tag: The identifier for the result of this operation 1270 | * @param table: Table name 1271 | * @param columns: List of column names 1272 | * @param values: List of column values 1273 | * @param whereClause: The optional WHERE clause. Passing null or an empty string will update all rows. 1274 | * @param bindParams: The list of parameter values to bind 1275 | * @return the number of rows affected 1276 | */ 1277 | @SimpleFunction(description = "Executes a SQL UPDATE statement, asynchronously. " 1278 | + "The tag identifies the result of this call in the AfterUpdate event. " 1279 | + "See Update for more information." 1280 | ) 1281 | public void UpdateAsync(final String tag, final String table, final YailList columns, final YailList values, final String whereClause, final YailList bindParams) { 1282 | AsynchUtil.runAsynchronously(new Runnable() { 1283 | @Override 1284 | public void run() { 1285 | final int res = Update(table, columns, values, whereClause, bindParams); 1286 | form.runOnUiThread(new Runnable() { 1287 | @Override 1288 | public void run() { 1289 | AfterUpdate(tag, res); 1290 | } 1291 | }); 1292 | } 1293 | }); 1294 | } 1295 | 1296 | 1297 | /** 1298 | * Executes a SQL DELETE statement. 1299 | * @param table: Table name 1300 | * @param whereClause: The optional WHERE clause. Passing null or an empty string will delete all rows. 1301 | * @param bindParams: The list of parameter values to bind 1302 | * @return the number of rows affected if a whereClause is passed in, 0 otherwise. To remove all rows and get a count pass "1" as the whereClause. 1303 | */ 1304 | @SimpleFunction(description = "Executes a SQL DELETE statement. " 1305 | + "There whereClause is optional. " 1306 | + "All rows in the table will be deleted if no whereClause is specified. " 1307 | + "Each bind parameter replaces the corresponding '?' in whereClause. " 1308 | + "Returns the number of rows affected if a whereClause is passed in, 0 otherwise." 1309 | + "If an error occurs or the database is not open, -1 is returned." 1310 | ) 1311 | public int Delete(final String table, final String whereClause, final YailList bindParams) { 1312 | if (! checkDB("Delete")) return -1; 1313 | final DBAsyncTask task = new DBAsyncTask(); 1314 | task.executeAndWait(new DBRunnable() { 1315 | @Override 1316 | public void run() { 1317 | try { 1318 | task.count = db.delete(table, (whereClause == "") ? null : whereClause, bindParams.toStringArray()); 1319 | } catch (SQLException e) { 1320 | debugException(e); 1321 | } 1322 | } 1323 | }); 1324 | if ((task.count == 0) && ((whereClause == null) || (whereClause == ""))) 1325 | debug("Delete: " + table + " all rows"); 1326 | else 1327 | debug("Delete: " + table + " " + task.count + " rows"); 1328 | return task.count; 1329 | } 1330 | 1331 | /** 1332 | * Executes a SQL DELETE statement, asynchronously. 1333 | * @param tag: The identifier for the result of this operation 1334 | * @param table: Table name 1335 | * @param whereClause: The optional WHERE clause. Passing null or an empty string will delete all rows. 1336 | * @param bindParams: The list of parameter values to bind 1337 | * @return the number of rows affected if a whereClause is passed in, 0 otherwise. To remove all rows and get a count pass "1" as the whereClause. 1338 | */ 1339 | @SimpleFunction(description = "Executes a SQL DELETE statement. " 1340 | + "The tag identifies the result of this call in the AfterDelete event. " 1341 | + "See Delete for more information." 1342 | ) 1343 | public void DeleteAsync(final String tag, final String table, final String whereClause, final YailList bindParams) { 1344 | AsynchUtil.runAsynchronously(new Runnable() { 1345 | @Override 1346 | public void run() { 1347 | final int res = Delete(table, whereClause, bindParams); 1348 | form.runOnUiThread(new Runnable() { 1349 | @Override 1350 | public void run() { 1351 | AfterDelete(tag, res); 1352 | } 1353 | }); 1354 | } 1355 | }); 1356 | } 1357 | 1358 | /** 1359 | * Converts a cursor of returned records to a list. 1360 | * @param c: The cursor 1361 | * @return The list of result rows 1362 | */ 1363 | private ArrayList cursorToList(Cursor c) { 1364 | String[] columnNames = c.getColumnNames(); 1365 | int columnCount = c.getColumnCount(); 1366 | boolean singleColumn = columnCount == 1; 1367 | ArrayList rows = new ArrayList(); 1368 | ArrayList row; 1369 | ArrayList column; 1370 | 1371 | while (c.moveToNext()) { 1372 | if (singleColumn) { 1373 | if (returnColumnNames) { 1374 | column = new ArrayList(); 1375 | column.add(columnNames[0]); 1376 | column.add(columnValue(c, 0)); 1377 | rows.add(column); 1378 | } else { 1379 | rows.add(columnValue(c, 0)); 1380 | } 1381 | } else { 1382 | row = new ArrayList(); 1383 | for (int i = 0; i < columnCount; i++) { 1384 | if (returnColumnNames) { 1385 | column = new ArrayList(); 1386 | column.add(columnNames[i]); 1387 | column.add(columnValue(c, i)); 1388 | row.add(column); 1389 | } else { 1390 | row.add(columnValue(c, i)); 1391 | } 1392 | } 1393 | rows.add(row); 1394 | } 1395 | } 1396 | c.close(); 1397 | return rows; 1398 | } 1399 | 1400 | /** 1401 | * Gets a column value from a cursor. 1402 | * @param c: The cursor 1403 | * @param column: The column index 1404 | * @return The value of the column 1405 | */ 1406 | private Object columnValue(Cursor c, int column) { 1407 | switch (c.getType(column)) { 1408 | case Cursor.FIELD_TYPE_NULL: 1409 | return null; 1410 | case Cursor.FIELD_TYPE_INTEGER: 1411 | return c.getInt(column); 1412 | case Cursor.FIELD_TYPE_FLOAT: 1413 | return c.getDouble(column); 1414 | case Cursor.FIELD_TYPE_STRING: 1415 | return c.getString(column); 1416 | case Cursor.FIELD_TYPE_BLOB: 1417 | return new String(c.getBlob(column)); 1418 | default: 1419 | return null; 1420 | } 1421 | } 1422 | 1423 | /** 1424 | * Converts column names and values to ContentValues. 1425 | */ 1426 | private ContentValues makeContentValues(final YailList columns, final YailList values) { 1427 | String[] cols = columns.toStringArray(); 1428 | String[] vals = values.toStringArray(); 1429 | ContentValues contentValues = new ContentValues(); 1430 | for (int i = 0; i < cols.length; i++) { 1431 | contentValues.put(cols[i], vals[i]); 1432 | } 1433 | return contentValues; 1434 | } 1435 | 1436 | /** 1437 | * Returns an input stream for the specified file. 1438 | * @param fileName The path to the file. 1439 | A prefix of "//" specifies a file in the app's assets. 1440 | A prefix of "/" specifies a file on the external SD card. 1441 | No prefix specifies a path relative to the app's private storage. 1442 | @return an InputStream 1443 | */ 1444 | private InputStream openInputStream(final String fileName) throws IOException { 1445 | if (fileName.startsWith("//")) { 1446 | if (isRepl) 1447 | return new FileInputStream(Environment.getExternalStorageDirectory().getPath() + "/AppInventor/assets/" + fileName); 1448 | else 1449 | return context.getAssets().open(fileName.substring(2)); 1450 | } else 1451 | return new FileInputStream(resolveFileName(fileName)); 1452 | } 1453 | 1454 | /** 1455 | * Returns absolute file path. 1456 | * @param filename The path to the file. 1457 | A prefix of "/" specifies a file on the external SD card. 1458 | No prefix specifies a path relative to the app's private storage. 1459 | */ 1460 | 1461 | private String resolveFileName(final String fileName) { 1462 | if (fileName.startsWith("/")) 1463 | return Environment.getExternalStorageDirectory().getPath() + fileName; 1464 | File dirPath = context.getFilesDir(); 1465 | if (isRepl) { 1466 | String path = Environment.getExternalStorageDirectory().getPath() + "/AppInventor/data/"; 1467 | dirPath = new File(path); 1468 | if (! dirPath.exists()) { 1469 | dirPath.mkdirs(); // Make sure it exists 1470 | } 1471 | } 1472 | return dirPath.getPath() + "/" + fileName; 1473 | } 1474 | 1475 | 1476 | 1477 | 1478 | // ========================================== 1479 | // Events 1480 | 1481 | @SimpleEvent(description = "This event fires when the database is opened.") 1482 | public void DatabaseOpened() { 1483 | EventDispatcher.dispatchEvent(this, "DatabaseOpened"); 1484 | } 1485 | 1486 | @SimpleEvent(description = "This event fires when the database is closed.") 1487 | public void DatabaseClosed() { 1488 | EventDispatcher.dispatchEvent(this, "DatabaseClosed"); 1489 | } 1490 | 1491 | @SimpleEvent(description = "This event fires when the database is created.") 1492 | public void DatabaseCreated() { 1493 | EventDispatcher.dispatchEvent(this, "DatabaseCreated"); 1494 | } 1495 | 1496 | @SimpleEvent(description = "This event fires when the database is upgraded. " 1497 | + "The previous and new version numbers are provided. " 1498 | + "Use this event to modify the database as required by the version change." 1499 | ) 1500 | public void DatabaseUpgrade(int oldVersion, int newVersion) { 1501 | EventDispatcher.dispatchEvent(this, "DatabaseUpgrade", oldVersion, newVersion); 1502 | } 1503 | 1504 | @SimpleEvent(description = "This event fires when the database is downgraded. " 1505 | + "The previous and new version numbers are provided. " 1506 | + "Use this event to modify the database as required by the version change." 1507 | ) 1508 | public void DatabaseDowngrade(int oldVersion, int newVersion) { 1509 | EventDispatcher.dispatchEvent(this, "DatabaseDowngrade", oldVersion, newVersion); 1510 | } 1511 | 1512 | @SimpleEvent(description = "This event fires after an ExecuteSQLAsync call. " 1513 | + "The tag specified in the original call and the result of the execution are provided." 1514 | ) 1515 | public void AfterExecute(String tag, boolean result) { 1516 | EventDispatcher.dispatchEvent(this, "AfterExecute", tag, result); 1517 | } 1518 | 1519 | @SimpleEvent(description = "This event fires after an ExecuteFileAsync. " 1520 | + "The tag specified in the original call and the result of the execution are provided." 1521 | ) 1522 | public void AfterExecuteFile(String tag, int execCount) { 1523 | EventDispatcher.dispatchEvent(this, "AfterExecuteFile", tag, execCount); 1524 | } 1525 | 1526 | @SimpleEvent(description = "This event fires after an asynchronous Select call. " 1527 | + "The tag specified in the original call, the number of returned rows, and the result rows are provided." 1528 | ) 1529 | public void AfterSelect(String tag, int rowCount, YailList rows) { 1530 | EventDispatcher.dispatchEvent(this, "AfterSelect", tag, rowCount, rows); 1531 | } 1532 | 1533 | @SimpleEvent(description = "This event fires after an asynchronous Insert call. " 1534 | + "The tag specified in the original call and the row ID of the new row are provided." 1535 | ) 1536 | public void AfterInsert(String tag, long rowId) { 1537 | EventDispatcher.dispatchEvent(this, "AfterInsert", tag, rowId); 1538 | } 1539 | 1540 | @SimpleEvent(description = "This event fires after an asynchronous InsertFile call. " 1541 | + "The tag specified in the original call and the count of inserted rows are provided." 1542 | ) 1543 | public void AfterInsertFile(String tag, int rowCount) { 1544 | EventDispatcher.dispatchEvent(this, "AfterInsertFile", tag, rowCount); 1545 | } 1546 | 1547 | @SimpleEvent(description = "This event fires after an asynchronous Replace call. " 1548 | + "The tag specified in the original call and the row ID of the new or updated row are provided." 1549 | ) 1550 | public void AfterReplace(String tag, long rowId) { 1551 | EventDispatcher.dispatchEvent(this, "AfterReplace", tag, rowId); 1552 | } 1553 | 1554 | @SimpleEvent(description = "This event fires after an asynchronous Update call. " 1555 | + "The tag specified in the original call and the number of changed rows are provided." 1556 | ) 1557 | public void AfterUpdate(String tag, int rowCount) { 1558 | EventDispatcher.dispatchEvent(this, "AfterUpdate", tag, rowCount); 1559 | } 1560 | 1561 | @SimpleEvent(description = "This event fires after an asynchronous Delete call. " 1562 | + "The tag specified in the original call and the number of deleted rows are provided." 1563 | ) 1564 | public void AfterDelete(String tag, int rowCount) { 1565 | EventDispatcher.dispatchEvent(this, "AfterDelete", tag, rowCount); 1566 | } 1567 | 1568 | @SimpleEvent(description = "This event fires when a SQL error occurs. " 1569 | + "The error message is provided." 1570 | ) 1571 | public void SQLError(String message) { 1572 | EventDispatcher.dispatchEvent(this, "SQLError", message); 1573 | } 1574 | 1575 | 1576 | 1577 | 1578 | } 1579 | 1580 | -------------------------------------------------------------------------------- /src/aiwebres/small-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frdfsnlght/aix-SQLite/63ad1bb80a23592c15e2e46a0324d9c19513e51d/src/aiwebres/small-icon.png -------------------------------------------------------------------------------- /test/README.md: -------------------------------------------------------------------------------- 1 | # SQLiteTest 2 | 3 | This is a very simple App Inventor program I use to test some of the features of the SQLite extension. 4 | It should work in all the other flavors of the App Inventor platform. 5 | 6 | ## Resources 7 | 8 | The "exec.sql", "monthNames.txt", and "insert.csv" files should be added to the application as media assets. 9 | The program uses them for some of the tests. 10 | -------------------------------------------------------------------------------- /test/SQLiteTest.aia: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frdfsnlght/aix-SQLite/63ad1bb80a23592c15e2e46a0324d9c19513e51d/test/SQLiteTest.aia -------------------------------------------------------------------------------- /test/exec.sql: -------------------------------------------------------------------------------- 1 | /* 2 | * This is a file used by the SQLite test app. 3 | */ 4 | 5 | create table if not exists months (id integer primary key, name text, updates integer default 0) 6 | insert into months (name) values ('January') 7 | insert into months (name) values ('February') 8 | insert into months (name) values ('March') 9 | insert into months (name) values ('April') 10 | insert into months (name) values ('May') 11 | insert into months (name) values ('June') /* this is a single line comment */ 12 | insert into months (name) values ('July') 13 | insert into months (name) values ('August') 14 | insert into months (name) values ('September') 15 | insert into months (name) values ('October') -- this is a single line comment 16 | insert into months (name) values ('November') 17 | insert into months (name) values ('December') 18 | update months set updates = updates + 1 where name like '%ry' 19 | delete from months where name = 'July' 20 | -------------------------------------------------------------------------------- /test/insert.csv: -------------------------------------------------------------------------------- 1 | name,updates 2 | This is not a month name,0 3 | This is a multi-line \ 4 | test.,0 5 | This is also\na multi-line test,0 6 | -------------------------------------------------------------------------------- /test/monthNames.txt: -------------------------------------------------------------------------------- 1 | January,February,March,April,May,June,July,August,September,October,November,December --------------------------------------------------------------------------------