SQL Server CLR Integration enlisting in current transaction - sql-server

I'm trying to use CLR integration in SQL Server to handle accessing external files instead of storing them internally as BLOBs. I'm trying to figure out the pattern I need to follow to make my code enlist in the current SQL transaction. I figured I would start with the simplest scenario, deleting an existing row, since the insert/update scenarios would be more complex.
[SqlProcedure]
public static void DeleteStoredImages(SqlInt64 DocumentID)
{
if (DocumentID.IsNull)
return;
using (var conn = new SqlConnection("context connection=true"))
{
conn.Open();
string FaceFileName, RearFileName;
int Offset, Length;
GetFileLocation(conn, DocumentID.Value, true,
out FaceFileName, out Offset, out Length);
GetFileLocation(conn, DocumentID.Value, false,
out RearFileName, out Offset, out Length);
new DeleteTransaction().Enlist(FaceFileName, RearFileName);
using (var comm = conn.CreateCommand())
{
comm.CommandText = "DELETE FROM ImagesStore WHERE DocumentID = " + DocumentID.Value;
comm.ExecuteNonQuery();
}
}
}
private class DeleteTransaction : IEnlistmentNotification
{
public string FaceFileName { get; set; }
public string RearFileName { get; set; }
public void Enlist(string FaceFileName, string RearFileName)
{
this.FaceFileName = FaceFileName;
this.RearFileName = RearFileName;
var trans = Transaction.Current;
if (trans == null)
Commit(null);
else
trans.EnlistVolatile(this, EnlistmentOptions.None);
}
public void Commit(Enlistment enlistment)
{
if (FaceFileName != null && File.Exists(FaceFileName))
{
File.Delete(FaceFileName);
}
if (RearFileName != null && File.Exists(RearFileName))
{
File.Delete(RearFileName);
}
}
public void InDoubt(Enlistment enlistment)
{
}
public void Prepare(PreparingEnlistment preparingEnlistment)
{
preparingEnlistment.Prepared();
}
public void Rollback(Enlistment enlistment)
{
}
}
When I actually try to run this, I get the following exception:
A .NET Framework error occurred during execution of user defined routine or aggregate 'DeleteStoredImages':
System.Transactions.TransactionException: The operation is not valid for the state of the transaction. ---> System.Transactions.TransactionPromotionException: MSDTC on server 'BD009' is unavailable. ---> System.Data.SqlClient.SqlException: MSDTC on server 'BD009' is unavailable.
System.Data.SqlClient.SqlException:
at System.Data.SqlServer.Internal.StandardEventSink.HandleErrors()
at System.Data.SqlServer.Internal.ClrLevelContext.SuperiorTransaction.Promote()
System.Transactions.TransactionPromotionException:
at System.Data.SqlServer.Internal.ClrLevelContext.SuperiorTransaction.Promote()
at System.Transactions.TransactionStatePSPEOperation.PSPEPromote(InternalTransaction tx)
at System.Transactions.TransactionStateDelegatedBase.EnterState(InternalTransaction tx)
System.Transactions.TransactionException:
at System.Transactions.TransactionState.EnlistVolatile(InternalTransaction tx, IEnlistmentNotification enlistmentNotification, EnlistmentOptions enlistmentOptions, Transaction atomicTransaction)
at System.Transactions.TransactionStateSubordinateActive.EnlistVolatile(InternalTransaction tx, IEnlistmentNotification enlistmentNotification, EnlistmentOptions enlistmentOptions, Transaction atomicTransaction)
at System.Transactions.Transaction.EnlistVolatile(IEnlistmentNotification enlistmentNotification, EnlistmentOptions enlistmentOptions)
at ExternalImages.StoredProcedures.DeleteTransaction.Enlist(String FaceFileName, String RearFileName)
at ExternalImages.StoredProcedures.DeleteStoredImages(SqlInt64 DocumentID)
. User transaction, if any, will be rolled back.
The statement has been terminated.
Can anyone explain what I'm doing wrong, or point me to an example of how to do it right?

You have hopefully solved this by now, but in case anyone else has a similar problem: the error message you are getting suggests that you need to start the Distributed Transaction Coordinator service on the BD009 machine (presumably your own machine).

#Aasmund's answer regarding the Distributed Transaction Coordinator might solve the stated problem, but that still leaves you in a non-ideal state: You are tying a transaction, which locks the ImagesStore table (even if it is just a RowLock), to two file system operations? And you need to BEGIN and COMMIT the transaction outside of this function (since that isn't being handled in the presented code).
I would separate those two pieces:
Step 1: Delete the row from the table
and then, IF that did not error,
Step 2: Delete the file(s)
In the scenario where Step 1 succeeds but then Step 2, for whatever reason, fails, do one or both of the following:
return an error status code and keep track of which DocumentIDs got an error when attempting to delete the file in a status table. You can use that to manually delete the files and/or debug why the error occurred.
create a process that can run periodically to find and remove unreferenced files.

Related

Can Android Room validate an imported database before it is opened?

Problem: I can't seem to get an imported database to fail in Android Room until first queried? I'm wanting to try/catch and validate a database and I'm not succeeding on catching it before the first query. I always thought Android Room validated the database at the moment of instance creation and build against the schema, but apparently not. So the database fails upon first query.
What I'm trying to do: This app manages multiple databases that can be shared between users. So databases can be imported or exported. I suspect that at some point, someone will attempt to import the wrong database and or structure or do something to cause it to fail. I'm trying to catch the failure at the instance / build of the database or sooner.
What I've tried: I have a try/catch/finally block at the first instance creation, but it is only failing when first queried... then it notices that a table or column is missing. I'd like to catch it sooner if possible. I've looked at the RoomDatabase methods but nothing specifically applies to validation that I see other than just letting it break.
I always thought Android Room validated the database at the moment of instance creation and build against the schema, but apparently not.
The database validation is part of the open process, which does not happen until you actually try to access the database, as opposed to when getting the instance.
I can't seem to get an imported database to fail in Android Room until first queried?
When you get the instance you can force an open by getting (or trying to get) a SupportSQLiteDatabase by using either getWritableDatabase or getReadableDatabase via the instance's openHelper.
e.g.
(Kotlin)
db = TheDatabase.getInstance(this)
try {
val supportDB = db.openHelper.writableDatabase
}
catch(e: Exception) {
....
}
(Java)
db = TheDatabase.getInstance(this);
try {
SupportSQLiteDatabase supportDB = db.getOpenHelper().getWritableDatabase();
}
catch (Exception e) {
....
}
Alternative - Self validation
You could also do your own validation and thus avoid an exception (if the validation is simple enough). You could also make corrections thus allowing perhaps minor transgressions to be acceptable.
Before getting the actual instance, you could get a validation instance (different database name) that is created as per Room and then compare the schemas yourself.
here's an example designed to detect a table missing by creating the real database with a different table name (nottablex instead of tableX).
The TableX entity :-
#Entity
class TableX {
#PrimaryKey
Long id=null;
String name;
String other;
}
No Dao's as not needed for the example.
The TheDatabase with get instance methods, one for normal, the other for getting another (validation (empty model for schema comparison)) database but as an SQLiteDatabase.
#Database(entities = {TableX.class},version = 1)
abstract class TheDatabase extends RoomDatabase {
private static volatile TheDatabase instance = null;
private static volatile TheDatabase validationInstance = null;
static TheDatabase getInstance(Context context, String databaseName) {
if (instance == null ) {
instance = Room.databaseBuilder(context,TheDatabase.class,databaseName)
.allowMainThreadQueries()
.build();
}
return instance;
}
static SQLiteDatabase getValidationInstance(Context context, String databaseName) {
// Delete DB if it exists
if (context.getDatabasePath(databaseName).exists()) {
context.getDatabasePath(databaseName).delete();
}
// Create database and close it
TheDatabase db = Room.databaseBuilder(context,TheDatabase.class,databaseName)
.allowMainThreadQueries()
.build();
db.getOpenHelper().getWritableDatabase();
db.close();
return SQLiteDatabase.openDatabase(context.getDatabasePath(databaseName).getPath(),null,SQLiteDatabase.OPEN_READWRITE);
}
}
note that this forces the open to create the model/validation database (else the openDatabase would fail).
And finally for the demo MainActivity which creates an invalid database and then goes on to do a simple validation (just check that the expected tables exist). Only opening (never in the case of the example) the database if the tables expected by room exist.
public class MainActivity extends AppCompatActivity {
public static final String DATABASE_NAME = "the_database.db";
public static final String VALIDATION_DATABASE_NAME = DATABASE_NAME + "_validation";
public static final String TAG = "DBVALIDATION";
TheDatabase db;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
createIncorrectDatabase(this,DATABASE_NAME);
if (validateDatabase(VALIDATION_DATABASE_NAME,DATABASE_NAME) < 0) {
Log.d(TAG,"DATABASE " + DATABASE_NAME + " does mot match model.");
} else {
/* Database validated OK so use it */
db = TheDatabase.getInstance(this,DATABASE_NAME);
}
}
/* Purposefully create invalid database */
private void createIncorrectDatabase(Context context, String databaseName) {
File dbfile = context.getDatabasePath(databaseName);
if (!dbfile.exists()) {
dbfile.getParentFile().mkdirs();
}
SQLiteDatabase db = SQLiteDatabase.openOrCreateDatabase(context.getDatabasePath(databaseName),null);
db.execSQL("CREATE TABLE IF NOT EXISTS nottablex(id INTEGER PRIMARY KEY,name TEXT)");
db.close();
}
#SuppressLint("Range")
private long validateDatabase(String modelDatabase, String actualDatabase) {
String sqlite_master = "sqlite_master";
/* Want to skip room_master_table and sqlite tables susch as sqlite_sequence */
/* in this example only checking tables to show the basic technique */
String wherecluase = "name NOT LIKE 'sqlite_%' AND name NOT LIKE 'room_%' AND type = 'table'";
long rv = 0;
/* Get the model/validation database */
SQLiteDatabase modelDB = TheDatabase.getValidationInstance(this,modelDatabase);
/* Only need to check if the database exists as otherwise it will be created according to Room */
if (this.getDatabasePath(actualDatabase).exists()) {
/* Open as an SQLiteDatabase so no Room open to throw an exception */
SQLiteDatabase actualDB = SQLiteDatabase.openDatabase(this.getDatabasePath(actualDatabase).getAbsolutePath(),null,SQLiteDatabase.OPEN_READWRITE);
/* Get the tables expected from the model Room database */
Cursor modelTableNames = modelDB.query(sqlite_master,null,wherecluase,null,null,null,null);
Cursor actualTableNames = null; /* prepare Cursor */
/* Loop through the tables names in the model checking if they exist */
while (modelTableNames.moveToNext()) {
/* See if the expected table exists */
actualTableNames = actualDB.query(sqlite_master,null,"name=?",new String[]{modelTableNames.getString(modelTableNames.getColumnIndex("name"))},null,null,null);
if (!actualTableNames.moveToFirst()) {
Log.d(TAG,"Table " + modelTableNames.getString(modelTableNames.getColumnIndex("name")) + " not found.");
rv = rv -1; /* Table not found so decrement rv to indicate number not found */
}
}
/* Close the actualTableNames Cursor if it was used */
if (actualTableNames != null) {
actualTableNames.close();
}
/* Close the modelTableNames Cursor */
modelTableNames.close();
/* Close the actual database so Room can use it (comment out to show results in database Inspector)*/
actualDB.close();
} else {
Log.d(TAG,"Actual Database " + actualDatabase + " does not exist. No validation required as it would be created");
}
/* Close and delete the model database (comment out to show results in database Inspector)*/
modelDB.close();
this.getDatabasePath(modelDatabase).delete();
return rv;
}
}
Result
The log includes :-
D/DBVALIDATION: Table TableX not found.
D/DBVALIDATION: DATABASE the_database.db does mot match model.
Bypassing the close and model delete the databases for the above are:-
Note in this simple example Room would actually create the TableX table rather than fail with an exception.

Cannot Pass Null Value to Custom Aggregate

Afternoon,
I'm writing a custom median function (without looking at existing solutions, i like the challenge), after lots of fiddling I'm most of the way there. I cannot however pass in a column that contains a null value. I'm handling this in the c# Code but it seems to be being stopped by SQL before it gets there.
You get this error...
Msg 6569, Level 16, State 1, Line 11 'Median' failed because parameter 1 is not allowed to be null.
C#:
namespace SQLMedianAggregate
{
[System.Serializable]
[Microsoft.SqlServer.Server.SqlUserDefinedAggregate(
Microsoft.SqlServer.Server.Format.UserDefined,
IsInvariantToDuplicates = false, // duplicates may change results
IsInvariantToNulls = true, // receiving a NULL is handled later in code
IsInvariantToOrder = true, // is sorted later
IsNullIfEmpty = true, // if no values are given the result is null
MaxByteSize = -1,
Name = "Median" // name of the aggregate
)]
public struct Median : IBinarySerialize
{
public double Result { get; private set; }
public bool HasValue { get; private set; }
public DataTable DT_Values { get; private set; } //only exists for merge essentially
public static DataTable DT_Final { get; private set; } //Need a static version so its accesible within terminate
public void Init()
{
Result = double.NaN;
HasValue = false;
DT_Values = new DataTable();
DT_Values.Columns.Add("Values", typeof(double));
DT_Final = new DataTable();
DT_Final.Columns.Add("Values", typeof(double));
}
public void Accumulate(double number)
{
if (double.IsNaN(number))
{
//skip
}
else
{
//add to tables
DataRow NR = DT_Values.NewRow();
NR[0] = number;
DT_Values.Rows.Add(NR);
DataRow NR2 = DT_Final.NewRow();
NR2[0] = number;
DT_Final.Rows.Add(NR2);
HasValue = true;
}
}
public void Merge(Median group)
{
// Count the product only if the other group has values
if (group.HasValue)
{
DT_Final.Merge(group.DT_Values);
//DT_Final = DT_Values;
}
}
public double Terminate()
{
if (DT_Final.Rows.Count == 0) //Just to handle roll up so it doesn't crash (doesnt actually work
{
DataRow DR = DT_Final.NewRow();
DR[0] = 0;
DT_Final.Rows.Add(DR);
}
//Sort Results
DataView DV = DT_Final.DefaultView;
DV.Sort = "Values asc";
DataTable DTF = new DataTable();
DTF = DV.ToTable();
////Calculate median and submit result
double MiddleRow = (DT_Final.Rows.Count -1.0) / 2.0;
if (MiddleRow % 2 != 0)
{
double upper = (double)(DT_Final.Rows[Convert.ToInt32(Math.Ceiling(MiddleRow))]["Values"]);
double lower = (double)(DT_Final.Rows[Convert.ToInt32(Math.Floor(MiddleRow))]["Values"]);
Result = lower + ((upper - lower) / 2);
} else
{
Result = (double)(DT_Final.Rows[Convert.ToInt32(MiddleRow)]["Values"]);
}
return Result;
}
public void Read(BinaryReader SerializationReader)
{
//Needed to get this working for some reason
}
public void Write(BinaryWriter SerializationWriter)
{
//Needed to get this working for some reason
}
}
}
SQL:
DROP AGGREGATE dbo.Median
DROP ASSEMBLY MedianAggregate
CREATE ASSEMBLY MedianAggregate
AUTHORIZATION dbo
FROM 'C:\Users\#######\Documents\Visual Studio 2017\Projects\SQLMedianAggregate\SQLMedianAggregate\bin\Debug\SQLMedianAggregate.dll'
WITH PERMISSION_SET = UNSAFE;
CREATE AGGREGATE dbo.Median (#number FLOAT) RETURNS FLOAT
EXTERNAL NAME [MedianAggregate]."SQLMedianAggregate.Median";
Any ideas of what setting or code i'm missing that will allow this. I pretty much just want it to ignore nulls.
SQL Version is SQL2008 R2 btw
The problem is your datatype. You need to use the Sql* types for SQLCLR parameters, return values, and result set columns. In this case, you need to change:
Accumulate(double number)
into:
Accumulate(SqlDouble number)
Then, you access the double value using the Value property that all Sql* types have (i.e. number.Value in this case).
And then, at the beginning of the Accumulate method, you need to check for NULL using the IsNull property:
if (number.IsNull)
{
return;
}
Also, for more information on using SQLCLR in general, please see the series I am writing on this topic on SQL Server Central: Stairway to SQLCLR (free registration is required to read content on that site, but it's worth it :-).
And, since we are talking about median calculations here, please see the article I wrote (also on SQL Server Central) on the topic of UDAs and UDTs that uses Median as the example: Getting The Most Out of SQL Server 2005 UDTs and UDAs. Please keep in mind that the article was written for SQL Server 2005 which has a hard limit of 8000 bytes of memory for UDTs and UDAs. That limit was lifted in SQL Server 2008, so rather than using the compression technique shown in that article, you could simply set MaxByteSize in the SqlUserDefinedAggregate to -1 (as you are currently doing) or SqlMetaData.MaxSize (or something very close to that).
Also, DataTable is a bit heavy-handed for this type of operation. All you need is a simple List<Double> :-).
Regarding the following line of code (broken into 2 lines here to prevent the need to scroll):
public static DataTable DT_Final { get; private set; }
//Need a static version so its accesible within terminate
This is a huge misunderstanding of how UDAs and UDTs work. Please do NOT use static variables here. Static variables are shared across Sessions, hence your current approach is not thread-safe. So you would either get errors about it already being declared or various Sessions would alter the value unbeknownst to other Sessions, as they would all share the single instance of
DT_Final. And the errors and/or odd behavior (i.e. erroneous results that you can't debug) might happen in a single session if a parallel plan is used.
UDTs and UDAs get serialized to a binary value stored in memory, and then are deserialized which keeps their state intact. This is the reason for the Read and Write methods, and why you needed to get those working.
Again, you don't need (or want) DataTables here as they are over-complicating the operation and take up more memory than is ideal. Please see the article I linked above on UDAs and UDTs to see how the Median operation (and UDAs in general) should work.

Entity Framework - Saving entity generates an Id but record isn't added to database

I'm trying to insert a new record using Entity Framework using the following code. Within the BuildRequest.Save() method in the BuildRequest class, when the 'Insert' occurs, and the db.Save() is done, a JobId is correctly generated for the BuildRequest (indicating that the Insert is being done correctly), however the record isn't added to the database.
When I check in SQL profiler, here's what the insert is trying to do:
exec sp_executesql N'INSERT [dbo].[BuildQueue]([ApplicationId], [BuildReason])
VALUES (#0, #1)
SELECT [JobId]
FROM [dbo].[BuildQueue]
WHERE ##ROWCOUNT > 0 AND [JobId] = scope_identity()',N'#0 int,#1 tinyint,#0=5819,#1=0
Here is my call to create a new 'job':
BuildRequest job = new BuildRequest(ApplicationId, Core.BuildReasons.NewApp);
job.Save()
which uses the following class:
public class BuildRequest
{
private Data.BuildQueue _buildRequest;
public BuildRequest(int applicationId, BuildReasons reason)
{
_buildRequest = new Data.BuildQueue();
ApplicationId = applicationId;
BuildReason = reason;
}
public int JobId
{
get { return _buildRequest.JobId; }
}
public int ApplicationId
{
get
{
return _buildRequest.ApplicationId;
}
set
{
_buildRequest.ApplicationId = value;
}
}
public BuildReasons BuildReason
{
get
{
return (BuildReasons)_buildRequest.BuildReason;
}
set
{
_buildRequest.BuildReason = (byte)value;
}
}
public int Save()
{
using (Core.Data.UnitOfWork db = new Data.UnitOfWork())
{
Data.BuildQueue buildRequest = db.BuildQueueRepository.Get(a => a.JobId == this.JobId).SingleOrDefault();
if (buildRequest == null)//current build request doesn't already exist
{
db.BuildQueueRepository.Insert(_buildRequest);
}
else // we're editing an existing build request
{
db.BuildQueueRepository.Update(_buildRequest);
}
db.Save();
// **At this point, _buildRequest has a JobId, but no record is added to the database
return JobId;
}
}
}
What I've checked so far:
the 'UnitOfWork' object I create ('db'), is connected to the correct database (a remote SQL server database)
the db.BuildQueueRepository.Insert(...) is correctly calling dbSet.Add(entity);
the db.Save() is correctly calling context.SaveChanges();
there's existing code elsewhere in my application that uses the same generic UnitOfWork repository, which appears almost identical to the code above, and it works perfectly
What could be happening? What could be the difference in the code that's working correctly?
Doh! There's a job that takes new records out of the queue if they're in a certain status. It turns out that this particular routine sets that status immediately and the record was being entered then immediately moved to a different table.

Does flyway migrations support PostgreSQL's COPY?

Having performed a pg_dump of an existing posgresql schema, I have an sql file containing a number of table population statements using the copy.
COPY test_table (id, itm, factor, created_timestamp, updated_timestamp, updated_by_user, version) FROM stdin;
1 600 0.000 2012-07-17 18:12:42.360828 2012-07-17 18:12:42.360828 system 0
2 700 0.000 2012-07-17 18:12:42.360828 2012-07-17 18:12:42.360828 system 0
\.
Though not standard this is part of PostgreSQL's PLSQL implementation.
Performing a flyway migration (via the maven plugin) I get:
[ERROR] Caused by org.postgresql.util.PSQLException: ERROR: unexpected message type 0x50 during COPY from stein
Am I doing something wrong, or is this just not supported?
Thanks.
The short answer is no.
The one definite problem is that the parser is currently not able to deal with this special construct.
The other question is jdbc driver support. Could you try and see if this syntax generally supported by the jdbc driver with a single createStatement call?
If it is, please file an issue in the issue tracker and I'll extend the parser.
Update: This is now supported
I have accomplished this for Postgres using
public abstract class SeedData implements JdbcMigration {
protected static final String CSV_COPY_STRING = "COPY %s(%s) FROM STDIN HEADER DELIMITER ',' CSV ENCODING 'UTF-8'";
protected CopyManager copyManager;
#Override
public void migrate(Connection connection) throws Exception {
log.info(String.format("[%s] Populating database with seed data", getClass().getName()));
copyManager = new CopyManager((BaseConnection) connection);
Resource[] resources = scanForResources();
List<Resource> res = Arrays.asList(resources);
for (Resource resource : res) {
load(resource);
}
}
private void load(Resource resource) throws SQLException, IOException {
String location = resource.getLocation();
InputStream inputStream = getClass().getClassLoader().getResourceAsStream(location);
if (inputStream == null) {
throw new FlywayException("Failure to load seed data. Unable to load from location: " + location);
}
if (!inputStream.markSupported()) {
// Sanity check. We have to be able to mark the stream.
throw new FlywayException(
"Failure to load seed data as mark is not supported. Unable to load from location: " + location);
}
// set our mark to something big
inputStream.mark(1 << 32);
String filename = resource.getFilename();
// Strip the prefix (e.g. 01_) and the file extension (e.g. .csv)
String table = filename.substring(3, filename.length() - 4);
String columns = loadCsvHeader(location, inputStream);
// reset to the mark
inputStream.reset();
// Use Postgres COPY command to bring it in
long result = copyManager.copyIn(String.format(CSV_COPY_STRING, table, columns), inputStream);
log.info(format(" %s - Inserted %d rows", location, result));
}
private String loadCsvHeader(String location, InputStream inputStream) {
try {
return new BufferedReader(new InputStreamReader(inputStream)).readLine();
} catch (IOException e) {
throw new FlywayException("Failure to load seed data. Unable to load from location: " + location, e);
}
}
private Resource[] scanForResources() throws IOException {
return new ClassPathScanner(getClass().getClassLoader()).scanForResources(getSeedDataLocation(), "", ".csv");
}
protected String getSeedDataLocation() {
return getClass().getPackage().getName().replace('.', '/');
}
}
To use implement the class with the appropriate classpath
package db.devSeedData.dev;
public class v0_90__seed extends db.devSeedData.v0_90__seed {
}
All that is needed then is to have CSV files in your classpath under db/devSeedData that follow the format 01_tablename.csv. Columns are extracted from the header line of the CSV.

Logging SQL Statements in Android; fired by application

I have a small Android app and currently I am firing a sql statement in android to get the count of rows in database for a specific where clause.
Following is my sample code:
public boolean exists(Balloon balloon) {
if(balloon != null) {
Cursor c = null;
String count_query = "Select count(*) from balloons where _id = ?";
try {
c = getReadableDatabase().rawQuery(count_query, new String[] {balloon.getId()});
if (c.getCount() > 0)
return true;
} catch(SQLException e) {
Log.e("Running Count Query", e.getMessage());
} finally {
if(c!=null) {
try {
c.close();
} catch (SQLException e) {}
}
}
}
return false;
}
This method returns me a count 1 even when the database table is actually empty. I am not able to figure out; why that would happen. Running the same query in database gets me a count of 0.
I was wondering if it's possible to log or see in a log, all the sql queries that eventually get fired on database after parameter substitution; so that I can understand what's going wrong.
Cheers
That query will always return one record (as will any SELECT COUNT). The reason is, that the record it returns contains a field that indicates how many records are present in the "balloons" table with that ID.
In other words, the one record (c.getcount()==1) is NOT saying that the number of records found in balloons is one, rather it is the record generated by Sqlite which contains a field with the result.
To find out the number of balloons, you should c.movetofirst() and then numberOfBalloons = c.getInt(0)

Resources