diff options
Diffstat (limited to 'storage')
134 files changed, 25047 insertions, 0 deletions
diff --git a/storage/.eslintrc.js b/storage/.eslintrc.js new file mode 100644 index 000000000..69afc2f3c --- /dev/null +++ b/storage/.eslintrc.js @@ -0,0 +1,7 @@ +"use strict"; + +module.exports = { + "extends": [ + "../toolkit/.eslintrc.js" + ] +}; diff --git a/storage/FileSystemModule.cpp b/storage/FileSystemModule.cpp new file mode 100644 index 000000000..ed2f8cdef --- /dev/null +++ b/storage/FileSystemModule.cpp @@ -0,0 +1,304 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "FileSystemModule.h" + +#include "sqlite3.h" +#include "nsString.h" +#include "nsISimpleEnumerator.h" +#include "nsIFile.h" + +namespace { + +struct VirtualTableCursorBase +{ + VirtualTableCursorBase() + { + memset(&mBase, 0, sizeof(mBase)); + } + + sqlite3_vtab_cursor mBase; +}; + +struct VirtualTableCursor : public VirtualTableCursorBase +{ +public: + VirtualTableCursor() + : mRowId(-1) + { + mCurrentFileName.SetIsVoid(true); + } + + const nsString& DirectoryPath() const + { + return mDirectoryPath; + } + + const nsString& CurrentFileName() const + { + return mCurrentFileName; + } + + int64_t RowId() const + { + return mRowId; + } + + nsresult Init(const nsAString& aPath); + nsresult NextFile(); + +private: + nsCOMPtr<nsISimpleEnumerator> mEntries; + + nsString mDirectoryPath; + nsString mCurrentFileName; + + int64_t mRowId; +}; + +nsresult +VirtualTableCursor::Init(const nsAString& aPath) +{ + nsCOMPtr<nsIFile> directory = + do_CreateInstance(NS_LOCAL_FILE_CONTRACTID); + NS_ENSURE_TRUE(directory, NS_ERROR_FAILURE); + + nsresult rv = directory->InitWithPath(aPath); + NS_ENSURE_SUCCESS(rv, rv); + + rv = directory->GetPath(mDirectoryPath); + NS_ENSURE_SUCCESS(rv, rv); + + rv = directory->GetDirectoryEntries(getter_AddRefs(mEntries)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = NextFile(); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +nsresult +VirtualTableCursor::NextFile() +{ + bool hasMore; + nsresult rv = mEntries->HasMoreElements(&hasMore); + NS_ENSURE_SUCCESS(rv, rv); + + if (!hasMore) { + mCurrentFileName.SetIsVoid(true); + return NS_OK; + } + + nsCOMPtr<nsISupports> entry; + rv = mEntries->GetNext(getter_AddRefs(entry)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIFile> file = do_QueryInterface(entry); + NS_ENSURE_TRUE(file, NS_ERROR_FAILURE); + + rv = file->GetLeafName(mCurrentFileName); + NS_ENSURE_SUCCESS(rv, rv); + + mRowId++; + + return NS_OK; +} + +int Connect(sqlite3* aDB, void* aAux, int aArgc, const char* const* aArgv, + sqlite3_vtab** aVtab, char** aErr) +{ + static const char virtualTableSchema[] = + "CREATE TABLE fs (" + "name TEXT, " + "path TEXT" + ")"; + + int rc = sqlite3_declare_vtab(aDB, virtualTableSchema); + if (rc != SQLITE_OK) { + return rc; + } + + sqlite3_vtab* vt = new sqlite3_vtab(); + memset(vt, 0, sizeof(*vt)); + + *aVtab = vt; + + return SQLITE_OK; +} + +int Disconnect(sqlite3_vtab* aVtab ) +{ + delete aVtab; + + return SQLITE_OK; +} + +int BestIndex(sqlite3_vtab* aVtab, sqlite3_index_info* aInfo) +{ + // Here we specify what index constraints we want to handle. That is, there + // might be some columns with particular constraints in which we can help + // SQLite narrow down the result set. + // + // For example, take the "path = x" where x is a directory. In this case, + // we can narrow our search to just this directory instead of the entire file + // system. This can be a significant optimization. So, we want to handle that + // constraint. To do so, we would look for two specific input conditions: + // + // 1. aInfo->aConstraint[i].iColumn == 1 + // 2. aInfo->aConstraint[i].op == SQLITE_INDEX_CONSTRAINT_EQ + // + // The first states that the path column is being used in one of the input + // constraints and the second states that the constraint involves the equal + // operator. + // + // An even more specific search would be for name='xxx', in which case we + // can limit the search to a single file, if it exists. + // + // What we have to do here is look for all of our index searches and select + // the narrowest. We can only pick one, so obviously we want the one that + // is the most specific, which leads to the smallest result set. + + for(int i = 0; i < aInfo->nConstraint; i++) { + if (aInfo->aConstraint[i].iColumn == 1 && aInfo->aConstraint[i].usable) { + if (aInfo->aConstraint[i].op & SQLITE_INDEX_CONSTRAINT_EQ) { + aInfo->aConstraintUsage[i].argvIndex = 1; + } + break; + } + + // TODO: handle single files (constrained also by the name column) + } + + return SQLITE_OK; +} + +int Open(sqlite3_vtab* aVtab, sqlite3_vtab_cursor** aCursor) +{ + VirtualTableCursor* cursor = new VirtualTableCursor(); + + *aCursor = reinterpret_cast<sqlite3_vtab_cursor*>(cursor); + + return SQLITE_OK; +} + +int Close(sqlite3_vtab_cursor* aCursor) +{ + VirtualTableCursor* cursor = reinterpret_cast<VirtualTableCursor*>(aCursor); + + delete cursor; + + return SQLITE_OK; +} + +int Filter(sqlite3_vtab_cursor* aCursor, int aIdxNum, const char* aIdxStr, + int aArgc, sqlite3_value** aArgv) +{ + VirtualTableCursor* cursor = reinterpret_cast<VirtualTableCursor*>(aCursor); + + if(aArgc <= 0) { + return SQLITE_OK; + } + + nsDependentString path( + reinterpret_cast<const char16_t*>(::sqlite3_value_text16(aArgv[0]))); + + nsresult rv = cursor->Init(path); + NS_ENSURE_SUCCESS(rv, SQLITE_ERROR); + + return SQLITE_OK; +} + +int Next(sqlite3_vtab_cursor* aCursor) +{ + VirtualTableCursor* cursor = reinterpret_cast<VirtualTableCursor*>(aCursor); + + nsresult rv = cursor->NextFile(); + NS_ENSURE_SUCCESS(rv, SQLITE_ERROR); + + return SQLITE_OK; +} + +int Eof(sqlite3_vtab_cursor* aCursor) +{ + VirtualTableCursor* cursor = reinterpret_cast<VirtualTableCursor*>(aCursor); + return cursor->CurrentFileName().IsVoid() ? 1 : 0; +} + +int Column(sqlite3_vtab_cursor* aCursor, sqlite3_context* aContext, + int aColumnIndex) +{ + VirtualTableCursor* cursor = reinterpret_cast<VirtualTableCursor*>(aCursor); + + switch (aColumnIndex) { + // name + case 0: { + const nsString& name = cursor->CurrentFileName(); + sqlite3_result_text16(aContext, name.get(), + name.Length() * sizeof(char16_t), + SQLITE_TRANSIENT); + break; + } + + // path + case 1: { + const nsString& path = cursor->DirectoryPath(); + sqlite3_result_text16(aContext, path.get(), + path.Length() * sizeof(char16_t), + SQLITE_TRANSIENT); + break; + } + default: + NS_NOTREACHED("Unsupported column!"); + } + + return SQLITE_OK; +} + +int RowId(sqlite3_vtab_cursor* aCursor, sqlite3_int64* aRowid) +{ + VirtualTableCursor* cursor = reinterpret_cast<VirtualTableCursor*>(aCursor); + + *aRowid = cursor->RowId(); + + return SQLITE_OK; +} + +} // namespace + +namespace mozilla { +namespace storage { + +int RegisterFileSystemModule(sqlite3* aDB, const char* aName) +{ + static sqlite3_module module = { + 1, + Connect, + Connect, + BestIndex, + Disconnect, + Disconnect, + Open, + Close, + Filter, + Next, + Eof, + Column, + RowId, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr + }; + + return sqlite3_create_module(aDB, aName, &module, nullptr); +} + +} // namespace storage +} // namespace mozilla diff --git a/storage/FileSystemModule.h b/storage/FileSystemModule.h new file mode 100644 index 000000000..40c3a77db --- /dev/null +++ b/storage/FileSystemModule.h @@ -0,0 +1,22 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_storage_FileSystemModule_h +#define mozilla_storage_FileSystemModule_h + +#include "nscore.h" + +struct sqlite3; + +namespace mozilla { +namespace storage { + +int RegisterFileSystemModule(sqlite3* aDB, const char* aName); + +} // namespace storage +} // namespace mozilla + +#endif // mozilla_storage_FileSystemModule_h diff --git a/storage/IStorageBindingParamsInternal.h b/storage/IStorageBindingParamsInternal.h new file mode 100644 index 000000000..e02778680 --- /dev/null +++ b/storage/IStorageBindingParamsInternal.h @@ -0,0 +1,51 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 sts=2 expandtab + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_storage_IStorageBindingParamsInternal_h_ +#define mozilla_storage_IStorageBindingParamsInternal_h_ + +#include "nsISupports.h" + +struct sqlite3_stmt; +class mozIStorageError; + +namespace mozilla { +namespace storage { + +#define ISTORAGEBINDINGPARAMSINTERNAL_IID \ + {0x4c43d33a, 0xc620, 0x41b8, {0xba, 0x1d, 0x50, 0xc5, 0xb1, 0xe9, 0x1a, 0x04}} + +/** + * Implementation-only interface for mozIStorageBindingParams. This defines the + * set of methods required by the asynchronous execution code in order to + * consume the contents stored in mozIStorageBindingParams instances. + */ +class IStorageBindingParamsInternal : public nsISupports +{ +public: + NS_DECLARE_STATIC_IID_ACCESSOR(ISTORAGEBINDINGPARAMSINTERNAL_IID) + + /** + * Binds our stored data to the statement. + * + * @param aStatement + * The statement to bind our data to. + * @return nullptr on success, or a mozIStorageError object if an error + * occurred. + */ + virtual already_AddRefed<mozIStorageError> bind(sqlite3_stmt *aStatement) = 0; +}; + +NS_DEFINE_STATIC_IID_ACCESSOR(IStorageBindingParamsInternal, + ISTORAGEBINDINGPARAMSINTERNAL_IID) + +#define NS_DECL_ISTORAGEBINDINGPARAMSINTERNAL \ + already_AddRefed<mozIStorageError> bind(sqlite3_stmt *aStatement) override; + +} // namespace storage +} // namespace mozilla + +#endif // mozilla_storage_IStorageBindingParamsInternal_h_ diff --git a/storage/SQLCollations.cpp b/storage/SQLCollations.cpp new file mode 100644 index 000000000..392bcd804 --- /dev/null +++ b/storage/SQLCollations.cpp @@ -0,0 +1,242 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/ArrayUtils.h" + +#include "SQLCollations.h" + +namespace mozilla { +namespace storage { + +//////////////////////////////////////////////////////////////////////////////// +//// Local Helper Functions + +namespace { + +/** + * Helper function for the UTF-8 locale collations. + * + * @param aService + * The Service that owns the nsICollation used by this collation. + * @param aLen1 + * The number of bytes in aStr1. + * @param aStr1 + * The string to be compared against aStr2 as provided by SQLite. It + * must be a non-null-terminated char* buffer. + * @param aLen2 + * The number of bytes in aStr2. + * @param aStr2 + * The string to be compared against aStr1 as provided by SQLite. It + * must be a non-null-terminated char* buffer. + * @param aComparisonStrength + * The sorting strength, one of the nsICollation constants. + * @return aStr1 - aStr2. That is, if aStr1 < aStr2, returns a negative number. + * If aStr1 > aStr2, returns a positive number. If aStr1 == aStr2, + * returns 0. + */ +int +localeCollationHelper8(void *aService, + int aLen1, + const void *aStr1, + int aLen2, + const void *aStr2, + int32_t aComparisonStrength) +{ + NS_ConvertUTF8toUTF16 str1(static_cast<const char *>(aStr1), aLen1); + NS_ConvertUTF8toUTF16 str2(static_cast<const char *>(aStr2), aLen2); + Service *serv = static_cast<Service *>(aService); + return serv->localeCompareStrings(str1, str2, aComparisonStrength); +} + +/** + * Helper function for the UTF-16 locale collations. + * + * @param aService + * The Service that owns the nsICollation used by this collation. + * @param aLen1 + * The number of bytes (not characters) in aStr1. + * @param aStr1 + * The string to be compared against aStr2 as provided by SQLite. It + * must be a non-null-terminated char16_t* buffer. + * @param aLen2 + * The number of bytes (not characters) in aStr2. + * @param aStr2 + * The string to be compared against aStr1 as provided by SQLite. It + * must be a non-null-terminated char16_t* buffer. + * @param aComparisonStrength + * The sorting strength, one of the nsICollation constants. + * @return aStr1 - aStr2. That is, if aStr1 < aStr2, returns a negative number. + * If aStr1 > aStr2, returns a positive number. If aStr1 == aStr2, + * returns 0. + */ +int +localeCollationHelper16(void *aService, + int aLen1, + const void *aStr1, + int aLen2, + const void *aStr2, + int32_t aComparisonStrength) +{ + const char16_t *buf1 = static_cast<const char16_t *>(aStr1); + const char16_t *buf2 = static_cast<const char16_t *>(aStr2); + + // The second argument to the nsDependentSubstring constructor is exclusive: + // It points to the char16_t immediately following the last one in the target + // substring. Since aLen1 and aLen2 are in bytes, divide by sizeof(char16_t) + // so that the pointer arithmetic is correct. + nsDependentSubstring str1(buf1, buf1 + (aLen1 / sizeof(char16_t))); + nsDependentSubstring str2(buf2, buf2 + (aLen2 / sizeof(char16_t))); + Service *serv = static_cast<Service *>(aService); + return serv->localeCompareStrings(str1, str2, aComparisonStrength); +} + +// This struct is used only by registerCollations below, but ISO C++98 forbids +// instantiating a template dependent on a locally-defined type. Boo-urns! +struct Collations { + const char *zName; + int enc; + int(*xCompare)(void*, int, const void*, int, const void*); +}; + +} // namespace + +//////////////////////////////////////////////////////////////////////////////// +//// Exposed Functions + +int +registerCollations(sqlite3 *aDB, + Service *aService) +{ + Collations collations[] = { + {"locale", + SQLITE_UTF8, + localeCollation8}, + {"locale_case_sensitive", + SQLITE_UTF8, + localeCollationCaseSensitive8}, + {"locale_accent_sensitive", + SQLITE_UTF8, + localeCollationAccentSensitive8}, + {"locale_case_accent_sensitive", + SQLITE_UTF8, + localeCollationCaseAccentSensitive8}, + {"locale", + SQLITE_UTF16, + localeCollation16}, + {"locale_case_sensitive", + SQLITE_UTF16, + localeCollationCaseSensitive16}, + {"locale_accent_sensitive", + SQLITE_UTF16, + localeCollationAccentSensitive16}, + {"locale_case_accent_sensitive", + SQLITE_UTF16, + localeCollationCaseAccentSensitive16}, + }; + + int rv = SQLITE_OK; + for (size_t i = 0; SQLITE_OK == rv && i < ArrayLength(collations); ++i) { + struct Collations *p = &collations[i]; + rv = ::sqlite3_create_collation(aDB, p->zName, p->enc, aService, + p->xCompare); + } + + return rv; +} + +//////////////////////////////////////////////////////////////////////////////// +//// SQL Collations + +int +localeCollation8(void *aService, + int aLen1, + const void *aStr1, + int aLen2, + const void *aStr2) +{ + return localeCollationHelper8(aService, aLen1, aStr1, aLen2, aStr2, + nsICollation::kCollationCaseInSensitive); +} + +int +localeCollationCaseSensitive8(void *aService, + int aLen1, + const void *aStr1, + int aLen2, + const void *aStr2) +{ + return localeCollationHelper8(aService, aLen1, aStr1, aLen2, aStr2, + nsICollation::kCollationAccentInsenstive); +} + +int +localeCollationAccentSensitive8(void *aService, + int aLen1, + const void *aStr1, + int aLen2, + const void *aStr2) +{ + return localeCollationHelper8(aService, aLen1, aStr1, aLen2, aStr2, + nsICollation::kCollationCaseInsensitiveAscii); +} + +int +localeCollationCaseAccentSensitive8(void *aService, + int aLen1, + const void *aStr1, + int aLen2, + const void *aStr2) +{ + return localeCollationHelper8(aService, aLen1, aStr1, aLen2, aStr2, + nsICollation::kCollationCaseSensitive); +} + +int +localeCollation16(void *aService, + int aLen1, + const void *aStr1, + int aLen2, + const void *aStr2) +{ + return localeCollationHelper16(aService, aLen1, aStr1, aLen2, aStr2, + nsICollation::kCollationCaseInSensitive); +} + +int +localeCollationCaseSensitive16(void *aService, + int aLen1, + const void *aStr1, + int aLen2, + const void *aStr2) +{ + return localeCollationHelper16(aService, aLen1, aStr1, aLen2, aStr2, + nsICollation::kCollationAccentInsenstive); +} + +int +localeCollationAccentSensitive16(void *aService, + int aLen1, + const void *aStr1, + int aLen2, + const void *aStr2) +{ + return localeCollationHelper16(aService, aLen1, aStr1, aLen2, aStr2, + nsICollation::kCollationCaseInsensitiveAscii); +} + +int +localeCollationCaseAccentSensitive16(void *aService, + int aLen1, + const void *aStr1, + int aLen2, + const void *aStr2) +{ + return localeCollationHelper16(aService, aLen1, aStr1, aLen2, aStr2, + nsICollation::kCollationCaseSensitive); +} + +} // namespace storage +} // namespace mozilla diff --git a/storage/SQLCollations.h b/storage/SQLCollations.h new file mode 100644 index 000000000..d6d0d4562 --- /dev/null +++ b/storage/SQLCollations.h @@ -0,0 +1,249 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_storage_SQLCollations_h +#define mozilla_storage_SQLCollations_h + +#include "mozStorageService.h" +#include "nscore.h" +#include "nsString.h" + +#include "sqlite3.h" + +namespace mozilla { +namespace storage { + +/** + * Registers the collating sequences declared here with the specified + * database and Service. + * + * @param aDB + * The database we'll be registering the collations with. + * @param aService + * The Service that owns the nsICollation used by our collations. + * @return the SQLite status code indicating success or failure. + */ +int registerCollations(sqlite3 *aDB, Service *aService); + +//////////////////////////////////////////////////////////////////////////////// +//// Predefined Functions + +/** + * Custom UTF-8 collating sequence that respects the application's locale. + * Comparison is case- and accent-insensitive. This is called by SQLite. + * + * @param aService + * The Service that owns the nsICollation used by this collation. + * @param aLen1 + * The number of bytes in aStr1. + * @param aStr1 + * The string to be compared against aStr2. It will be passed in by + * SQLite as a non-null-terminated char* buffer. + * @param aLen2 + * The number of bytes in aStr2. + * @param aStr2 + * The string to be compared against aStr1. It will be passed in by + * SQLite as a non-null-terminated char* buffer. + * @return aStr1 - aStr2. That is, if aStr1 < aStr2, returns a negative number. + * If aStr1 > aStr2, returns a positive number. If aStr1 == aStr2, + * returns 0. + */ +int localeCollation8(void *aService, + int aLen1, + const void *aStr1, + int aLen2, + const void *aStr2); + +/** + * Custom UTF-8 collating sequence that respects the application's locale. + * Comparison is case-sensitive and accent-insensitive. This is called by + * SQLite. + * + * @param aService + * The Service that owns the nsICollation used by this collation. + * @param aLen1 + * The number of bytes in aStr1. + * @param aStr1 + * The string to be compared against aStr2. It will be passed in by + * SQLite as a non-null-terminated char* buffer. + * @param aLen2 + * The number of bytes in aStr2. + * @param aStr2 + * The string to be compared against aStr1. It will be passed in by + * SQLite as a non-null-terminated char* buffer. + * @return aStr1 - aStr2. That is, if aStr1 < aStr2, returns a negative number. + * If aStr1 > aStr2, returns a positive number. If aStr1 == aStr2, + * returns 0. + */ +int localeCollationCaseSensitive8(void *aService, + int aLen1, + const void *aStr1, + int aLen2, + const void *aStr2); + +/** + * Custom UTF-8 collating sequence that respects the application's locale. + * Comparison is case-insensitive and accent-sensitive. This is called by + * SQLite. + * + * @param aService + * The Service that owns the nsICollation used by this collation. + * @param aLen1 + * The number of bytes in aStr1. + * @param aStr1 + * The string to be compared against aStr2. It will be passed in by + * SQLite as a non-null-terminated char* buffer. + * @param aLen2 + * The number of bytes in aStr2. + * @param aStr2 + * The string to be compared against aStr1. It will be passed in by + * SQLite as a non-null-terminated char* buffer. + * @return aStr1 - aStr2. That is, if aStr1 < aStr2, returns a negative number. + * If aStr1 > aStr2, returns a positive number. If aStr1 == aStr2, + * returns 0. + */ +int localeCollationAccentSensitive8(void *aService, + int aLen1, + const void *aStr1, + int aLen2, + const void *aStr2); + +/** + * Custom UTF-8 collating sequence that respects the application's locale. + * Comparison is case- and accent-sensitive. This is called by SQLite. + * + * @param aService + * The Service that owns the nsICollation used by this collation. + * @param aLen1 + * The number of bytes in aStr1. + * @param aStr1 + * The string to be compared against aStr2. It will be passed in by + * SQLite as a non-null-terminated char* buffer. + * @param aLen2 + * The number of bytes in aStr2. + * @param aStr2 + * The string to be compared against aStr1. It will be passed in by + * SQLite as a non-null-terminated char* buffer. + * @return aStr1 - aStr2. That is, if aStr1 < aStr2, returns a negative number. + * If aStr1 > aStr2, returns a positive number. If aStr1 == aStr2, + * returns 0. + */ +int localeCollationCaseAccentSensitive8(void *aService, + int aLen1, + const void *aStr1, + int aLen2, + const void *aStr2); + +/** + * Custom UTF-16 collating sequence that respects the application's locale. + * Comparison is case- and accent-insensitive. This is called by SQLite. + * + * @param aService + * The Service that owns the nsICollation used by this collation. + * @param aLen1 + * The number of bytes (not characters) in aStr1. + * @param aStr1 + * The string to be compared against aStr2. It will be passed in by + * SQLite as a non-null-terminated char16_t* buffer. + * @param aLen2 + * The number of bytes (not characters) in aStr2. + * @param aStr2 + * The string to be compared against aStr1. It will be passed in by + * SQLite as a non-null-terminated char16_t* buffer. + * @return aStr1 - aStr2. That is, if aStr1 < aStr2, returns a negative number. + * If aStr1 > aStr2, returns a positive number. If aStr1 == aStr2, + * returns 0. + */ +int localeCollation16(void *aService, + int aLen1, + const void *aStr1, + int aLen2, + const void *aStr2); + +/** + * Custom UTF-16 collating sequence that respects the application's locale. + * Comparison is case-sensitive and accent-insensitive. This is called by + * SQLite. + * + * @param aService + * The Service that owns the nsICollation used by this collation. + * @param aLen1 + * The number of bytes (not characters) in aStr1. + * @param aStr1 + * The string to be compared against aStr2. It will be passed in by + * SQLite as a non-null-terminated char16_t* buffer. + * @param aLen2 + * The number of bytes (not characters) in aStr2. + * @param aStr2 + * The string to be compared against aStr1. It will be passed in by + * SQLite as a non-null-terminated char16_t* buffer. + * @return aStr1 - aStr2. That is, if aStr1 < aStr2, returns a negative number. + * If aStr1 > aStr2, returns a positive number. If aStr1 == aStr2, + * returns 0. + */ +int localeCollationCaseSensitive16(void *aService, + int aLen1, + const void *aStr1, + int aLen2, + const void *aStr2); + +/** + * Custom UTF-16 collating sequence that respects the application's locale. + * Comparison is case-insensitive and accent-sensitive. This is called by + * SQLite. + * + * @param aService + * The Service that owns the nsICollation used by this collation. + * @param aLen1 + * The number of bytes (not characters) in aStr1. + * @param aStr1 + * The string to be compared against aStr2. It will be passed in by + * SQLite as a non-null-terminated char16_t* buffer. + * @param aLen2 + * The number of bytes (not characters) in aStr2. + * @param aStr2 + * The string to be compared against aStr1. It will be passed in by + * SQLite as a non-null-terminated char16_t* buffer. + * @return aStr1 - aStr2. That is, if aStr1 < aStr2, returns a negative number. + * If aStr1 > aStr2, returns a positive number. If aStr1 == aStr2, + * returns 0. + */ +int localeCollationAccentSensitive16(void *aService, + int aLen1, + const void *aStr1, + int aLen2, + const void *aStr2); + +/** + * Custom UTF-16 collating sequence that respects the application's locale. + * Comparison is case- and accent-sensitive. This is called by SQLite. + * + * @param aService + * The Service that owns the nsICollation used by this collation. + * @param aLen1 + * The number of bytes (not characters) in aStr1. + * @param aStr1 + * The string to be compared against aStr2. It will be passed in by + * SQLite as a non-null-terminated char16_t* buffer. + * @param aLen2 + * The number of bytes (not characters) in aStr2. + * @param aStr2 + * The string to be compared against aStr1. It will be passed in by + * SQLite as a non-null-terminated char16_t* buffer. + * @return aStr1 - aStr2. That is, if aStr1 < aStr2, returns a negative number. + * If aStr1 > aStr2, returns a positive number. If aStr1 == aStr2, + * returns 0. + */ +int localeCollationCaseAccentSensitive16(void *aService, + int aLen1, + const void *aStr1, + int aLen2, + const void *aStr2); + +} // namespace storage +} // namespace mozilla + +#endif // mozilla_storage_SQLCollations_h diff --git a/storage/SQLiteMutex.h b/storage/SQLiteMutex.h new file mode 100644 index 000000000..a38525fd6 --- /dev/null +++ b/storage/SQLiteMutex.h @@ -0,0 +1,173 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_storage_SQLiteMutex_h_ +#define mozilla_storage_SQLiteMutex_h_ + +#include "mozilla/BlockingResourceBase.h" +#include "sqlite3.h" + +namespace mozilla { +namespace storage { + +/** + * Wrapper class for sqlite3_mutexes. To be used whenever we want to use a + * sqlite3_mutex. + * + * @warning Never EVER wrap the same sqlite3_mutex with a different SQLiteMutex. + * If you do this, you void the deadlock detector's warranty! + */ +class SQLiteMutex : private BlockingResourceBase +{ +public: + /** + * Constructs a wrapper for a sqlite3_mutex that has deadlock detecting. + * + * @param aName + * A name which can be used to reference this mutex. + */ + explicit SQLiteMutex(const char *aName) + : BlockingResourceBase(aName, eMutex) + , mMutex(nullptr) + { + } + + /** + * Sets the mutex that we are wrapping. We generally do not have access to + * our mutex at class construction, so we have to set it once we get access to + * it. + * + * @param aMutex + * The sqlite3_mutex that we are going to wrap. + */ + void initWithMutex(sqlite3_mutex *aMutex) + { + NS_ASSERTION(aMutex, "You must pass in a valid mutex!"); + NS_ASSERTION(!mMutex, "A mutex has already been set for this!"); + mMutex = aMutex; + } + +#if !defined(DEBUG) || defined(MOZ_SYSTEM_SQLITE) + /** + * Acquires the mutex. + */ + void lock() + { + sqlite3_mutex_enter(mMutex); + } + + /** + * Releases the mutex. + */ + void unlock() + { + sqlite3_mutex_leave(mMutex); + } + + /** + * Asserts that the current thread owns the mutex. + */ + void assertCurrentThreadOwns() + { + } + + /** + * Asserts that the current thread does not own the mutex. + */ + void assertNotCurrentThreadOwns() + { + } + +#else + void lock() + { + NS_ASSERTION(mMutex, "No mutex associated with this wrapper!"); + + // While SQLite Mutexes may be recursive, in our own code we do not want to + // treat them as such. + + CheckAcquire(); + sqlite3_mutex_enter(mMutex); + Acquire(); // Call is protected by us holding the mutex. + } + + void unlock() + { + NS_ASSERTION(mMutex, "No mutex associated with this wrapper!"); + + // While SQLite Mutexes may be recursive, in our own code we do not want to + // treat them as such. + Release(); // Call is protected by us holding the mutex. + sqlite3_mutex_leave(mMutex); + } + + void assertCurrentThreadOwns() + { + NS_ASSERTION(mMutex, "No mutex associated with this wrapper!"); + NS_ASSERTION(sqlite3_mutex_held(mMutex), + "Mutex is not held, but we expect it to be!"); + } + + void assertNotCurrentThreadOwns() + { + NS_ASSERTION(mMutex, "No mutex associated with this wrapper!"); + NS_ASSERTION(sqlite3_mutex_notheld(mMutex), + "Mutex is held, but we expect it to not be!"); + } +#endif // ifndef DEBUG + +private: + sqlite3_mutex *mMutex; +}; + +/** + * Automatically acquires the mutex when it enters scope, and releases it when + * it leaves scope. + */ +class MOZ_STACK_CLASS SQLiteMutexAutoLock +{ +public: + explicit SQLiteMutexAutoLock(SQLiteMutex &aMutex) + : mMutex(aMutex) + { + mMutex.lock(); + } + + ~SQLiteMutexAutoLock() + { + mMutex.unlock(); + } + +private: + SQLiteMutex &mMutex; +}; + +/** + * Automatically releases the mutex when it enters scope, and acquires it when + * it leaves scope. + */ +class MOZ_STACK_CLASS SQLiteMutexAutoUnlock +{ +public: + explicit SQLiteMutexAutoUnlock(SQLiteMutex &aMutex) + : mMutex(aMutex) + { + mMutex.unlock(); + } + + ~SQLiteMutexAutoUnlock() + { + mMutex.lock(); + } + +private: + SQLiteMutex &mMutex; +}; + +} // namespace storage +} // namespace mozilla + +#endif // mozilla_storage_SQLiteMutex_h_ diff --git a/storage/StatementCache.h b/storage/StatementCache.h new file mode 100644 index 000000000..ed7714799 --- /dev/null +++ b/storage/StatementCache.h @@ -0,0 +1,141 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_storage_StatementCache_h +#define mozilla_storage_StatementCache_h + +#include "mozIStorageConnection.h" +#include "mozIStorageStatement.h" +#include "mozIStorageAsyncStatement.h" + +#include "nsAutoPtr.h" +#include "nsHashKeys.h" +#include "nsInterfaceHashtable.h" + +namespace mozilla { +namespace storage { + +/** + * Class used to cache statements (mozIStorageStatement or + * mozIStorageAsyncStatement). + */ +template<typename StatementType> +class StatementCache { +public: + /** + * Constructor for the cache. + * + * @note a connection can have more than one cache. + * + * @param aConnection + * A reference to the nsCOMPtr for the connection this cache is to be + * used for. This nsCOMPtr must at least live as long as this class, + * otherwise crashes will happen. + */ + explicit StatementCache(nsCOMPtr<mozIStorageConnection>& aConnection) + : mConnection(aConnection) + { + } + + /** + * Obtains a cached statement. If this statement is not yet created, it will + * be created and stored for later use. + * + * @param aQuery + * The SQL string (either a const char [] or nsACString) to get a + * cached query for. + * @return the cached statement, or null upon error. + */ + inline + already_AddRefed<StatementType> + GetCachedStatement(const nsACString& aQuery) + { + nsCOMPtr<StatementType> stmt; + if (!mCachedStatements.Get(aQuery, getter_AddRefs(stmt))) { + stmt = CreateStatement(aQuery); + NS_ENSURE_TRUE(stmt, nullptr); + + mCachedStatements.Put(aQuery, stmt); + } + return stmt.forget(); + } + + template<int N> + MOZ_ALWAYS_INLINE already_AddRefed<StatementType> + GetCachedStatement(const char (&aQuery)[N]) + { + nsDependentCString query(aQuery, N - 1); + return GetCachedStatement(query); + } + + /** + * Finalizes all cached statements so the database can be safely closed. The + * behavior of this cache is unspecified after this method is called. + */ + inline + void + FinalizeStatements() + { + for (auto iter = mCachedStatements.Iter(); !iter.Done(); iter.Next()) { + (void)iter.Data()->Finalize(); + } + + // Clear the cache at this time too! + (void)mCachedStatements.Clear(); + } + +private: + inline + already_AddRefed<StatementType> + CreateStatement(const nsACString& aQuery); + + nsInterfaceHashtable<nsCStringHashKey, StatementType> mCachedStatements; + nsCOMPtr<mozIStorageConnection>& mConnection; +}; + +template< > +inline +already_AddRefed<mozIStorageStatement> +StatementCache<mozIStorageStatement>::CreateStatement(const nsACString& aQuery) +{ + NS_ENSURE_TRUE(mConnection, nullptr); + + nsCOMPtr<mozIStorageStatement> stmt; + nsresult rv = mConnection->CreateStatement(aQuery, getter_AddRefs(stmt)); + if (NS_FAILED(rv)) { + nsCString error; + error.AppendLiteral("The statement '"); + error.Append(aQuery); + error.AppendLiteral("' failed to compile with the error message '"); + nsCString msg; + (void)mConnection->GetLastErrorString(msg); + error.Append(msg); + error.AppendLiteral("'."); + NS_ERROR(error.get()); + } + NS_ENSURE_SUCCESS(rv, nullptr); + + return stmt.forget(); +} + +template< > +inline +already_AddRefed<mozIStorageAsyncStatement> +StatementCache<mozIStorageAsyncStatement>::CreateStatement(const nsACString& aQuery) +{ + NS_ENSURE_TRUE(mConnection, nullptr); + + nsCOMPtr<mozIStorageAsyncStatement> stmt; + nsresult rv = mConnection->CreateAsyncStatement(aQuery, getter_AddRefs(stmt)); + NS_ENSURE_SUCCESS(rv, nullptr); + + return stmt.forget(); +} + +} // namespace storage +} // namespace mozilla + +#endif // mozilla_storage_StatementCache_h diff --git a/storage/StorageBaseStatementInternal.cpp b/storage/StorageBaseStatementInternal.cpp new file mode 100644 index 000000000..d6545fcb4 --- /dev/null +++ b/storage/StorageBaseStatementInternal.cpp @@ -0,0 +1,221 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 sts=2 expandtab + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "StorageBaseStatementInternal.h" + +#include "nsProxyRelease.h" + +#include "mozStorageBindingParamsArray.h" +#include "mozStorageStatementData.h" +#include "mozStorageAsyncStatementExecution.h" + +namespace mozilla { +namespace storage { + +//////////////////////////////////////////////////////////////////////////////// +//// Local Classes + +/** + * Used to finalize an asynchronous statement on the background thread. + */ +class AsyncStatementFinalizer : public Runnable +{ +public: + /** + * Constructor for the event. + * + * @param aStatement + * We need the AsyncStatement to be able to get at the sqlite3_stmt; + * we only access/create it on the async thread. + * @param aConnection + * We need the connection to know what thread to release the statement + * on. We release the statement on that thread since releasing the + * statement might end up releasing the connection too. + */ + AsyncStatementFinalizer(StorageBaseStatementInternal *aStatement, + Connection *aConnection) + : mStatement(aStatement) + , mConnection(aConnection) + { + } + + NS_IMETHOD Run() override + { + if (mStatement->mAsyncStatement) { + sqlite3_finalize(mStatement->mAsyncStatement); + mStatement->mAsyncStatement = nullptr; + } + + nsCOMPtr<nsIThread> targetThread(mConnection->threadOpenedOn); + NS_ProxyRelease(targetThread, mStatement.forget()); + return NS_OK; + } +private: + RefPtr<StorageBaseStatementInternal> mStatement; + RefPtr<Connection> mConnection; +}; + +/** + * Finalize a sqlite3_stmt on the background thread for a statement whose + * destructor was invoked and the statement was non-null. + */ +class LastDitchSqliteStatementFinalizer : public Runnable +{ +public: + /** + * Event constructor. + * + * @param aConnection + * Used to keep the connection alive. If we failed to do this, it + * is possible that the statement going out of scope invoking us + * might have the last reference to the connection and so trigger + * an attempt to close the connection which is doomed to fail + * (because the asynchronous execution thread must exist which will + * trigger the failure case). + * @param aStatement + * The sqlite3_stmt to finalize. This object takes ownership / + * responsibility for the instance and all other references to it + * should be forgotten. + */ + LastDitchSqliteStatementFinalizer(RefPtr<Connection> &aConnection, + sqlite3_stmt *aStatement) + : mConnection(aConnection) + , mAsyncStatement(aStatement) + { + NS_PRECONDITION(aConnection, "You must provide a Connection"); + } + + NS_IMETHOD Run() override + { + (void)::sqlite3_finalize(mAsyncStatement); + mAsyncStatement = nullptr; + + nsCOMPtr<nsIThread> target(mConnection->threadOpenedOn); + (void)::NS_ProxyRelease(target, mConnection.forget()); + return NS_OK; + } +private: + RefPtr<Connection> mConnection; + sqlite3_stmt *mAsyncStatement; +}; + +//////////////////////////////////////////////////////////////////////////////// +//// StorageBaseStatementInternal + +StorageBaseStatementInternal::StorageBaseStatementInternal() +: mAsyncStatement(nullptr) +{ +} + +void +StorageBaseStatementInternal::asyncFinalize() +{ + nsIEventTarget *target = mDBConnection->getAsyncExecutionTarget(); + if (target) { + // Attempt to finalize asynchronously + nsCOMPtr<nsIRunnable> event = + new AsyncStatementFinalizer(this, mDBConnection); + + // Dispatch. Note that dispatching can fail, typically if + // we have a race condition with asyncClose(). It's ok, + // let asyncClose() win. + (void)target->Dispatch(event, NS_DISPATCH_NORMAL); + } + // If we cannot get the background thread, + // mozStorageConnection::AsyncClose() has already been called and + // the statement either has been or will be cleaned up by + // internalClose(). +} + +void +StorageBaseStatementInternal::destructorAsyncFinalize() +{ + if (!mAsyncStatement) + return; + + // If we reach this point, our owner has not finalized this + // statement, yet we are being destructed. If possible, we want to + // auto-finalize it early, to release the resources early. + nsIEventTarget *target = mDBConnection->getAsyncExecutionTarget(); + if (target) { + // If we can get the async execution target, we can indeed finalize + // the statement, as the connection is still open. + bool isAsyncThread = false; + (void)target->IsOnCurrentThread(&isAsyncThread); + + nsCOMPtr<nsIRunnable> event = + new LastDitchSqliteStatementFinalizer(mDBConnection, mAsyncStatement); + if (isAsyncThread) { + (void)event->Run(); + } else { + (void)target->Dispatch(event, NS_DISPATCH_NORMAL); + } + } + + // We might not be able to dispatch to the background thread, + // presumably because it is being shutdown. Since said shutdown will + // finalize the statement, we just need to clean-up around here. + mAsyncStatement = nullptr; +} + +NS_IMETHODIMP +StorageBaseStatementInternal::NewBindingParamsArray( + mozIStorageBindingParamsArray **_array +) +{ + nsCOMPtr<mozIStorageBindingParamsArray> array = new BindingParamsArray(this); + NS_ENSURE_TRUE(array, NS_ERROR_OUT_OF_MEMORY); + + array.forget(_array); + return NS_OK; +} + +NS_IMETHODIMP +StorageBaseStatementInternal::ExecuteAsync( + mozIStorageStatementCallback *aCallback, + mozIStoragePendingStatement **_stmt +) +{ + // We used to call Connection::ExecuteAsync but it takes a + // mozIStorageBaseStatement signature because it is also a public API. Since + // our 'this' has no static concept of mozIStorageBaseStatement and Connection + // would just QI it back across to a StorageBaseStatementInternal and the + // actual logic is very simple, we now roll our own. + nsTArray<StatementData> stmts(1); + StatementData data; + nsresult rv = getAsynchronousStatementData(data); + NS_ENSURE_SUCCESS(rv, rv); + NS_ENSURE_TRUE(stmts.AppendElement(data), NS_ERROR_OUT_OF_MEMORY); + + // Dispatch to the background + return AsyncExecuteStatements::execute(stmts, mDBConnection, + mNativeConnection, aCallback, _stmt); +} + +NS_IMETHODIMP +StorageBaseStatementInternal::EscapeStringForLIKE( + const nsAString &aValue, + const char16_t aEscapeChar, + nsAString &_escapedString +) +{ + const char16_t MATCH_ALL('%'); + const char16_t MATCH_ONE('_'); + + _escapedString.Truncate(0); + + for (uint32_t i = 0; i < aValue.Length(); i++) { + if (aValue[i] == aEscapeChar || aValue[i] == MATCH_ALL || + aValue[i] == MATCH_ONE) { + _escapedString += aEscapeChar; + } + _escapedString += aValue[i]; + } + return NS_OK; +} + +} // namespace storage +} // namespace mozilla diff --git a/storage/StorageBaseStatementInternal.h b/storage/StorageBaseStatementInternal.h new file mode 100644 index 000000000..97e68e6b5 --- /dev/null +++ b/storage/StorageBaseStatementInternal.h @@ -0,0 +1,353 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 sts=2 expandtab + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_storage_StorageBaseStatementInternal_h_ +#define mozilla_storage_StorageBaseStatementInternal_h_ + +#include "nsISupports.h" +#include "nsCOMPtr.h" +#include "nsAutoPtr.h" + +struct sqlite3; +struct sqlite3_stmt; +class mozIStorageBindingParamsArray; +class mozIStorageBindingParams; +class mozIStorageStatementCallback; +class mozIStoragePendingStatement; + +namespace mozilla { +namespace storage { + +#define STORAGEBASESTATEMENTINTERNAL_IID \ + {0xd18856c9, 0xbf07, 0x4ae2, {0x94, 0x5b, 0x1a, 0xdd, 0x49, 0x19, 0x55, 0x2a}} + +class Connection; +class StatementData; + +class AsyncStatementFinalizer; + +/** + * Implementation-only interface and shared logix mix-in corresponding to + * mozIStorageBaseStatement. Both Statement and AsyncStatement inherit from + * this. The interface aspect makes them look the same to implementation innards + * that aren't publicly accessible. The mix-in avoids code duplication in + * common implementations of mozIStorageBaseStatement, albeit with some minor + * performance/space overhead because we have to use defines to officially + * implement the methods on Statement/AsyncStatement (and proxy to this base + * class.) + */ +class StorageBaseStatementInternal : public nsISupports +{ +public: + NS_DECLARE_STATIC_IID_ACCESSOR(STORAGEBASESTATEMENTINTERNAL_IID) + + /** + * @return the connection that this statement belongs to. + */ + Connection *getOwner() + { + return mDBConnection; + } + + /** + * Return the asynchronous statement, creating it if required. + * + * This is for use by the asynchronous execution code for StatementData + * created by AsyncStatements. Statement internally uses this method to + * prepopulate StatementData with the sqlite3_stmt. + * + * @param[out] stmt + * The sqlite3_stmt for asynchronous use. + * @return The SQLite result code for creating the statement if created, + * SQLITE_OK if creation was not required. + */ + virtual int getAsyncStatement(sqlite3_stmt **_stmt) = 0; + + /** + * Obtains the StatementData needed for asynchronous execution. + * + * This is for use by Connection to retrieve StatementData from statements + * when executeAsync is invoked. + * + * @param[out] _data + * A reference to a StatementData object that will be populated + * upon successful execution of this method. + * @return NS_OK if we were able to assemble the data, failure otherwise. + */ + virtual nsresult getAsynchronousStatementData(StatementData &_data) = 0; + + /** + * Construct a new BindingParams to be owned by the provided binding params + * array. This method exists so that BindingParamsArray does not need + * factory logic to determine what type of BindingParams to instantiate. + * + * @param aOwner + * The binding params array to own the newly created binding params. + * @return The new mozIStorageBindingParams instance appropriate to the + * underlying statement type. + */ + virtual already_AddRefed<mozIStorageBindingParams> newBindingParams( + mozIStorageBindingParamsArray *aOwner + ) = 0; + +protected: // mix-in bits are protected + StorageBaseStatementInternal(); + + RefPtr<Connection> mDBConnection; + sqlite3 *mNativeConnection; + + /** + * Our asynchronous statement. + * + * For Statement this is populated by the first invocation to + * getAsyncStatement. + * + * For AsyncStatement, this is null at creation time and initialized by the + * async thread when it calls getAsyncStatement the first time the statement + * is executed. (Or in the event of badly formed SQL, every time.) + */ + sqlite3_stmt *mAsyncStatement; + + /** + * Initiate asynchronous finalization by dispatching an event to the + * asynchronous thread to finalize mAsyncStatement. This acquires a reference + * to this statement and proxies it back to the connection's owning thread + * for release purposes. + * + * In the event the asynchronous thread is already gone or we otherwise fail + * to dispatch an event to it we failover to invoking internalAsyncFinalize + * directly. (That's what the asynchronous finalizer would have called.) + * + * @note You must not call this method from your destructor because its + * operation assumes we are still alive. Call internalAsyncFinalize + * directly in that case. + */ + void asyncFinalize(); + + /** + * Cleanup the async sqlite3_stmt stored in mAsyncStatement if it exists by + * attempting to dispatch to the asynchronous thread if available, finalizing + * on this thread if it is not. + * + * @note Call this from your destructor, call asyncFinalize otherwise. + */ + void destructorAsyncFinalize(); + + NS_IMETHOD NewBindingParamsArray(mozIStorageBindingParamsArray **_array); + NS_IMETHOD ExecuteAsync(mozIStorageStatementCallback *aCallback, + mozIStoragePendingStatement **_stmt); + NS_IMETHOD EscapeStringForLIKE(const nsAString &aValue, + char16_t aEscapeChar, + nsAString &_escapedString); + + // Needs access to internalAsyncFinalize + friend class AsyncStatementFinalizer; +}; + +NS_DEFINE_STATIC_IID_ACCESSOR(StorageBaseStatementInternal, + STORAGEBASESTATEMENTINTERNAL_IID) + +#define NS_DECL_STORAGEBASESTATEMENTINTERNAL \ + virtual Connection *getOwner(); \ + virtual int getAsyncStatement(sqlite3_stmt **_stmt) override; \ + virtual nsresult getAsynchronousStatementData(StatementData &_data) override; \ + virtual already_AddRefed<mozIStorageBindingParams> newBindingParams( \ + mozIStorageBindingParamsArray *aOwner) override; + +/** + * Helper macro to implement the proxying implementations. Because we are + * implementing methods that are part of mozIStorageBaseStatement and the + * implementation classes already use NS_DECL_MOZISTORAGEBASESTATEMENT we don't + * need to provide declaration support. + */ +#define MIX_IMPL(_class, _optionalGuard, _method, _declArgs, _invokeArgs) \ + NS_IMETHODIMP _class::_method _declArgs \ + { \ + _optionalGuard \ + return StorageBaseStatementInternal::_method _invokeArgs; \ + } + + +/** + * Define proxying implementation for the given _class. If a state invariant + * needs to be checked and an early return possibly performed, pass the clause + * to use as _optionalGuard. + */ +#define MIXIN_IMPL_STORAGEBASESTATEMENTINTERNAL(_class, _optionalGuard) \ + MIX_IMPL(_class, _optionalGuard, \ + NewBindingParamsArray, \ + (mozIStorageBindingParamsArray **_array), \ + (_array)) \ + MIX_IMPL(_class, _optionalGuard, \ + ExecuteAsync, \ + (mozIStorageStatementCallback *aCallback, \ + mozIStoragePendingStatement **_stmt), \ + (aCallback, _stmt)) \ + MIX_IMPL(_class, _optionalGuard, \ + EscapeStringForLIKE, \ + (const nsAString &aValue, char16_t aEscapeChar, \ + nsAString &_escapedString), \ + (aValue, aEscapeChar, _escapedString)) + +/** + * Name-building helper for BIND_GEN_IMPL. + */ +#define BIND_NAME_CONCAT(_nameBit, _concatBit) \ + Bind##_nameBit##_concatBit + +/** + * We have type-specific convenience methods for C++ implementations in + * 3 different forms; 2 by index, 1 by name. The following macro allows + * us to avoid having to define repetitive things by hand. + * + * Because of limitations of macros and our desire to avoid requiring special + * permutations for the null and blob cases (whose argument count varies), + * we require that the argument declarations and corresponding invocation + * usages are passed in. + * + * @param _class + * The class name. + * @param _guard + * The guard clause to inject. + * @param _declName + * The argument list (with parens) for the ByName variants. + * @param _declIndex + * The argument list (with parens) for the index variants. + * @param _invArgs + * The invocation argumment list. + */ +#define BIND_GEN_IMPL(_class, _guard, _name, _declName, _declIndex, _invArgs) \ + NS_IMETHODIMP _class::BIND_NAME_CONCAT(_name, ByName) _declName \ + { \ + _guard \ + mozIStorageBindingParams *params = getParams(); \ + NS_ENSURE_TRUE(params, NS_ERROR_OUT_OF_MEMORY); \ + return params->BIND_NAME_CONCAT(_name, ByName) _invArgs; \ + } \ + NS_IMETHODIMP _class::BIND_NAME_CONCAT(_name, ByIndex) _declIndex \ + { \ + _guard \ + mozIStorageBindingParams *params = getParams(); \ + NS_ENSURE_TRUE(params, NS_ERROR_OUT_OF_MEMORY); \ + return params->BIND_NAME_CONCAT(_name, ByIndex) _invArgs; \ + } \ + NS_IMETHODIMP _class::BIND_NAME_CONCAT(_name, Parameter) _declIndex \ + { \ + _guard \ + mozIStorageBindingParams *params = getParams(); \ + NS_ENSURE_TRUE(params, NS_ERROR_OUT_OF_MEMORY); \ + return params->BIND_NAME_CONCAT(_name, ByIndex) _invArgs; \ + } + +/** + * Implement BindByName/BindByIndex for the given class. + * + * @param _class The class name. + * @param _optionalGuard The guard clause to inject. + */ +#define BIND_BASE_IMPLS(_class, _optionalGuard) \ + NS_IMETHODIMP _class::BindByName(const nsACString &aName, \ + nsIVariant *aValue) \ + { \ + _optionalGuard \ + mozIStorageBindingParams *params = getParams(); \ + NS_ENSURE_TRUE(params, NS_ERROR_OUT_OF_MEMORY); \ + return params->BindByName(aName, aValue); \ + } \ + NS_IMETHODIMP _class::BindByIndex(uint32_t aIndex, \ + nsIVariant *aValue) \ + { \ + _optionalGuard \ + mozIStorageBindingParams *params = getParams(); \ + NS_ENSURE_TRUE(params, NS_ERROR_OUT_OF_MEMORY); \ + return params->BindByIndex(aIndex, aValue); \ + } + +/** + * Define the various Bind*Parameter, Bind*ByIndex, Bind*ByName stubs that just + * end up proxying to the params object. + */ +#define BOILERPLATE_BIND_PROXIES(_class, _optionalGuard) \ + BIND_BASE_IMPLS(_class, _optionalGuard) \ + BIND_GEN_IMPL(_class, _optionalGuard, \ + UTF8String, \ + (const nsACString &aWhere, \ + const nsACString &aValue), \ + (uint32_t aWhere, \ + const nsACString &aValue), \ + (aWhere, aValue)) \ + BIND_GEN_IMPL(_class, _optionalGuard, \ + String, \ + (const nsACString &aWhere, \ + const nsAString &aValue), \ + (uint32_t aWhere, \ + const nsAString &aValue), \ + (aWhere, aValue)) \ + BIND_GEN_IMPL(_class, _optionalGuard, \ + Double, \ + (const nsACString &aWhere, \ + double aValue), \ + (uint32_t aWhere, \ + double aValue), \ + (aWhere, aValue)) \ + BIND_GEN_IMPL(_class, _optionalGuard, \ + Int32, \ + (const nsACString &aWhere, \ + int32_t aValue), \ + (uint32_t aWhere, \ + int32_t aValue), \ + (aWhere, aValue)) \ + BIND_GEN_IMPL(_class, _optionalGuard, \ + Int64, \ + (const nsACString &aWhere, \ + int64_t aValue), \ + (uint32_t aWhere, \ + int64_t aValue), \ + (aWhere, aValue)) \ + BIND_GEN_IMPL(_class, _optionalGuard, \ + Null, \ + (const nsACString &aWhere), \ + (uint32_t aWhere), \ + (aWhere)) \ + BIND_GEN_IMPL(_class, _optionalGuard, \ + Blob, \ + (const nsACString &aWhere, \ + const uint8_t *aValue, \ + uint32_t aValueSize), \ + (uint32_t aWhere, \ + const uint8_t *aValue, \ + uint32_t aValueSize), \ + (aWhere, aValue, aValueSize)) \ + BIND_GEN_IMPL(_class, _optionalGuard, \ + StringAsBlob, \ + (const nsACString &aWhere, \ + const nsAString& aValue), \ + (uint32_t aWhere, \ + const nsAString& aValue), \ + (aWhere, aValue)) \ + BIND_GEN_IMPL(_class, _optionalGuard, \ + UTF8StringAsBlob, \ + (const nsACString &aWhere, \ + const nsACString& aValue), \ + (uint32_t aWhere, \ + const nsACString& aValue), \ + (aWhere, aValue)) \ + BIND_GEN_IMPL(_class, _optionalGuard, \ + AdoptedBlob, \ + (const nsACString &aWhere, \ + uint8_t *aValue, \ + uint32_t aValueSize), \ + (uint32_t aWhere, \ + uint8_t *aValue, \ + uint32_t aValueSize), \ + (aWhere, aValue, aValueSize)) + + + +} // namespace storage +} // namespace mozilla + +#endif // mozilla_storage_StorageBaseStatementInternal_h_ diff --git a/storage/TelemetryVFS.cpp b/storage/TelemetryVFS.cpp new file mode 100644 index 000000000..f4f28b36c --- /dev/null +++ b/storage/TelemetryVFS.cpp @@ -0,0 +1,902 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include <string.h> +#include "mozilla/Telemetry.h" +#include "mozilla/Preferences.h" +#include "sqlite3.h" +#include "nsThreadUtils.h" +#include "mozilla/dom/quota/PersistenceType.h" +#include "mozilla/dom/quota/QuotaManager.h" +#include "mozilla/dom/quota/QuotaObject.h" +#include "mozilla/IOInterposer.h" + +// The last VFS version for which this file has been updated. +#define LAST_KNOWN_VFS_VERSION 3 + +// The last io_methods version for which this file has been updated. +#define LAST_KNOWN_IOMETHODS_VERSION 3 + +/** + * This preference is a workaround to allow users/sysadmins to identify + * that the profile exists on an NFS share whose implementation + * is incompatible with SQLite's default locking implementation. + * Bug 433129 attempted to automatically identify such file-systems, + * but a reliable way was not found and it was determined that the fallback + * locking is slower than POSIX locking, so we do not want to do it by default. +*/ +#define PREF_NFS_FILESYSTEM "storage.nfs_filesystem" + +namespace { + +using namespace mozilla; +using namespace mozilla::dom::quota; + +struct Histograms { + const char *name; + const Telemetry::ID readB; + const Telemetry::ID writeB; + const Telemetry::ID readMS; + const Telemetry::ID writeMS; + const Telemetry::ID syncMS; +}; + +#define SQLITE_TELEMETRY(FILENAME, HGRAM) \ + { FILENAME, \ + Telemetry::MOZ_SQLITE_ ## HGRAM ## _READ_B, \ + Telemetry::MOZ_SQLITE_ ## HGRAM ## _WRITE_B, \ + Telemetry::MOZ_SQLITE_ ## HGRAM ## _READ_MS, \ + Telemetry::MOZ_SQLITE_ ## HGRAM ## _WRITE_MS, \ + Telemetry::MOZ_SQLITE_ ## HGRAM ## _SYNC_MS \ + } + +Histograms gHistograms[] = { + SQLITE_TELEMETRY("places.sqlite", PLACES), + SQLITE_TELEMETRY("cookies.sqlite", COOKIES), + SQLITE_TELEMETRY("webappsstore.sqlite", WEBAPPS), + SQLITE_TELEMETRY(nullptr, OTHER) +}; +#undef SQLITE_TELEMETRY + +/** RAII class for measuring how long io takes on/off main thread + */ +class IOThreadAutoTimer { +public: + /** + * IOThreadAutoTimer measures time spent in IO. Additionally it + * automatically determines whether IO is happening on the main + * thread and picks an appropriate histogram. + * + * @param id takes a telemetry histogram id. The id+1 must be an + * equivalent histogram for the main thread. Eg, MOZ_SQLITE_OPEN_MS + * is followed by MOZ_SQLITE_OPEN_MAIN_THREAD_MS. + * + * @param aOp optionally takes an IO operation to report through the + * IOInterposer. Filename will be reported as NULL, and reference will be + * either "sqlite-mainthread" or "sqlite-otherthread". + */ + explicit IOThreadAutoTimer(Telemetry::ID aId, + IOInterposeObserver::Operation aOp = IOInterposeObserver::OpNone) + : start(TimeStamp::Now()), + id(aId), + op(aOp) + { + } + + /** + * This constructor is for when we want to report an operation to + * IOInterposer but do not require a telemetry probe. + * + * @param aOp IO Operation to report through the IOInterposer. + */ + explicit IOThreadAutoTimer(IOInterposeObserver::Operation aOp) + : start(TimeStamp::Now()), + id(Telemetry::HistogramCount), + op(aOp) + { + } + + ~IOThreadAutoTimer() + { + TimeStamp end(TimeStamp::Now()); + uint32_t mainThread = NS_IsMainThread() ? 1 : 0; + if (id != Telemetry::HistogramCount) { + Telemetry::AccumulateTimeDelta(static_cast<Telemetry::ID>(id + mainThread), + start, end); + } + // We don't report SQLite I/O on Windows because we have a comprehensive + // mechanism for intercepting I/O on that platform that captures a superset + // of the data captured here. +#if defined(MOZ_ENABLE_PROFILER_SPS) && !defined(XP_WIN) + if (IOInterposer::IsObservedOperation(op)) { + const char* main_ref = "sqlite-mainthread"; + const char* other_ref = "sqlite-otherthread"; + + // Create observation + IOInterposeObserver::Observation ob(op, start, end, + (mainThread ? main_ref : other_ref)); + // Report observation + IOInterposer::Report(ob); + } +#endif /* defined(MOZ_ENABLE_PROFILER_SPS) && !defined(XP_WIN) */ + } + +private: + const TimeStamp start; + const Telemetry::ID id; + IOInterposeObserver::Operation op; +}; + +struct telemetry_file { + // Base class. Must be first + sqlite3_file base; + + // histograms pertaining to this file + Histograms *histograms; + + // quota object for this file + RefPtr<QuotaObject> quotaObject; + + // The chunk size for this file. See the documentation for + // sqlite3_file_control() and FCNTL_CHUNK_SIZE. + int fileChunkSize; + + // This contains the vfs that actually does work + sqlite3_file pReal[1]; +}; + +const char* +DatabasePathFromWALPath(const char *zWALName) +{ + /** + * Do some sketchy pointer arithmetic to find the parameter key. The WAL + * filename is in the middle of a big allocated block that contains: + * + * - Random Values + * - Main Database Path + * - \0 + * - Multiple URI components consisting of: + * - Key + * - \0 + * - Value + * - \0 + * - \0 + * - Journal Path + * - \0 + * - WAL Path (zWALName) + * - \0 + * + * Because the main database path is preceded by a random value we have to be + * careful when trying to figure out when we should terminate this loop. + */ + MOZ_ASSERT(zWALName); + + nsDependentCSubstring dbPath(zWALName, strlen(zWALName)); + + // Chop off the "-wal" suffix. + NS_NAMED_LITERAL_CSTRING(kWALSuffix, "-wal"); + MOZ_ASSERT(StringEndsWith(dbPath, kWALSuffix)); + + dbPath.Rebind(zWALName, dbPath.Length() - kWALSuffix.Length()); + MOZ_ASSERT(!dbPath.IsEmpty()); + + // We want to scan to the end of the key/value URI pairs. Skip the preceding + // null and go to the last char of the journal path. + const char* cursor = zWALName - 2; + + // Make sure we just skipped a null. + MOZ_ASSERT(!*(cursor + 1)); + + // Walk backwards over the journal path. + while (*cursor) { + cursor--; + } + + // There should be another null here. + cursor--; + MOZ_ASSERT(!*cursor); + + // Back up one more char to the last char of the previous string. It may be + // the database path or it may be a key/value URI pair. + cursor--; + +#ifdef DEBUG + { + // Verify that we just walked over the journal path. Account for the two + // nulls we just skipped. + const char *journalStart = cursor + 3; + + nsDependentCSubstring journalPath(journalStart, + strlen(journalStart)); + + // Chop off the "-journal" suffix. + NS_NAMED_LITERAL_CSTRING(kJournalSuffix, "-journal"); + MOZ_ASSERT(StringEndsWith(journalPath, kJournalSuffix)); + + journalPath.Rebind(journalStart, + journalPath.Length() - kJournalSuffix.Length()); + MOZ_ASSERT(!journalPath.IsEmpty()); + + // Make sure that the database name is a substring of the journal name. + MOZ_ASSERT(journalPath == dbPath); + } +#endif + + // Now we're either at the end of the key/value URI pairs or we're at the + // end of the database path. Carefully walk backwards one character at a + // time to do this safely without running past the beginning of the database + // path. + const char *const dbPathStart = dbPath.BeginReading(); + const char *dbPathCursor = dbPath.EndReading() - 1; + bool isDBPath = true; + + while (true) { + MOZ_ASSERT(*dbPathCursor, "dbPathCursor should never see a null char!"); + + if (isDBPath) { + isDBPath = dbPathStart <= dbPathCursor && + *dbPathCursor == *cursor && + *cursor; + } + + if (!isDBPath) { + // This isn't the database path so it must be a value. Scan past it and + // the key also. + for (size_t stringCount = 0; stringCount < 2; stringCount++) { + // Scan past the string to the preceding null character. + while (*cursor) { + cursor--; + } + + // Back up one more char to the last char of preceding string. + cursor--; + } + + // Reset and start again. + dbPathCursor = dbPath.EndReading() - 1; + isDBPath = true; + + continue; + } + + MOZ_ASSERT(isDBPath); + MOZ_ASSERT(*cursor); + + if (dbPathStart == dbPathCursor) { + // Found the full database path, we're all done. + MOZ_ASSERT(nsDependentCString(cursor) == dbPath); + return cursor; + } + + // Change the cursors and go through the loop again. + cursor--; + dbPathCursor--; + } + + MOZ_CRASH("Should never get here!"); +} + +already_AddRefed<QuotaObject> +GetQuotaObjectFromNameAndParameters(const char *zName, + const char *zURIParameterKey) +{ + MOZ_ASSERT(zName); + MOZ_ASSERT(zURIParameterKey); + + const char *persistenceType = + sqlite3_uri_parameter(zURIParameterKey, "persistenceType"); + if (!persistenceType) { + return nullptr; + } + + const char *group = sqlite3_uri_parameter(zURIParameterKey, "group"); + if (!group) { + NS_WARNING("SQLite URI had 'persistenceType' but not 'group'?!"); + return nullptr; + } + + const char *origin = sqlite3_uri_parameter(zURIParameterKey, "origin"); + if (!origin) { + NS_WARNING("SQLite URI had 'persistenceType' and 'group' but not " + "'origin'?!"); + return nullptr; + } + + QuotaManager *quotaManager = QuotaManager::Get(); + MOZ_ASSERT(quotaManager); + + return quotaManager->GetQuotaObject( + PersistenceTypeFromText(nsDependentCString(persistenceType)), + nsDependentCString(group), + nsDependentCString(origin), + NS_ConvertUTF8toUTF16(zName)); +} + +void +MaybeEstablishQuotaControl(const char *zName, + telemetry_file *pFile, + int flags) +{ + MOZ_ASSERT(pFile); + MOZ_ASSERT(!pFile->quotaObject); + + if (!(flags & (SQLITE_OPEN_URI | SQLITE_OPEN_WAL))) { + return; + } + + MOZ_ASSERT(zName); + + const char *zURIParameterKey = (flags & SQLITE_OPEN_WAL) ? + DatabasePathFromWALPath(zName) : + zName; + + MOZ_ASSERT(zURIParameterKey); + + pFile->quotaObject = + GetQuotaObjectFromNameAndParameters(zName, zURIParameterKey); +} + +/* +** Close a telemetry_file. +*/ +int +xClose(sqlite3_file *pFile) +{ + telemetry_file *p = (telemetry_file *)pFile; + int rc; + { // Scope for IOThreadAutoTimer + IOThreadAutoTimer ioTimer(IOInterposeObserver::OpClose); + rc = p->pReal->pMethods->xClose(p->pReal); + } + if( rc==SQLITE_OK ){ + delete p->base.pMethods; + p->base.pMethods = nullptr; + p->quotaObject = nullptr; +#ifdef DEBUG + p->fileChunkSize = 0; +#endif + } + return rc; +} + +/* +** Read data from a telemetry_file. +*/ +int +xRead(sqlite3_file *pFile, void *zBuf, int iAmt, sqlite_int64 iOfst) +{ + telemetry_file *p = (telemetry_file *)pFile; + IOThreadAutoTimer ioTimer(p->histograms->readMS, IOInterposeObserver::OpRead); + int rc; + rc = p->pReal->pMethods->xRead(p->pReal, zBuf, iAmt, iOfst); + // sqlite likes to read from empty files, this is normal, ignore it. + if (rc != SQLITE_IOERR_SHORT_READ) + Telemetry::Accumulate(p->histograms->readB, rc == SQLITE_OK ? iAmt : 0); + return rc; +} + +/* +** Return the current file-size of a telemetry_file. +*/ +int +xFileSize(sqlite3_file *pFile, sqlite_int64 *pSize) +{ + IOThreadAutoTimer ioTimer(IOInterposeObserver::OpStat); + telemetry_file *p = (telemetry_file *)pFile; + int rc; + rc = p->pReal->pMethods->xFileSize(p->pReal, pSize); + return rc; +} + +/* +** Write data to a telemetry_file. +*/ +int +xWrite(sqlite3_file *pFile, const void *zBuf, int iAmt, sqlite_int64 iOfst) +{ + telemetry_file *p = (telemetry_file *)pFile; + IOThreadAutoTimer ioTimer(p->histograms->writeMS, IOInterposeObserver::OpWrite); + int rc; + if (p->quotaObject) { + MOZ_ASSERT(INT64_MAX - iOfst >= iAmt); + if (!p->quotaObject->MaybeUpdateSize(iOfst + iAmt, /* aTruncate */ false)) { + return SQLITE_FULL; + } + } + rc = p->pReal->pMethods->xWrite(p->pReal, zBuf, iAmt, iOfst); + Telemetry::Accumulate(p->histograms->writeB, rc == SQLITE_OK ? iAmt : 0); + if (p->quotaObject && rc != SQLITE_OK) { + NS_WARNING("xWrite failed on a quota-controlled file, attempting to " + "update its current size..."); + sqlite_int64 currentSize; + if (xFileSize(pFile, ¤tSize) == SQLITE_OK) { + p->quotaObject->MaybeUpdateSize(currentSize, /* aTruncate */ true); + } + } + return rc; +} + +/* +** Truncate a telemetry_file. +*/ +int +xTruncate(sqlite3_file *pFile, sqlite_int64 size) +{ + IOThreadAutoTimer ioTimer(Telemetry::MOZ_SQLITE_TRUNCATE_MS); + telemetry_file *p = (telemetry_file *)pFile; + int rc; + Telemetry::AutoTimer<Telemetry::MOZ_SQLITE_TRUNCATE_MS> timer; + if (p->quotaObject) { + if (p->fileChunkSize > 0) { + // Round up to the smallest multiple of the chunk size that will hold all + // the data. + size = + ((size + p->fileChunkSize - 1) / p->fileChunkSize) * p->fileChunkSize; + } + if (!p->quotaObject->MaybeUpdateSize(size, /* aTruncate */ true)) { + return SQLITE_FULL; + } + } + rc = p->pReal->pMethods->xTruncate(p->pReal, size); + if (p->quotaObject) { + if (rc == SQLITE_OK) { +#ifdef DEBUG + // Make sure xTruncate set the size exactly as we calculated above. + sqlite_int64 newSize; + MOZ_ASSERT(xFileSize(pFile, &newSize) == SQLITE_OK); + MOZ_ASSERT(newSize == size); +#endif + } else { + NS_WARNING("xTruncate failed on a quota-controlled file, attempting to " + "update its current size..."); + if (xFileSize(pFile, &size) == SQLITE_OK) { + p->quotaObject->MaybeUpdateSize(size, /* aTruncate */ true); + } + } + } + return rc; +} + +/* +** Sync a telemetry_file. +*/ +int +xSync(sqlite3_file *pFile, int flags) +{ + telemetry_file *p = (telemetry_file *)pFile; + IOThreadAutoTimer ioTimer(p->histograms->syncMS, IOInterposeObserver::OpFSync); + return p->pReal->pMethods->xSync(p->pReal, flags); +} + +/* +** Lock a telemetry_file. +*/ +int +xLock(sqlite3_file *pFile, int eLock) +{ + telemetry_file *p = (telemetry_file *)pFile; + int rc; + rc = p->pReal->pMethods->xLock(p->pReal, eLock); + return rc; +} + +/* +** Unlock a telemetry_file. +*/ +int +xUnlock(sqlite3_file *pFile, int eLock) +{ + telemetry_file *p = (telemetry_file *)pFile; + int rc; + rc = p->pReal->pMethods->xUnlock(p->pReal, eLock); + return rc; +} + +/* +** Check if another file-handle holds a RESERVED lock on a telemetry_file. +*/ +int +xCheckReservedLock(sqlite3_file *pFile, int *pResOut) +{ + telemetry_file *p = (telemetry_file *)pFile; + int rc = p->pReal->pMethods->xCheckReservedLock(p->pReal, pResOut); + return rc; +} + +/* +** File control method. For custom operations on a telemetry_file. +*/ +int +xFileControl(sqlite3_file *pFile, int op, void *pArg) +{ + telemetry_file *p = (telemetry_file *)pFile; + int rc; + // Hook SQLITE_FCNTL_SIZE_HINT for quota-controlled files and do the necessary + // work before passing to the SQLite VFS. + if (op == SQLITE_FCNTL_SIZE_HINT && p->quotaObject) { + sqlite3_int64 hintSize = *static_cast<sqlite3_int64*>(pArg); + sqlite3_int64 currentSize; + rc = xFileSize(pFile, ¤tSize); + if (rc != SQLITE_OK) { + return rc; + } + if (hintSize > currentSize) { + rc = xTruncate(pFile, hintSize); + if (rc != SQLITE_OK) { + return rc; + } + } + } + rc = p->pReal->pMethods->xFileControl(p->pReal, op, pArg); + // Grab the file chunk size after the SQLite VFS has approved. + if (op == SQLITE_FCNTL_CHUNK_SIZE && rc == SQLITE_OK) { + p->fileChunkSize = *static_cast<int*>(pArg); + } +#ifdef DEBUG + if (op == SQLITE_FCNTL_SIZE_HINT && p->quotaObject && rc == SQLITE_OK) { + sqlite3_int64 hintSize = *static_cast<sqlite3_int64*>(pArg); + if (p->fileChunkSize > 0) { + hintSize = + ((hintSize + p->fileChunkSize - 1) / p->fileChunkSize) * + p->fileChunkSize; + } + sqlite3_int64 currentSize; + MOZ_ASSERT(xFileSize(pFile, ¤tSize) == SQLITE_OK); + MOZ_ASSERT(currentSize >= hintSize); + } +#endif + return rc; +} + +/* +** Return the sector-size in bytes for a telemetry_file. +*/ +int +xSectorSize(sqlite3_file *pFile) +{ + telemetry_file *p = (telemetry_file *)pFile; + int rc; + rc = p->pReal->pMethods->xSectorSize(p->pReal); + return rc; +} + +/* +** Return the device characteristic flags supported by a telemetry_file. +*/ +int +xDeviceCharacteristics(sqlite3_file *pFile) +{ + telemetry_file *p = (telemetry_file *)pFile; + int rc; + rc = p->pReal->pMethods->xDeviceCharacteristics(p->pReal); + return rc; +} + +/* +** Shared-memory operations. +*/ +int +xShmLock(sqlite3_file *pFile, int ofst, int n, int flags) +{ + telemetry_file *p = (telemetry_file *)pFile; + return p->pReal->pMethods->xShmLock(p->pReal, ofst, n, flags); +} + +int +xShmMap(sqlite3_file *pFile, int iRegion, int szRegion, int isWrite, void volatile **pp) +{ + telemetry_file *p = (telemetry_file *)pFile; + int rc; + rc = p->pReal->pMethods->xShmMap(p->pReal, iRegion, szRegion, isWrite, pp); + return rc; +} + +void +xShmBarrier(sqlite3_file *pFile){ + telemetry_file *p = (telemetry_file *)pFile; + p->pReal->pMethods->xShmBarrier(p->pReal); +} + +int +xShmUnmap(sqlite3_file *pFile, int delFlag){ + telemetry_file *p = (telemetry_file *)pFile; + int rc; + rc = p->pReal->pMethods->xShmUnmap(p->pReal, delFlag); + return rc; +} + +int +xFetch(sqlite3_file *pFile, sqlite3_int64 iOff, int iAmt, void **pp) +{ + telemetry_file *p = (telemetry_file *)pFile; + MOZ_ASSERT(p->pReal->pMethods->iVersion >= 3); + return p->pReal->pMethods->xFetch(p->pReal, iOff, iAmt, pp); +} + +int +xUnfetch(sqlite3_file *pFile, sqlite3_int64 iOff, void *pResOut) +{ + telemetry_file *p = (telemetry_file *)pFile; + MOZ_ASSERT(p->pReal->pMethods->iVersion >= 3); + return p->pReal->pMethods->xUnfetch(p->pReal, iOff, pResOut); +} + +int +xOpen(sqlite3_vfs* vfs, const char *zName, sqlite3_file* pFile, + int flags, int *pOutFlags) +{ + IOThreadAutoTimer ioTimer(Telemetry::MOZ_SQLITE_OPEN_MS, + IOInterposeObserver::OpCreateOrOpen); + Telemetry::AutoTimer<Telemetry::MOZ_SQLITE_OPEN_MS> timer; + sqlite3_vfs *orig_vfs = static_cast<sqlite3_vfs*>(vfs->pAppData); + int rc; + telemetry_file *p = (telemetry_file *)pFile; + Histograms *h = nullptr; + // check if the filename is one we are probing for + for(size_t i = 0;i < sizeof(gHistograms)/sizeof(gHistograms[0]);i++) { + h = &gHistograms[i]; + // last probe is the fallback probe + if (!h->name) + break; + if (!zName) + continue; + const char *match = strstr(zName, h->name); + if (!match) + continue; + char c = match[strlen(h->name)]; + // include -wal/-journal too + if (!c || c == '-') + break; + } + p->histograms = h; + + MaybeEstablishQuotaControl(zName, p, flags); + + rc = orig_vfs->xOpen(orig_vfs, zName, p->pReal, flags, pOutFlags); + if( rc != SQLITE_OK ) + return rc; + if( p->pReal->pMethods ){ + sqlite3_io_methods *pNew = new sqlite3_io_methods; + const sqlite3_io_methods *pSub = p->pReal->pMethods; + memset(pNew, 0, sizeof(*pNew)); + // If the io_methods version is higher than the last known one, you should + // update this VFS adding appropriate IO methods for any methods added in + // the version change. + pNew->iVersion = pSub->iVersion; + MOZ_ASSERT(pNew->iVersion <= LAST_KNOWN_IOMETHODS_VERSION); + pNew->xClose = xClose; + pNew->xRead = xRead; + pNew->xWrite = xWrite; + pNew->xTruncate = xTruncate; + pNew->xSync = xSync; + pNew->xFileSize = xFileSize; + pNew->xLock = xLock; + pNew->xUnlock = xUnlock; + pNew->xCheckReservedLock = xCheckReservedLock; + pNew->xFileControl = xFileControl; + pNew->xSectorSize = xSectorSize; + pNew->xDeviceCharacteristics = xDeviceCharacteristics; + if (pNew->iVersion >= 2) { + // Methods added in version 2. + pNew->xShmMap = pSub->xShmMap ? xShmMap : 0; + pNew->xShmLock = pSub->xShmLock ? xShmLock : 0; + pNew->xShmBarrier = pSub->xShmBarrier ? xShmBarrier : 0; + pNew->xShmUnmap = pSub->xShmUnmap ? xShmUnmap : 0; + } + if (pNew->iVersion >= 3) { + // Methods added in version 3. + // SQLite 3.7.17 calls these methods without checking for nullptr first, + // so we always define them. Verify that we're not going to call + // nullptrs, though. + MOZ_ASSERT(pSub->xFetch); + pNew->xFetch = xFetch; + MOZ_ASSERT(pSub->xUnfetch); + pNew->xUnfetch = xUnfetch; + } + pFile->pMethods = pNew; + } + return rc; +} + +int +xDelete(sqlite3_vfs* vfs, const char *zName, int syncDir) +{ + sqlite3_vfs *orig_vfs = static_cast<sqlite3_vfs*>(vfs->pAppData); + int rc; + RefPtr<QuotaObject> quotaObject; + + if (StringEndsWith(nsDependentCString(zName), NS_LITERAL_CSTRING("-wal"))) { + const char *zURIParameterKey = DatabasePathFromWALPath(zName); + MOZ_ASSERT(zURIParameterKey); + + quotaObject = GetQuotaObjectFromNameAndParameters(zName, zURIParameterKey); + } + + rc = orig_vfs->xDelete(orig_vfs, zName, syncDir); + if (rc == SQLITE_OK && quotaObject) { + MOZ_ALWAYS_TRUE(quotaObject->MaybeUpdateSize(0, /* aTruncate */ true)); + } + + return rc; +} + +int +xAccess(sqlite3_vfs *vfs, const char *zName, int flags, int *pResOut) +{ + sqlite3_vfs *orig_vfs = static_cast<sqlite3_vfs*>(vfs->pAppData); + return orig_vfs->xAccess(orig_vfs, zName, flags, pResOut); +} + +int +xFullPathname(sqlite3_vfs *vfs, const char *zName, int nOut, char *zOut) +{ + sqlite3_vfs *orig_vfs = static_cast<sqlite3_vfs*>(vfs->pAppData); + return orig_vfs->xFullPathname(orig_vfs, zName, nOut, zOut); +} + +void* +xDlOpen(sqlite3_vfs *vfs, const char *zFilename) +{ + sqlite3_vfs *orig_vfs = static_cast<sqlite3_vfs*>(vfs->pAppData); + return orig_vfs->xDlOpen(orig_vfs, zFilename); +} + +void +xDlError(sqlite3_vfs *vfs, int nByte, char *zErrMsg) +{ + sqlite3_vfs *orig_vfs = static_cast<sqlite3_vfs*>(vfs->pAppData); + orig_vfs->xDlError(orig_vfs, nByte, zErrMsg); +} + +void +(*xDlSym(sqlite3_vfs *vfs, void *pHdle, const char *zSym))(void){ + sqlite3_vfs *orig_vfs = static_cast<sqlite3_vfs*>(vfs->pAppData); + return orig_vfs->xDlSym(orig_vfs, pHdle, zSym); +} + +void +xDlClose(sqlite3_vfs *vfs, void *pHandle) +{ + sqlite3_vfs *orig_vfs = static_cast<sqlite3_vfs*>(vfs->pAppData); + orig_vfs->xDlClose(orig_vfs, pHandle); +} + +int +xRandomness(sqlite3_vfs *vfs, int nByte, char *zOut) +{ + sqlite3_vfs *orig_vfs = static_cast<sqlite3_vfs*>(vfs->pAppData); + return orig_vfs->xRandomness(orig_vfs, nByte, zOut); +} + +int +xSleep(sqlite3_vfs *vfs, int microseconds) +{ + sqlite3_vfs *orig_vfs = static_cast<sqlite3_vfs*>(vfs->pAppData); + return orig_vfs->xSleep(orig_vfs, microseconds); +} + +int +xCurrentTime(sqlite3_vfs *vfs, double *prNow) +{ + sqlite3_vfs *orig_vfs = static_cast<sqlite3_vfs*>(vfs->pAppData); + return orig_vfs->xCurrentTime(orig_vfs, prNow); +} + +int +xGetLastError(sqlite3_vfs *vfs, int nBuf, char *zBuf) +{ + sqlite3_vfs *orig_vfs = static_cast<sqlite3_vfs*>(vfs->pAppData); + return orig_vfs->xGetLastError(orig_vfs, nBuf, zBuf); +} + +int +xCurrentTimeInt64(sqlite3_vfs *vfs, sqlite3_int64 *piNow) +{ + sqlite3_vfs *orig_vfs = static_cast<sqlite3_vfs*>(vfs->pAppData); + return orig_vfs->xCurrentTimeInt64(orig_vfs, piNow); +} + +static +int +xSetSystemCall(sqlite3_vfs *vfs, const char *zName, sqlite3_syscall_ptr pFunc) +{ + sqlite3_vfs *orig_vfs = static_cast<sqlite3_vfs*>(vfs->pAppData); + return orig_vfs->xSetSystemCall(orig_vfs, zName, pFunc); +} + +static +sqlite3_syscall_ptr +xGetSystemCall(sqlite3_vfs *vfs, const char *zName) +{ + sqlite3_vfs *orig_vfs = static_cast<sqlite3_vfs*>(vfs->pAppData); + return orig_vfs->xGetSystemCall(orig_vfs, zName); +} + +static +const char * +xNextSystemCall(sqlite3_vfs *vfs, const char *zName) +{ + sqlite3_vfs *orig_vfs = static_cast<sqlite3_vfs*>(vfs->pAppData); + return orig_vfs->xNextSystemCall(orig_vfs, zName); +} + +} // namespace + +namespace mozilla { +namespace storage { + +sqlite3_vfs* ConstructTelemetryVFS() +{ +#if defined(XP_WIN) +#define EXPECTED_VFS "win32" +#define EXPECTED_VFS_NFS "win32" +#else +#define EXPECTED_VFS "unix" +#define EXPECTED_VFS_NFS "unix-excl" +#endif + + bool expected_vfs; + sqlite3_vfs *vfs; + if (Preferences::GetBool(PREF_NFS_FILESYSTEM)) { + vfs = sqlite3_vfs_find(EXPECTED_VFS_NFS); + expected_vfs = (vfs != nullptr); + } + else { + vfs = sqlite3_vfs_find(nullptr); + expected_vfs = vfs->zName && !strcmp(vfs->zName, EXPECTED_VFS); + } + if (!expected_vfs) { + return nullptr; + } + + sqlite3_vfs *tvfs = new ::sqlite3_vfs; + memset(tvfs, 0, sizeof(::sqlite3_vfs)); + // If the VFS version is higher than the last known one, you should update + // this VFS adding appropriate methods for any methods added in the version + // change. + tvfs->iVersion = vfs->iVersion; + MOZ_ASSERT(vfs->iVersion <= LAST_KNOWN_VFS_VERSION); + tvfs->szOsFile = sizeof(telemetry_file) - sizeof(sqlite3_file) + vfs->szOsFile; + tvfs->mxPathname = vfs->mxPathname; + tvfs->zName = "telemetry-vfs"; + tvfs->pAppData = vfs; + tvfs->xOpen = xOpen; + tvfs->xDelete = xDelete; + tvfs->xAccess = xAccess; + tvfs->xFullPathname = xFullPathname; + tvfs->xDlOpen = xDlOpen; + tvfs->xDlError = xDlError; + tvfs->xDlSym = xDlSym; + tvfs->xDlClose = xDlClose; + tvfs->xRandomness = xRandomness; + tvfs->xSleep = xSleep; + tvfs->xCurrentTime = xCurrentTime; + tvfs->xGetLastError = xGetLastError; + if (tvfs->iVersion >= 2) { + // Methods added in version 2. + tvfs->xCurrentTimeInt64 = xCurrentTimeInt64; + } + if (tvfs->iVersion >= 3) { + // Methods added in version 3. + tvfs->xSetSystemCall = xSetSystemCall; + tvfs->xGetSystemCall = xGetSystemCall; + tvfs->xNextSystemCall = xNextSystemCall; + } + return tvfs; +} + +already_AddRefed<QuotaObject> +GetQuotaObjectForFile(sqlite3_file *pFile) +{ + MOZ_ASSERT(pFile); + + telemetry_file *p = (telemetry_file *)pFile; + RefPtr<QuotaObject> result = p->quotaObject; + return result.forget(); +} + +} // namespace storage +} // namespace mozilla diff --git a/storage/VacuumManager.cpp b/storage/VacuumManager.cpp new file mode 100644 index 000000000..f35ded2d6 --- /dev/null +++ b/storage/VacuumManager.cpp @@ -0,0 +1,388 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/DebugOnly.h" + +#include "VacuumManager.h" + +#include "mozilla/Services.h" +#include "mozilla/Preferences.h" +#include "nsIObserverService.h" +#include "nsIFile.h" +#include "nsThreadUtils.h" +#include "mozilla/Logging.h" +#include "prtime.h" + +#include "mozStorageConnection.h" +#include "mozIStorageStatement.h" +#include "mozIStorageAsyncStatement.h" +#include "mozIStoragePendingStatement.h" +#include "mozIStorageError.h" +#include "mozStorageHelper.h" +#include "nsXULAppAPI.h" + +#define OBSERVER_TOPIC_IDLE_DAILY "idle-daily" +#define OBSERVER_TOPIC_XPCOM_SHUTDOWN "xpcom-shutdown" + +// Used to notify begin and end of a heavy IO task. +#define OBSERVER_TOPIC_HEAVY_IO "heavy-io-task" +#define OBSERVER_DATA_VACUUM_BEGIN NS_LITERAL_STRING("vacuum-begin") +#define OBSERVER_DATA_VACUUM_END NS_LITERAL_STRING("vacuum-end") + +// This preferences root will contain last vacuum timestamps (in seconds) for +// each database. The database filename is used as a key. +#define PREF_VACUUM_BRANCH "storage.vacuum.last." + +// Time between subsequent vacuum calls for a certain database. +#define VACUUM_INTERVAL_SECONDS 30 * 86400 // 30 days. + +extern mozilla::LazyLogModule gStorageLog; + +namespace mozilla { +namespace storage { + +namespace { + +//////////////////////////////////////////////////////////////////////////////// +//// BaseCallback + +class BaseCallback : public mozIStorageStatementCallback +{ +public: + NS_DECL_ISUPPORTS + NS_DECL_MOZISTORAGESTATEMENTCALLBACK + BaseCallback() {} +protected: + virtual ~BaseCallback() {} +}; + +NS_IMETHODIMP +BaseCallback::HandleError(mozIStorageError *aError) +{ +#ifdef DEBUG + int32_t result; + nsresult rv = aError->GetResult(&result); + NS_ENSURE_SUCCESS(rv, rv); + nsAutoCString message; + rv = aError->GetMessage(message); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoCString warnMsg; + warnMsg.AppendLiteral("An error occured during async execution: "); + warnMsg.AppendInt(result); + warnMsg.Append(' '); + warnMsg.Append(message); + NS_WARNING(warnMsg.get()); +#endif + return NS_OK; +} + +NS_IMETHODIMP +BaseCallback::HandleResult(mozIStorageResultSet *aResultSet) +{ + // We could get results from PRAGMA statements, but we don't mind them. + return NS_OK; +} + +NS_IMETHODIMP +BaseCallback::HandleCompletion(uint16_t aReason) +{ + // By default BaseCallback will just be silent on completion. + return NS_OK; +} + +NS_IMPL_ISUPPORTS( + BaseCallback +, mozIStorageStatementCallback +) + +//////////////////////////////////////////////////////////////////////////////// +//// Vacuumer declaration. + +class Vacuumer : public BaseCallback +{ +public: + NS_DECL_MOZISTORAGESTATEMENTCALLBACK + + explicit Vacuumer(mozIStorageVacuumParticipant *aParticipant); + + bool execute(); + nsresult notifyCompletion(bool aSucceeded); + +private: + nsCOMPtr<mozIStorageVacuumParticipant> mParticipant; + nsCString mDBFilename; + nsCOMPtr<mozIStorageConnection> mDBConn; +}; + +//////////////////////////////////////////////////////////////////////////////// +//// Vacuumer implementation. + +Vacuumer::Vacuumer(mozIStorageVacuumParticipant *aParticipant) + : mParticipant(aParticipant) +{ +} + +bool +Vacuumer::execute() +{ + MOZ_ASSERT(NS_IsMainThread(), "Must be running on the main thread!"); + + // Get the connection and check its validity. + nsresult rv = mParticipant->GetDatabaseConnection(getter_AddRefs(mDBConn)); + NS_ENSURE_SUCCESS(rv, false); + bool ready = false; + if (!mDBConn || NS_FAILED(mDBConn->GetConnectionReady(&ready)) || !ready) { + NS_WARNING("Unable to get a connection to vacuum database"); + return false; + } + + // Ask for the expected page size. Vacuum can change the page size, unless + // the database is using WAL journaling. + // TODO Bug 634374: figure out a strategy to fix page size with WAL. + int32_t expectedPageSize = 0; + rv = mParticipant->GetExpectedDatabasePageSize(&expectedPageSize); + if (NS_FAILED(rv) || !Service::pageSizeIsValid(expectedPageSize)) { + NS_WARNING("Invalid page size requested for database, will use default "); + NS_WARNING(mDBFilename.get()); + expectedPageSize = Service::getDefaultPageSize(); + } + + // Get the database filename. Last vacuum time is stored under this name + // in PREF_VACUUM_BRANCH. + nsCOMPtr<nsIFile> databaseFile; + mDBConn->GetDatabaseFile(getter_AddRefs(databaseFile)); + if (!databaseFile) { + NS_WARNING("Trying to vacuum a in-memory database!"); + return false; + } + nsAutoString databaseFilename; + rv = databaseFile->GetLeafName(databaseFilename); + NS_ENSURE_SUCCESS(rv, false); + mDBFilename = NS_ConvertUTF16toUTF8(databaseFilename); + MOZ_ASSERT(!mDBFilename.IsEmpty(), "Database filename cannot be empty"); + + // Check interval from last vacuum. + int32_t now = static_cast<int32_t>(PR_Now() / PR_USEC_PER_SEC); + int32_t lastVacuum; + nsAutoCString prefName(PREF_VACUUM_BRANCH); + prefName += mDBFilename; + rv = Preferences::GetInt(prefName.get(), &lastVacuum); + if (NS_SUCCEEDED(rv) && (now - lastVacuum) < VACUUM_INTERVAL_SECONDS) { + // This database was vacuumed recently, skip it. + return false; + } + + // Notify that we are about to start vacuuming. The participant can opt-out + // if it cannot handle a vacuum at this time, and then we'll move to the next + // one. + bool vacuumGranted = false; + rv = mParticipant->OnBeginVacuum(&vacuumGranted); + NS_ENSURE_SUCCESS(rv, false); + if (!vacuumGranted) { + return false; + } + + // Notify a heavy IO task is about to start. + nsCOMPtr<nsIObserverService> os = mozilla::services::GetObserverService(); + if (os) { + rv = + os->NotifyObservers(nullptr, OBSERVER_TOPIC_HEAVY_IO, + OBSERVER_DATA_VACUUM_BEGIN.get()); + MOZ_ASSERT(NS_SUCCEEDED(rv), "Should be able to notify"); + } + + // Execute the statements separately, since the pragma may conflict with the + // vacuum, if they are executed in the same transaction. + nsCOMPtr<mozIStorageAsyncStatement> pageSizeStmt; + nsAutoCString pageSizeQuery(MOZ_STORAGE_UNIQUIFY_QUERY_STR + "PRAGMA page_size = "); + pageSizeQuery.AppendInt(expectedPageSize); + rv = mDBConn->CreateAsyncStatement(pageSizeQuery, + getter_AddRefs(pageSizeStmt)); + NS_ENSURE_SUCCESS(rv, false); + RefPtr<BaseCallback> callback = new BaseCallback(); + nsCOMPtr<mozIStoragePendingStatement> ps; + rv = pageSizeStmt->ExecuteAsync(callback, getter_AddRefs(ps)); + NS_ENSURE_SUCCESS(rv, false); + + nsCOMPtr<mozIStorageAsyncStatement> stmt; + rv = mDBConn->CreateAsyncStatement(NS_LITERAL_CSTRING( + "VACUUM" + ), getter_AddRefs(stmt)); + NS_ENSURE_SUCCESS(rv, false); + rv = stmt->ExecuteAsync(this, getter_AddRefs(ps)); + NS_ENSURE_SUCCESS(rv, false); + + return true; +} + +//////////////////////////////////////////////////////////////////////////////// +//// mozIStorageStatementCallback + +NS_IMETHODIMP +Vacuumer::HandleError(mozIStorageError *aError) +{ + int32_t result; + nsresult rv; + nsAutoCString message; + +#ifdef DEBUG + rv = aError->GetResult(&result); + NS_ENSURE_SUCCESS(rv, rv); + rv = aError->GetMessage(message); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoCString warnMsg; + warnMsg.AppendLiteral("Unable to vacuum database: "); + warnMsg.Append(mDBFilename); + warnMsg.AppendLiteral(" - "); + warnMsg.AppendInt(result); + warnMsg.Append(' '); + warnMsg.Append(message); + NS_WARNING(warnMsg.get()); +#endif + + if (MOZ_LOG_TEST(gStorageLog, LogLevel::Error)) { + rv = aError->GetResult(&result); + NS_ENSURE_SUCCESS(rv, rv); + rv = aError->GetMessage(message); + NS_ENSURE_SUCCESS(rv, rv); + MOZ_LOG(gStorageLog, LogLevel::Error, + ("Vacuum failed with error: %d '%s'. Database was: '%s'", + result, message.get(), mDBFilename.get())); + } + return NS_OK; +} + +NS_IMETHODIMP +Vacuumer::HandleResult(mozIStorageResultSet *aResultSet) +{ + NS_NOTREACHED("Got a resultset from a vacuum?"); + return NS_OK; +} + +NS_IMETHODIMP +Vacuumer::HandleCompletion(uint16_t aReason) +{ + if (aReason == REASON_FINISHED) { + // Update last vacuum time. + int32_t now = static_cast<int32_t>(PR_Now() / PR_USEC_PER_SEC); + MOZ_ASSERT(!mDBFilename.IsEmpty(), "Database filename cannot be empty"); + nsAutoCString prefName(PREF_VACUUM_BRANCH); + prefName += mDBFilename; + DebugOnly<nsresult> rv = Preferences::SetInt(prefName.get(), now); + MOZ_ASSERT(NS_SUCCEEDED(rv), "Should be able to set a preference"); + } + + notifyCompletion(aReason == REASON_FINISHED); + + return NS_OK; +} + +nsresult +Vacuumer::notifyCompletion(bool aSucceeded) +{ + nsCOMPtr<nsIObserverService> os = mozilla::services::GetObserverService(); + if (os) { + os->NotifyObservers(nullptr, OBSERVER_TOPIC_HEAVY_IO, + OBSERVER_DATA_VACUUM_END.get()); + } + + nsresult rv = mParticipant->OnEndVacuum(aSucceeded); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +} // namespace + +//////////////////////////////////////////////////////////////////////////////// +//// VacuumManager + +NS_IMPL_ISUPPORTS( + VacuumManager +, nsIObserver +) + +VacuumManager * +VacuumManager::gVacuumManager = nullptr; + +VacuumManager * +VacuumManager::getSingleton() +{ + //Don't allocate it in the child Process. + if (!XRE_IsParentProcess()) { + return nullptr; + } + + if (gVacuumManager) { + NS_ADDREF(gVacuumManager); + return gVacuumManager; + } + gVacuumManager = new VacuumManager(); + if (gVacuumManager) { + NS_ADDREF(gVacuumManager); + } + return gVacuumManager; +} + +VacuumManager::VacuumManager() + : mParticipants("vacuum-participant") +{ + MOZ_ASSERT(!gVacuumManager, + "Attempting to create two instances of the service!"); + gVacuumManager = this; +} + +VacuumManager::~VacuumManager() +{ + // Remove the static reference to the service. Check to make sure its us + // in case somebody creates an extra instance of the service. + MOZ_ASSERT(gVacuumManager == this, + "Deleting a non-singleton instance of the service"); + if (gVacuumManager == this) { + gVacuumManager = nullptr; + } +} + +//////////////////////////////////////////////////////////////////////////////// +//// nsIObserver + +NS_IMETHODIMP +VacuumManager::Observe(nsISupports *aSubject, + const char *aTopic, + const char16_t *aData) +{ + if (strcmp(aTopic, OBSERVER_TOPIC_IDLE_DAILY) == 0) { + // Try to run vacuum on all registered entries. Will stop at the first + // successful one. + nsCOMArray<mozIStorageVacuumParticipant> entries; + mParticipants.GetEntries(entries); + // If there are more entries than what a month can contain, we could end up + // skipping some, since we run daily. So we use a starting index. + static const char* kPrefName = PREF_VACUUM_BRANCH "index"; + int32_t startIndex = Preferences::GetInt(kPrefName, 0); + if (startIndex >= entries.Count()) { + startIndex = 0; + } + int32_t index; + for (index = startIndex; index < entries.Count(); ++index) { + RefPtr<Vacuumer> vacuum = new Vacuumer(entries[index]); + // Only vacuum one database per day. + if (vacuum->execute()) { + break; + } + } + DebugOnly<nsresult> rv = Preferences::SetInt(kPrefName, index); + MOZ_ASSERT(NS_SUCCEEDED(rv), "Should be able to set a preference"); + } + + return NS_OK; +} + +} // namespace storage +} // namespace mozilla diff --git a/storage/VacuumManager.h b/storage/VacuumManager.h new file mode 100644 index 000000000..12603deb6 --- /dev/null +++ b/storage/VacuumManager.h @@ -0,0 +1,45 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_storage_VacuumManager_h__ +#define mozilla_storage_VacuumManager_h__ + +#include "nsCOMPtr.h" +#include "nsIObserver.h" +#include "mozIStorageStatementCallback.h" +#include "mozIStorageVacuumParticipant.h" +#include "nsCategoryCache.h" +#include "mozilla/Attributes.h" + +namespace mozilla { +namespace storage { + +class VacuumManager final : public nsIObserver +{ +public: + NS_DECL_ISUPPORTS + NS_DECL_NSIOBSERVER + + VacuumManager(); + + /** + * Obtains the VacuumManager object. + */ + static VacuumManager * getSingleton(); + +private: + ~VacuumManager(); + + static VacuumManager *gVacuumManager; + + // Cache of components registered in "vacuum-participant" category. + nsCategoryCache<mozIStorageVacuumParticipant> mParticipants; +}; + +} // namespace storage +} // namespace mozilla + +#endif diff --git a/storage/Variant.h b/storage/Variant.h new file mode 100644 index 000000000..265abb02a --- /dev/null +++ b/storage/Variant.h @@ -0,0 +1,446 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_storage_Variant_h__ +#define mozilla_storage_Variant_h__ + +#include <utility> + +#include "nsIVariant.h" +#include "nsString.h" +#include "nsTArray.h" + +#define VARIANT_BASE_IID \ +{ /* 78888042-0fa3-4f7a-8b19-7996f99bf1aa */ \ + 0x78888042, 0x0fa3, 0x4f7a, \ + { 0x8b, 0x19, 0x79, 0x96, 0xf9, 0x9b, 0xf1, 0xaa } \ +} + +/** + * This class is used by the storage module whenever an nsIVariant needs to be + * returned. We provide traits for the basic sqlite types to make use easier. + * The following types map to the indicated sqlite type: + * int64_t -> INTEGER (use IntegerVariant) + * double -> FLOAT (use FloatVariant) + * nsString -> TEXT (use TextVariant) + * nsCString -> TEXT (use UTF8TextVariant) + * uint8_t[] -> BLOB (use BlobVariant) + * nullptr -> NULL (use NullVariant) + */ + +namespace mozilla { +namespace storage { + +//////////////////////////////////////////////////////////////////////////////// +//// Base Class + +class Variant_base : public nsIVariant +{ +public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIVARIANT + NS_DECLARE_STATIC_IID_ACCESSOR(VARIANT_BASE_IID) + +protected: + virtual ~Variant_base() { } +}; + +NS_DEFINE_STATIC_IID_ACCESSOR(Variant_base, + VARIANT_BASE_IID) + +//////////////////////////////////////////////////////////////////////////////// +//// Traits + +/** + * Generics + */ + +template <typename DataType> +struct variant_traits +{ + static inline uint16_t type() { return nsIDataType::VTYPE_EMPTY; } +}; + +template <typename DataType, bool Adopting=false> +struct variant_storage_traits +{ + typedef DataType ConstructorType; + typedef DataType StorageType; + static inline void storage_conversion(const ConstructorType aData, StorageType* _storage) + { + *_storage = aData; + } + + static inline void destroy(const StorageType& _storage) + { } +}; + +#define NO_CONVERSION return NS_ERROR_CANNOT_CONVERT_DATA; + +template <typename DataType, bool Adopting=false> +struct variant_integer_traits +{ + typedef typename variant_storage_traits<DataType, Adopting>::StorageType StorageType; + static inline nsresult asInt32(const StorageType &, int32_t *) { NO_CONVERSION } + static inline nsresult asInt64(const StorageType &, int64_t *) { NO_CONVERSION } +}; + +template <typename DataType, bool Adopting=false> +struct variant_float_traits +{ + typedef typename variant_storage_traits<DataType, Adopting>::StorageType StorageType; + static inline nsresult asDouble(const StorageType &, double *) { NO_CONVERSION } +}; + +template <typename DataType, bool Adopting=false> +struct variant_text_traits +{ + typedef typename variant_storage_traits<DataType, Adopting>::StorageType StorageType; + static inline nsresult asUTF8String(const StorageType &, nsACString &) { NO_CONVERSION } + static inline nsresult asString(const StorageType &, nsAString &) { NO_CONVERSION } +}; + +template <typename DataType, bool Adopting=false> +struct variant_blob_traits +{ + typedef typename variant_storage_traits<DataType, Adopting>::StorageType StorageType; + static inline nsresult asArray(const StorageType &, uint16_t *, uint32_t *, void **) + { NO_CONVERSION } +}; + +#undef NO_CONVERSION + +/** + * INTEGER types + */ + +template < > +struct variant_traits<int64_t> +{ + static inline uint16_t type() { return nsIDataType::VTYPE_INT64; } +}; +template < > +struct variant_integer_traits<int64_t> +{ + static inline nsresult asInt32(int64_t aValue, + int32_t *_result) + { + if (aValue > INT32_MAX || aValue < INT32_MIN) + return NS_ERROR_CANNOT_CONVERT_DATA; + + *_result = static_cast<int32_t>(aValue); + return NS_OK; + } + static inline nsresult asInt64(int64_t aValue, + int64_t *_result) + { + *_result = aValue; + return NS_OK; + } +}; +// xpcvariant just calls get double for integers... +template < > +struct variant_float_traits<int64_t> +{ + static inline nsresult asDouble(int64_t aValue, + double *_result) + { + *_result = double(aValue); + return NS_OK; + } +}; + +/** + * FLOAT types + */ + +template < > +struct variant_traits<double> +{ + static inline uint16_t type() { return nsIDataType::VTYPE_DOUBLE; } +}; +template < > +struct variant_float_traits<double> +{ + static inline nsresult asDouble(double aValue, + double *_result) + { + *_result = aValue; + return NS_OK; + } +}; + +/** + * TEXT types + */ + +template < > +struct variant_traits<nsString> +{ + static inline uint16_t type() { return nsIDataType::VTYPE_ASTRING; } +}; +template < > +struct variant_storage_traits<nsString> +{ + typedef const nsAString & ConstructorType; + typedef nsString StorageType; + static inline void storage_conversion(ConstructorType aText, StorageType* _outData) + { + *_outData = aText; + } + static inline void destroy(const StorageType& _outData) + { } +}; +template < > +struct variant_text_traits<nsString> +{ + static inline nsresult asUTF8String(const nsString &aValue, + nsACString &_result) + { + CopyUTF16toUTF8(aValue, _result); + return NS_OK; + } + static inline nsresult asString(const nsString &aValue, + nsAString &_result) + { + _result = aValue; + return NS_OK; + } +}; + +template < > +struct variant_traits<nsCString> +{ + static inline uint16_t type() { return nsIDataType::VTYPE_UTF8STRING; } +}; +template < > +struct variant_storage_traits<nsCString> +{ + typedef const nsACString & ConstructorType; + typedef nsCString StorageType; + static inline void storage_conversion(ConstructorType aText, StorageType* _outData) + { + *_outData = aText; + } + static inline void destroy(const StorageType &aData) + { } +}; +template < > +struct variant_text_traits<nsCString> +{ + static inline nsresult asUTF8String(const nsCString &aValue, + nsACString &_result) + { + _result = aValue; + return NS_OK; + } + static inline nsresult asString(const nsCString &aValue, + nsAString &_result) + { + CopyUTF8toUTF16(aValue, _result); + return NS_OK; + } +}; + +/** + * BLOB types + */ + +template < > +struct variant_traits<uint8_t[]> +{ + static inline uint16_t type() { return nsIDataType::VTYPE_ARRAY; } +}; +template < > +struct variant_storage_traits<uint8_t[], false> +{ + typedef std::pair<const void *, int> ConstructorType; + typedef FallibleTArray<uint8_t> StorageType; + static inline void storage_conversion(ConstructorType aBlob, StorageType* _outData) + { + _outData->Clear(); + (void)_outData->AppendElements(static_cast<const uint8_t *>(aBlob.first), + aBlob.second, fallible); + } + static inline void destroy(const StorageType& _outData) + { } +}; +template < > +struct variant_storage_traits<uint8_t[], true> +{ + typedef std::pair<uint8_t *, int> ConstructorType; + typedef std::pair<uint8_t *, int> StorageType; + static inline void storage_conversion(ConstructorType aBlob, StorageType* _outData) + { + *_outData = aBlob; + } + static inline void destroy(StorageType &aData) + { + if (aData.first) { + free(aData.first); + aData.first = nullptr; + } + } +}; +template < > +struct variant_blob_traits<uint8_t[], false> +{ + static inline nsresult asArray(FallibleTArray<uint8_t> &aData, + uint16_t *_type, + uint32_t *_size, + void **_result) + { + // For empty blobs, we return nullptr. + if (aData.Length() == 0) { + *_result = nullptr; + *_type = nsIDataType::VTYPE_UINT8; + *_size = 0; + return NS_OK; + } + + // Otherwise, we copy the array. + *_result = nsMemory::Clone(aData.Elements(), aData.Length() * sizeof(uint8_t)); + NS_ENSURE_TRUE(*_result, NS_ERROR_OUT_OF_MEMORY); + + // Set type and size + *_type = nsIDataType::VTYPE_UINT8; + *_size = aData.Length(); + return NS_OK; + } +}; + +template < > +struct variant_blob_traits<uint8_t[], true> +{ + static inline nsresult asArray(std::pair<uint8_t *, int> &aData, + uint16_t *_type, + uint32_t *_size, + void **_result) + { + // For empty blobs, we return nullptr. + if (aData.second == 0) { + *_result = nullptr; + *_type = nsIDataType::VTYPE_UINT8; + *_size = 0; + return NS_OK; + } + + // Otherwise, transfer the data out. + *_result = aData.first; + aData.first = nullptr; + MOZ_ASSERT(*_result); // We asked for it twice, better not use adopting! + + // Set type and size + *_type = nsIDataType::VTYPE_UINT8; + *_size = aData.second; + return NS_OK; + } +}; + +/** + * nullptr type + */ + +class NullVariant : public Variant_base +{ +public: + NS_IMETHOD GetDataType(uint16_t *_type) + { + NS_ENSURE_ARG_POINTER(_type); + *_type = nsIDataType::VTYPE_EMPTY; + return NS_OK; + } + + NS_IMETHOD GetAsAUTF8String(nsACString &_str) + { + // Return a void string. + _str.SetIsVoid(true); + return NS_OK; + } + + NS_IMETHOD GetAsAString(nsAString &_str) + { + // Return a void string. + _str.SetIsVoid(true); + return NS_OK; + } +}; + +//////////////////////////////////////////////////////////////////////////////// +//// Template Implementation + +template <typename DataType, bool Adopting=false> +class Variant final : public Variant_base +{ + ~Variant() + { + variant_storage_traits<DataType, Adopting>::destroy(mData); + } + +public: + explicit Variant(const typename variant_storage_traits<DataType, Adopting>::ConstructorType aData) + { + variant_storage_traits<DataType, Adopting>::storage_conversion(aData, &mData); + } + + NS_IMETHOD GetDataType(uint16_t *_type) + { + *_type = variant_traits<DataType>::type(); + return NS_OK; + } + NS_IMETHOD GetAsInt32(int32_t *_integer) + { + return variant_integer_traits<DataType, Adopting>::asInt32(mData, _integer); + } + + NS_IMETHOD GetAsInt64(int64_t *_integer) + { + return variant_integer_traits<DataType, Adopting>::asInt64(mData, _integer); + } + + NS_IMETHOD GetAsDouble(double *_double) + { + return variant_float_traits<DataType, Adopting>::asDouble(mData, _double); + } + + NS_IMETHOD GetAsAUTF8String(nsACString &_str) + { + return variant_text_traits<DataType, Adopting>::asUTF8String(mData, _str); + } + + NS_IMETHOD GetAsAString(nsAString &_str) + { + return variant_text_traits<DataType, Adopting>::asString(mData, _str); + } + + NS_IMETHOD GetAsArray(uint16_t *_type, + nsIID *, + uint32_t *_size, + void **_data) + { + return variant_blob_traits<DataType, Adopting>::asArray(mData, _type, _size, _data); + } + +private: + typename variant_storage_traits<DataType, Adopting>::StorageType mData; +}; + +//////////////////////////////////////////////////////////////////////////////// +//// Handy typedefs! Use these for the right mapping. + +typedef Variant<int64_t> IntegerVariant; +typedef Variant<double> FloatVariant; +typedef Variant<nsString> TextVariant; +typedef Variant<nsCString> UTF8TextVariant; +typedef Variant<uint8_t[], false> BlobVariant; +typedef Variant<uint8_t[], true> AdoptedBlobVariant; + +} // namespace storage +} // namespace mozilla + +#include "Variant_inl.h" + +#endif // mozilla_storage_Variant_h__ diff --git a/storage/Variant_inl.h b/storage/Variant_inl.h new file mode 100644 index 000000000..2e0571ae2 --- /dev/null +++ b/storage/Variant_inl.h @@ -0,0 +1,229 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Note: This file is included by Variant.h. + */ + +#ifndef mozilla_storage_Variant_h__ +#error "Do not include this file directly!" +#endif + +namespace mozilla { +namespace storage { + +//////////////////////////////////////////////////////////////////////////////// +//// Variant_base + +inline NS_IMPL_ADDREF(Variant_base) +inline NS_IMPL_RELEASE(Variant_base) +inline NS_IMPL_QUERY_INTERFACE( + Variant_base, + nsIVariant +) + +//////////////////////////////////////////////////////////////////////////////// +//// nsIVariant + +inline +NS_IMETHODIMP +Variant_base::GetDataType(uint16_t *_type) +{ + NS_ENSURE_ARG_POINTER(_type); + *_type = nsIDataType::VTYPE_VOID; + return NS_OK; +} + +inline +NS_IMETHODIMP +Variant_base::GetAsInt32(int32_t *) +{ + return NS_ERROR_CANNOT_CONVERT_DATA; +} + +inline +NS_IMETHODIMP +Variant_base::GetAsInt64(int64_t *) +{ + return NS_ERROR_CANNOT_CONVERT_DATA; +} + +inline +NS_IMETHODIMP +Variant_base::GetAsDouble(double *) +{ + return NS_ERROR_CANNOT_CONVERT_DATA; +} + +inline +NS_IMETHODIMP +Variant_base::GetAsAUTF8String(nsACString &) +{ + return NS_ERROR_CANNOT_CONVERT_DATA; +} + +inline +NS_IMETHODIMP +Variant_base::GetAsAString(nsAString &) +{ + return NS_ERROR_CANNOT_CONVERT_DATA; +} + +inline +NS_IMETHODIMP +Variant_base::GetAsArray(uint16_t *, + nsIID *, + uint32_t *, + void **) +{ + return NS_ERROR_CANNOT_CONVERT_DATA; +} + +inline +NS_IMETHODIMP +Variant_base::GetAsInt8(uint8_t *) +{ + return NS_ERROR_CANNOT_CONVERT_DATA; +} + +inline +NS_IMETHODIMP +Variant_base::GetAsInt16(int16_t *) +{ + return NS_ERROR_CANNOT_CONVERT_DATA; +} + +inline +NS_IMETHODIMP +Variant_base::GetAsUint8(uint8_t *) +{ + return NS_ERROR_CANNOT_CONVERT_DATA; +} + +inline +NS_IMETHODIMP +Variant_base::GetAsUint16(uint16_t *) +{ + return NS_ERROR_CANNOT_CONVERT_DATA; +} + +inline +NS_IMETHODIMP +Variant_base::GetAsUint32(uint32_t *) +{ + return NS_ERROR_CANNOT_CONVERT_DATA; +} + +inline +NS_IMETHODIMP +Variant_base::GetAsUint64(uint64_t *) +{ + return NS_ERROR_CANNOT_CONVERT_DATA; +} + +inline +NS_IMETHODIMP +Variant_base::GetAsFloat(float *) +{ + return NS_ERROR_CANNOT_CONVERT_DATA; +} + +inline +NS_IMETHODIMP +Variant_base::GetAsBool(bool *) +{ + return NS_ERROR_CANNOT_CONVERT_DATA; +} + +inline +NS_IMETHODIMP +Variant_base::GetAsChar(char *) +{ + return NS_ERROR_CANNOT_CONVERT_DATA; +} + +inline +NS_IMETHODIMP +Variant_base::GetAsWChar(char16_t *) +{ + return NS_ERROR_CANNOT_CONVERT_DATA; +} + +inline +NS_IMETHODIMP +Variant_base::GetAsID(nsID *) +{ + return NS_ERROR_CANNOT_CONVERT_DATA; +} + +inline +NS_IMETHODIMP +Variant_base::GetAsDOMString(nsAString &) +{ + return NS_ERROR_CANNOT_CONVERT_DATA; +} + +inline +NS_IMETHODIMP +Variant_base::GetAsString(char **) +{ + return NS_ERROR_CANNOT_CONVERT_DATA; +} + +inline +NS_IMETHODIMP +Variant_base::GetAsWString(char16_t **) +{ + return NS_ERROR_CANNOT_CONVERT_DATA; +} + +inline +NS_IMETHODIMP +Variant_base::GetAsISupports(nsISupports **) +{ + return NS_ERROR_CANNOT_CONVERT_DATA; +} + +inline +NS_IMETHODIMP +Variant_base::GetAsInterface(nsIID **, + void **) +{ + return NS_ERROR_CANNOT_CONVERT_DATA; +} + +inline +NS_IMETHODIMP +Variant_base::GetAsACString(nsACString &) +{ + return NS_ERROR_CANNOT_CONVERT_DATA; +} + +inline +NS_IMETHODIMP +Variant_base::GetAsStringWithSize(uint32_t *, + char **) +{ + return NS_ERROR_CANNOT_CONVERT_DATA; +} + +inline +NS_IMETHODIMP +Variant_base::GetAsWStringWithSize(uint32_t *, + char16_t **) +{ + return NS_ERROR_CANNOT_CONVERT_DATA; +} + +inline +NS_IMETHODIMP +Variant_base::GetAsJSVal(JS::MutableHandle<JS::Value>) +{ + return NS_ERROR_CANNOT_CONVERT_DATA; +} + +} // namespace storage +} // namespace mozilla diff --git a/storage/build/moz.build b/storage/build/moz.build new file mode 100644 index 000000000..284cd7e3f --- /dev/null +++ b/storage/build/moz.build @@ -0,0 +1,21 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +EXPORTS += [ + 'mozStorageCID.h', +] + +SOURCES += [ + 'mozStorageModule.cpp', +] + +FINAL_LIBRARY = 'xul' + +LOCAL_INCLUDES += [ + '..', +] + +CXXFLAGS += CONFIG['SQLITE_CFLAGS'] diff --git a/storage/build/mozStorageCID.h b/storage/build/mozStorageCID.h new file mode 100644 index 000000000..c682d07dd --- /dev/null +++ b/storage/build/mozStorageCID.h @@ -0,0 +1,30 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef MOZSTORAGECID_H +#define MOZSTORAGECID_H + +#define MOZ_STORAGE_CONTRACTID_PREFIX "@mozilla.org/storage" + + +/* b71a1f84-3a70-4d37-a348-f1ba0e27eead */ +#define MOZ_STORAGE_CONNECTION_CID \ +{ 0xb71a1f84, 0x3a70, 0x4d37, {0xa3, 0x48, 0xf1, 0xba, 0x0e, 0x27, 0xee, 0xad} } + +#define MOZ_STORAGE_CONNECTION_CONTRACTID MOZ_STORAGE_CONTRACTID_PREFIX "/connection;1" + +/* bbbb1d61-438f-4436-92ed-8308e5830fb0 */ +#define MOZ_STORAGE_SERVICE_CID \ +{ 0xbbbb1d61, 0x438f, 0x4436, {0x92, 0xed, 0x83, 0x08, 0xe5, 0x83, 0x0f, 0xb0} } + +#define MOZ_STORAGE_SERVICE_CONTRACTID MOZ_STORAGE_CONTRACTID_PREFIX "/service;1" + +/* 3b667ee0-d2da-4ccc-9c3d-95f2ca6a8b4c */ +#define VACUUMMANAGER_CID \ +{ 0x3b667ee0, 0xd2da, 0x4ccc, { 0x9c, 0x3d, 0x95, 0xf2, 0xca, 0x6a, 0x8b, 0x4c } } + +#define VACUUMMANAGER_CONTRACTID MOZ_STORAGE_CONTRACTID_PREFIX "/vacuum;1" + +#endif /* MOZSTORAGECID_H */ diff --git a/storage/build/mozStorageModule.cpp b/storage/build/mozStorageModule.cpp new file mode 100644 index 000000000..ba77e4c62 --- /dev/null +++ b/storage/build/mozStorageModule.cpp @@ -0,0 +1,53 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsCOMPtr.h" +#include "mozilla/ModuleUtils.h" + +#include "mozStorageService.h" +#include "mozStorageConnection.h" +#include "VacuumManager.h" + +#include "mozStorageCID.h" + +namespace mozilla { +namespace storage { + +NS_GENERIC_FACTORY_SINGLETON_CONSTRUCTOR(Service, + Service::getSingleton) +NS_GENERIC_FACTORY_SINGLETON_CONSTRUCTOR(VacuumManager, + VacuumManager::getSingleton) + +} // namespace storage +} // namespace mozilla + +NS_DEFINE_NAMED_CID(MOZ_STORAGE_SERVICE_CID); +NS_DEFINE_NAMED_CID(VACUUMMANAGER_CID); + +static const mozilla::Module::CIDEntry kStorageCIDs[] = { + { &kMOZ_STORAGE_SERVICE_CID, false, nullptr, mozilla::storage::ServiceConstructor }, + { &kVACUUMMANAGER_CID, false, nullptr, mozilla::storage::VacuumManagerConstructor }, + { nullptr } +}; + +static const mozilla::Module::ContractIDEntry kStorageContracts[] = { + { MOZ_STORAGE_SERVICE_CONTRACTID, &kMOZ_STORAGE_SERVICE_CID }, + { VACUUMMANAGER_CONTRACTID, &kVACUUMMANAGER_CID }, + { nullptr } +}; + +static const mozilla::Module::CategoryEntry kStorageCategories[] = { + { "idle-daily", "MozStorage Vacuum Manager", VACUUMMANAGER_CONTRACTID }, + { nullptr } +}; + +static const mozilla::Module kStorageModule = { + mozilla::Module::kVersion, + kStorageCIDs, + kStorageContracts, + kStorageCategories +}; + +NSMODULE_DEFN(mozStorageModule) = &kStorageModule; diff --git a/storage/moz.build b/storage/moz.build new file mode 100644 index 000000000..8863105c9 --- /dev/null +++ b/storage/moz.build @@ -0,0 +1,121 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +DIRS += ['build'] + +TEST_DIRS += ['test'] + +XPIDL_SOURCES += [ + 'mozIStorageAggregateFunction.idl', + 'mozIStorageAsyncConnection.idl', + 'mozIStorageAsyncStatement.idl', + 'mozIStorageBaseStatement.idl', + 'mozIStorageBindingParams.idl', + 'mozIStorageBindingParamsArray.idl', + 'mozIStorageCompletionCallback.idl', + 'mozIStorageConnection.idl', + 'mozIStorageError.idl', + 'mozIStorageFunction.idl', + 'mozIStoragePendingStatement.idl', + 'mozIStorageProgressHandler.idl', + 'mozIStorageResultSet.idl', + 'mozIStorageRow.idl', + 'mozIStorageService.idl', + 'mozIStorageStatement.idl', + 'mozIStorageStatementCallback.idl', + 'mozIStorageStatementParams.idl', + 'mozIStorageStatementRow.idl', + 'mozIStorageVacuumParticipant.idl', + 'mozIStorageValueArray.idl', +] + +XPIDL_MODULE = 'storage' + +EXPORTS += [ + 'mozStorageHelper.h', +] + +EXPORTS.mozilla += [ + 'storage.h', +] + +# NOTE When adding something to this list, you probably need to add it to the +# storage.h file too. +EXPORTS.mozilla.storage += [ + 'StatementCache.h', + 'Variant.h', + 'Variant_inl.h', +] +# SEE ABOVE NOTE! + +UNIFIED_SOURCES += [ + 'FileSystemModule.cpp', + 'mozStorageArgValueArray.cpp', + 'mozStorageAsyncStatement.cpp', + 'mozStorageAsyncStatementExecution.cpp', + 'mozStorageAsyncStatementJSHelper.cpp', + 'mozStorageAsyncStatementParams.cpp', + 'mozStorageBindingParamsArray.cpp', + 'mozStorageError.cpp', + 'mozStoragePrivateHelpers.cpp', + 'mozStorageResultSet.cpp', + 'mozStorageRow.cpp', + 'mozStorageService.cpp', + 'mozStorageSQLFunctions.cpp', + 'mozStorageStatement.cpp', + 'mozStorageStatementJSHelper.cpp', + 'mozStorageStatementParams.cpp', + 'mozStorageStatementRow.cpp', + 'SQLCollations.cpp', + 'StorageBaseStatementInternal.cpp', + 'TelemetryVFS.cpp', + 'VacuumManager.cpp', +] + +# These files need to be built separately because they #include variantToSQLiteT_impl.h. +SOURCES += [ + 'mozStorageBindingParams.cpp', + 'mozStorageConnection.cpp', +] + +include('/ipc/chromium/chromium-config.mozbuild') + +FINAL_LIBRARY = 'xul' + +# Don't use the jemalloc allocator on Android, because we can't guarantee +# that Gecko will configure sqlite before it is first used (bug 730495). +# +# Don't use the jemalloc allocator when using system sqlite. Linked in libraries +# (such as NSS) might trigger an initialization of sqlite and allocation +# of memory using the default allocator, prior to the storage service +# registering its allocator, causing memory management failures (bug 938730). +# However, this is not an issue if both the jemalloc allocator and the default +# allocator are the same thing. +# +# Note: On Windows our sqlite build assumes we use jemalloc. If you disable +# MOZ_STORAGE_MEMORY on Windows, you will also need to change the "ifdef +# MOZ_MEMORY" options in db/sqlite3/src/Makefile.in. +if CONFIG['MOZ_MEMORY'] and (not CONFIG['MOZ_SYSTEM_SQLITE'] + or CONFIG['MOZ_SYSTEM_JEMALLOC']): + if CONFIG['OS_TARGET'] != 'Android': + DEFINES['MOZ_STORAGE_MEMORY'] = True + +# This is the default value. If we ever change it when compiling sqlite, we +# will need to change it here as well. +DEFINES['SQLITE_MAX_LIKE_PATTERN_LENGTH'] = 50000 + +# See Sqlite moz.build for reasoning about TEMP_STORE. +# For system sqlite we cannot use the compile time option, so we use a pragma. +if CONFIG['MOZ_SYSTEM_SQLITE'] and (CONFIG['OS_TARGET'] == 'Android' + or CONFIG['HAVE_64BIT_BUILD']): + DEFINES['MOZ_MEMORY_TEMP_STORE_PRAGMA'] = True + +LOCAL_INCLUDES += [ + '/db/sqlite3/src', + '/dom/base', +] + +CXXFLAGS += CONFIG['SQLITE_CFLAGS'] diff --git a/storage/mozIStorageAggregateFunction.idl b/storage/mozIStorageAggregateFunction.idl new file mode 100644 index 000000000..28579318d --- /dev/null +++ b/storage/mozIStorageAggregateFunction.idl @@ -0,0 +1,38 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" + +interface mozIStorageConnection; +interface mozIStorageValueArray; +interface nsIArray; +interface nsIVariant; + +/** + * mozIStorageAggregateFunction represents aggregate SQL function. + * Common examples of aggregate functions are SUM() and COUNT(). + * + * An aggregate function calculates one result for a given set of data, where + * a set of data is a group of tuples. There can be one group + * per request or many of them, if GROUP BY clause is used or not. + */ +[scriptable, uuid(763217b7-3123-11da-918d-000347412e16)] +interface mozIStorageAggregateFunction : nsISupports { + /** + * onStep is called when next value should be passed to + * a custom function. + * + * @param aFunctionArguments The arguments passed in to the function + */ + void onStep(in mozIStorageValueArray aFunctionArguments); + + /** + * Called when all tuples in a group have been processed and the engine + * needs the aggregate function's value. + * + * @returns aggregate result as Variant. + */ + nsIVariant onFinal(); +}; diff --git a/storage/mozIStorageAsyncConnection.idl b/storage/mozIStorageAsyncConnection.idl new file mode 100644 index 000000000..aeb2bf1b5 --- /dev/null +++ b/storage/mozIStorageAsyncConnection.idl @@ -0,0 +1,220 @@ +/* -*- Mode: idl; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" + +interface mozIStorageAggregateFunction; +interface mozIStorageCompletionCallback; +interface mozIStorageFunction; +interface mozIStorageProgressHandler; +interface mozIStorageBaseStatement; +interface mozIStorageStatement; +interface mozIStorageAsyncStatement; +interface mozIStorageStatementCallback; +interface mozIStoragePendingStatement; +interface nsIFile; + +/** + * mozIStorageAsyncConnection represents an asynchronous database + * connection attached to a specific file or to an in-memory data + * storage. It is the primary interface for interacting with a + * database from the main thread, including creating prepared + * statements, executing SQL, and examining database errors. + */ +[scriptable, uuid(8bfd34d5-4ddf-4e4b-89dd-9b14f33534c6)] +interface mozIStorageAsyncConnection : nsISupports { + /** + * Close this database connection, allowing all pending statements + * to complete first. + * + * @param aCallback [optional] + * A callback that will be notified when the close is completed, + * with the following arguments: + * - status: the status of the call + * - value: |null| + * + * @throws NS_ERROR_NOT_SAME_THREAD + * If called on a thread other than the one that opened it. The + * callback will not be dispatched. + * @throws NS_ERROR_NOT_INITIALIZED + * If called on a connection that has already been closed or was + * never properly opened. The callback will still be dispatched + * to the main thread despite the returned error. + */ + void asyncClose([optional] in mozIStorageCompletionCallback aCallback); + + /** + * Clone a database and make the clone read only if needed. + * SQL Functions and attached on-disk databases are applied to the new clone. + * + * @param aReadOnly + * If true, the returned database should be put into read-only mode. + * + * @param aCallback + * A callback that will be notified when the operation is complete, + * with the following arguments: + * - status: the status of the operation + * - value: in case of success, an intance of + * mozIStorageAsyncConnection cloned from this one. + * + * @throws NS_ERROR_NOT_SAME_THREAD + * If is called on a thread other than the one that opened it. + * @throws NS_ERROR_UNEXPECTED + * If this connection is a memory database. + * + * @note If your connection is already read-only, you will get a read-only + * clone. + * @note Due to a bug in SQLite, if you use the shared cache + * (see mozIStorageService), you end up with the same privileges as the + * first connection opened regardless of what is specified in aReadOnly. + * @note The following pragmas are copied over to a read-only clone: + * - cache_size + * - temp_store + * The following pragmas are copied over to a writeable clone: + * - cache_size + * - temp_store + * - foreign_keys + * - journal_size_limit + * - synchronous + * - wal_autocheckpoint + */ + void asyncClone(in boolean aReadOnly, + in mozIStorageCompletionCallback aCallback); + + /** + * The current database nsIFile. Null if the database + * connection refers to an in-memory database. + */ + readonly attribute nsIFile databaseFile; + + ////////////////////////////////////////////////////////////////////////////// + //// Statement creation + + /** + * Create an asynchronous statement for the given SQL. An + * asynchronous statement can only be used to dispatch asynchronous + * requests to the asynchronous execution thread and cannot be used + * to take any synchronous actions on the database. + * + * The expression may use ? to indicate sequential numbered arguments, + * ?1, ?2 etc. to indicate specific numbered arguments or :name and + * $var to indicate named arguments. + * + * @param aSQLStatement + * The SQL statement to execute. + * @return a new mozIStorageAsyncStatement + * @note The statement is created lazily on first execution. + */ + mozIStorageAsyncStatement createAsyncStatement(in AUTF8String aSQLStatement); + + /** + * Execute an array of statements created with this connection using + * any currently bound parameters. When the array contains multiple + * statements, the execution is wrapped in a single + * transaction. These statements can be reused immediately, and + * reset does not need to be called. + * + * @param aStatements + * The array of statements to execute asynchronously, in the order they + * are given in the array. + * @param aNumStatements + * The number of statements in aStatements. + * @param aCallback [optional] + * The callback object that will be notified of progress, errors, and + * completion. + * @return an object that can be used to cancel the statements execution. + * + * @note If you have any custom defined functions, they must be + * re-entrant since they can be called on multiple threads. + */ + mozIStoragePendingStatement executeAsync( + [array, size_is(aNumStatements)] in mozIStorageBaseStatement aStatements, + in unsigned long aNumStatements, + [optional] in mozIStorageStatementCallback aCallback + ); + + /** + * Execute asynchronously an SQL expression, expecting no arguments. + * + * @param aSQLStatement + * The SQL statement to execute + * @param aCallback [optional] + * The callback object that will be notified of progress, errors, and + * completion. + * @return an object that can be used to cancel the statement execution. + */ + mozIStoragePendingStatement executeSimpleSQLAsync( + in AUTF8String aSQLStatement, + [optional] in mozIStorageStatementCallback aCallback); + + ////////////////////////////////////////////////////////////////////////////// + //// Functions + + /** + * Create a new SQL function. If you use your connection on multiple threads, + * your function needs to be threadsafe, or it should only be called on one + * thread. + * + * @param aFunctionName + * The name of function to create, as seen in SQL. + * @param aNumArguments + * The number of arguments the function takes. Pass -1 for + * variable-argument functions. + * @param aFunction + * The instance of mozIStorageFunction, which implements the function + * in question. + */ + void createFunction(in AUTF8String aFunctionName, + in long aNumArguments, + in mozIStorageFunction aFunction); + + /** + * Create a new SQL aggregate function. If you use your connection on + * multiple threads, your function needs to be threadsafe, or it should only + * be called on one thread. + * + * @param aFunctionName + * The name of aggregate function to create, as seen in SQL. + * @param aNumArguments + * The number of arguments the function takes. Pass -1 for + * variable-argument functions. + * @param aFunction + * The instance of mozIStorageAggreagteFunction, which implements the + * function in question. + */ + void createAggregateFunction(in AUTF8String aFunctionName, + in long aNumArguments, + in mozIStorageAggregateFunction aFunction); + /** + * Delete custom SQL function (simple or aggregate one). + * + * @param aFunctionName + * The name of function to remove. + */ + void removeFunction(in AUTF8String aFunctionName); + + /** + * Sets a progress handler. Only one handler can be registered at a time. + * If you need more than one, you need to chain them yourself. This progress + * handler should be threadsafe if you use this connection object on more than + * one thread. + * + * @param aGranularity + * The number of SQL virtual machine steps between progress handler + * callbacks. + * @param aHandler + * The instance of mozIStorageProgressHandler. + * @return previous registered handler. + */ + mozIStorageProgressHandler setProgressHandler(in int32_t aGranularity, + in mozIStorageProgressHandler aHandler); + + /** + * Remove a progress handler. + * + * @return previous registered handler. + */ + mozIStorageProgressHandler removeProgressHandler(); +}; diff --git a/storage/mozIStorageAsyncStatement.idl b/storage/mozIStorageAsyncStatement.idl new file mode 100644 index 000000000..41d03f122 --- /dev/null +++ b/storage/mozIStorageAsyncStatement.idl @@ -0,0 +1,37 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 sts=2 expandtab + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozIStorageBaseStatement.idl" + +/** + * An asynchronous SQL statement. This differs from mozIStorageStatement by + * only being usable for asynchronous execution. (mozIStorageStatement can + * be used for both synchronous and asynchronous purposes.) This specialization + * for asynchronous operation allows us to avoid needing to acquire + * synchronization primitives also used by the asynchronous execution thread. + * In contrast, mozIStorageStatement may need to acquire the primitives and + * consequently can cause the main thread to lock for extended intervals while + * the asynchronous thread performs some long-running operation. + */ +[scriptable, uuid(52e49370-3b2e-4a27-a3fc-79e20ad4056b)] +interface mozIStorageAsyncStatement : mozIStorageBaseStatement { + /* + * 'params' provides a magic JS helper that lets you assign parameters by + * name. Unlike the helper on mozIStorageStatement, you cannot enumerate + * in order to find out what parameters are legal. + * + * This does not work for BLOBs. You must use an explicit binding API for + * that. + * + * example: + * stmt.params.foo = 1; + * stmt.params["bar"] = 2; + * let argName = "baz"; + * stmt.params[argName] = 3; + * + * readonly attribute nsIMagic params; + */ +}; diff --git a/storage/mozIStorageBaseStatement.idl b/storage/mozIStorageBaseStatement.idl new file mode 100644 index 000000000..52cd30500 --- /dev/null +++ b/storage/mozIStorageBaseStatement.idl @@ -0,0 +1,158 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 sts=2 et + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" +#include "mozIStorageBindingParams.idl" + +interface mozIStorageConnection; +interface mozIStorageStatementCallback; +interface mozIStoragePendingStatement; +interface mozIStorageBindingParams; +interface mozIStorageBindingParamsArray; + +/** + * The base interface for both pure asynchronous storage statements + * (mozIStorageAsyncStatement) and 'classic' storage statements + * (mozIStorageStatement) that can be used for both synchronous and asynchronous + * purposes. + */ +[scriptable, uuid(16ca67aa-1325-43e2-aac7-859afd1590b2)] +interface mozIStorageBaseStatement : mozIStorageBindingParams { + /** + * Finalizes a statement so you can successfully close a database connection. + * Once a statement has been finalized it can no longer be used for any + * purpose. + * + * Statements are implicitly finalized when their reference counts hits zero. + * If you are a native (C++) caller this is accomplished by setting all of + * your nsCOMPtr instances to be NULL. If you are operating from JavaScript + * code then you cannot rely on this behavior because of the involvement of + * garbage collection. + * + * When finalizing an asynchronous statement you do not need to worry about + * whether the statement has actually been executed by the asynchronous + * thread; you just need to call finalize after your last call to executeAsync + * involving the statement. However, you do need to use asyncClose instead of + * close on the connection if any statements have been used asynchronously. + */ + void finalize(); + + /** + * Bind the given value at the given numeric index. + * + * @param aParamIndex + * 0-based index, 0 corresponding to the first numbered argument or + * "?1". + * @param aValue + * Argument value. + * @param aValueSize + * Length of aValue in bytes. + * @{ + */ + [deprecated] void bindUTF8StringParameter(in unsigned long aParamIndex, + in AUTF8String aValue); + [deprecated] void bindStringParameter(in unsigned long aParamIndex, + in AString aValue); + [deprecated] void bindDoubleParameter(in unsigned long aParamIndex, + in double aValue); + [deprecated] void bindInt32Parameter(in unsigned long aParamIndex, + in long aValue); + [deprecated] void bindInt64Parameter(in unsigned long aParamIndex, + in long long aValue); + [deprecated] void bindNullParameter(in unsigned long aParamIndex); + [deprecated] void bindBlobParameter( + in unsigned long aParamIndex, + [array,const,size_is(aValueSize)] in octet aValue, + in unsigned long aValueSize); + [deprecated] void bindStringAsBlobParameter( + in unsigned long aParamIndex, + in AString aValue); + [deprecated] void bindUTF8StringAsBlobParameter( + in unsigned long aParamIndex, + in AUTF8String aValue); + [deprecated] void bindAdoptedBlobParameter( + in unsigned long aParamIndex, + [array,size_is(aValueSize)] in octet aValue, + in unsigned long aValueSize); + /**@}*/ + + /** + * Binds the array of parameters to the statement. When executeAsync is + * called, all the parameters in aParameters are bound and then executed. + * + * @param aParameters + * The array of parameters to bind to the statement upon execution. + * + * @note This is only works on statements being used asynchronously. + */ + void bindParameters(in mozIStorageBindingParamsArray aParameters); + + /** + * Creates a new mozIStorageBindingParamsArray that can be used to bind + * multiple sets of data to a statement with bindParameters. + * + * @return a mozIStorageBindingParamsArray that multiple sets of parameters + * can be bound to. + * + * @note This is only useful for statements being used asynchronously. + */ + mozIStorageBindingParamsArray newBindingParamsArray(); + + /** + * Execute a query asynchronously using any currently bound parameters. This + * statement can be reused immediately, and reset does not need to be called. + * + * @note If you have any custom defined functions, they must be re-entrant + * since they can be called on multiple threads. + * + * @param aCallback [optional] + * The callback object that will be notified of progress, errors, and + * completion. + * @return an object that can be used to cancel the statements execution. + */ + mozIStoragePendingStatement executeAsync( + [optional] in mozIStorageStatementCallback aCallback + ); + + /** + * The statement is not usable, either because it failed to initialize or + * was explicitly finalized. + */ + const long MOZ_STORAGE_STATEMENT_INVALID = 0; + /** + * The statement is usable. + */ + const long MOZ_STORAGE_STATEMENT_READY = 1; + /** + * Indicates that the statement is executing and the row getters may be used. + * + * @note This is only relevant for mozIStorageStatement instances being used + * in a synchronous fashion. + */ + const long MOZ_STORAGE_STATEMENT_EXECUTING = 2; + + /** + * Find out whether the statement is usable (has not been finalized). + */ + readonly attribute long state; + + /** + * Escape a string for SQL LIKE search. + * + * @note Consumers will have to use same escape char when doing statements + * such as: ...LIKE '?1' ESCAPE '/'... + * + * @param aValue + * The string to escape for SQL LIKE. + * @param aEscapeChar + * The escape character. + * @return an AString of an escaped version of aValue + * (%, _ and the escape char are escaped with the escape char) + * For example, we will convert "foo/bar_baz%20cheese" + * into "foo//bar/_baz/%20cheese" (if the escape char is '/'). + */ + AString escapeStringForLIKE(in AString aValue, in wchar aEscapeChar); +}; diff --git a/storage/mozIStorageBindingParams.idl b/storage/mozIStorageBindingParams.idl new file mode 100644 index 000000000..2c537aaf0 --- /dev/null +++ b/storage/mozIStorageBindingParams.idl @@ -0,0 +1,86 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 sts=2 et + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" + +interface nsIVariant; + +[scriptable, uuid(2d09f42f-966e-4663-b4b3-b0c8676bf2bf)] +interface mozIStorageBindingParams : nsISupports { + /** + * Binds aValue to the parameter with the name aName. + * + * @param aName + * The name of the parameter to bind aValue to. + * @param aValue + * The value to bind. + */ + void bindByName(in AUTF8String aName, + in nsIVariant aValue); + [noscript] void bindUTF8StringByName(in AUTF8String aName, + in AUTF8String aValue); + [noscript] void bindStringByName(in AUTF8String aName, + in AString aValue); + [noscript] void bindDoubleByName(in AUTF8String aName, + in double aValue); + [noscript] void bindInt32ByName(in AUTF8String aName, + in long aValue); + [noscript] void bindInt64ByName(in AUTF8String aName, + in long long aValue); + [noscript] void bindNullByName(in AUTF8String aName); + void bindBlobByName(in AUTF8String aName, + [array, const, size_is(aValueSize)] in octet aValue, + in unsigned long aValueSize); + + // Convenience routines for storing strings as blobs. + void bindStringAsBlobByName(in AUTF8String aName, in AString aValue); + void bindUTF8StringAsBlobByName(in AUTF8String aName, in AUTF8String aValue); + + // The function adopts the storage for the provided blob. After calling + // this function, mozStorage will ensure that free is called on the + // underlying pointer. + [noscript] + void bindAdoptedBlobByName(in AUTF8String aName, + [array, size_is(aValueSize)] in octet aValue, + in unsigned long aValueSize); + + /** + * Binds aValue to the parameter with the index aIndex. + * + * @param aIndex + * The zero-based index of the parameter to bind aValue to. + * @param aValue + * The value to bind. + */ + void bindByIndex(in unsigned long aIndex, + in nsIVariant aValue); + [noscript] void bindUTF8StringByIndex(in unsigned long aIndex, + in AUTF8String aValue); + [noscript] void bindStringByIndex(in unsigned long aIndex, + in AString aValue); + [noscript] void bindDoubleByIndex(in unsigned long aIndex, + in double aValue); + [noscript] void bindInt32ByIndex(in unsigned long aIndex, + in long aValue); + [noscript] void bindInt64ByIndex(in unsigned long aIndex, + in long long aValue); + [noscript] void bindNullByIndex(in unsigned long aIndex); + void bindBlobByIndex(in unsigned long aIndex, + [array, const, size_is(aValueSize)] in octet aValue, + in unsigned long aValueSize); + + // Convenience routines for storing strings as blobs. + void bindStringAsBlobByIndex(in unsigned long aIndex, in AString aValue); + void bindUTF8StringAsBlobByIndex(in unsigned long aIndex, in AUTF8String aValue); + + // The function adopts the storage for the provided blob. After calling + // this function, mozStorage will ensure that free is called on the + // underlying pointer. + [noscript] + void bindAdoptedBlobByIndex(in unsigned long aIndex, + [array, size_is(aValueSize)] in octet aValue, + in unsigned long aValueSize); +}; diff --git a/storage/mozIStorageBindingParamsArray.idl b/storage/mozIStorageBindingParamsArray.idl new file mode 100644 index 000000000..5f504c051 --- /dev/null +++ b/storage/mozIStorageBindingParamsArray.idl @@ -0,0 +1,34 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 sts=2 et + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" + +interface mozIStorageBindingParams; + +[scriptable, uuid(67eea5c3-4881-41ff-b0fe-09f2356aeadb)] +interface mozIStorageBindingParamsArray : nsISupports { + /** + * Creates a new mozIStorageBindingParams object that can be added to this + * array. + * + * @return a mozIStorageBindingParams object that can be used to specify + * parameters that need to be bound. + */ + mozIStorageBindingParams newBindingParams(); + + /** + * Adds the parameters to the end of this array. + * + * @param aParameters + * The parameters to add to this array. + */ + void addParams(in mozIStorageBindingParams aParameters); + + /** + * The number of mozIStorageBindingParams this object contains. + */ + readonly attribute unsigned long length; +}; diff --git a/storage/mozIStorageCompletionCallback.idl b/storage/mozIStorageCompletionCallback.idl new file mode 100644 index 000000000..1c31cc2c2 --- /dev/null +++ b/storage/mozIStorageCompletionCallback.idl @@ -0,0 +1,24 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" + +[scriptable, function, uuid(8cbf2dc2-91e0-44bc-984f-553638412071)] +interface mozIStorageCompletionCallback : nsISupports { + /** + * Indicates that the event this callback was passed in for has completed. + * + * @param status + * The status of the call. Generally NS_OK if the operation + * completed successfully. + * @param value + * If the operation produces a result, the result. Otherwise, + * |null|. + * + * @see The calling method for expected values. + */ + void complete(in nsresult status, [optional] in nsISupports value); +}; diff --git a/storage/mozIStorageConnection.idl b/storage/mozIStorageConnection.idl new file mode 100644 index 000000000..11d8aa5ac --- /dev/null +++ b/storage/mozIStorageConnection.idl @@ -0,0 +1,268 @@ +/* -*- Mode: idl; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" +#include "mozIStorageAsyncConnection.idl" + +%{C++ +namespace mozilla { +namespace dom { +namespace quota { +class QuotaObject; +} +} +} + +%} + +[ptr] native QuotaObject(mozilla::dom::quota::QuotaObject); + +interface mozIStorageAggregateFunction; +interface mozIStorageCompletionCallback; +interface mozIStorageFunction; +interface mozIStorageProgressHandler; +interface mozIStorageBaseStatement; +interface mozIStorageStatement; +interface mozIStorageAsyncStatement; +interface mozIStorageStatementCallback; +interface mozIStoragePendingStatement; +interface nsIFile; + +/** + * mozIStorageConnection represents a database connection attached to + * a specific file or to the in-memory data storage. It is the + * primary interface for interacting with a database, including + * creating prepared statements, executing SQL, and examining database + * errors. + * + * @note From the main thread, you should rather use mozIStorageAsyncConnection. + * + * @threadsafe + */ +[scriptable, uuid(4aa2ac47-8d24-4004-9b31-ec0bd85f0cc3)] +interface mozIStorageConnection : mozIStorageAsyncConnection { + /** + * Closes a database connection. Callers must finalize all statements created + * for this connection prior to calling this method. It is illegal to use + * call this method if any asynchronous statements have been executed on this + * connection. + * + * @throws NS_ERROR_UNEXPECTED + * If any statement has been executed asynchronously on this object. + * @throws NS_ERROR_UNEXPECTED + * If is called on a thread other than the one that opened it. + */ + void close(); + + /** + * Clones a database connection and makes the clone read only if needed. + * SQL Functions and attached on-disk databases are applied to the new clone. + * + * @param aReadOnly + * If true, the returned database should be put into read-only mode. + * Defaults to false. + * @return the cloned database connection. + * + * @throws NS_ERROR_UNEXPECTED + * If this connection is a memory database. + * @note If your connection is already read-only, you will get a read-only + * clone. + * @note Due to a bug in SQLite, if you use the shared cache (openDatabase), + * you end up with the same privileges as the first connection opened + * regardless of what is specified in aReadOnly. + * @note The following pragmas are copied over to a read-only clone: + * - cache_size + * - temp_store + * The following pragmas are copied over to a writeable clone: + * - cache_size + * - temp_store + * - foreign_keys + * - journal_size_limit + * - synchronous + * - wal_autocheckpoint + * + */ + mozIStorageConnection clone([optional] in boolean aReadOnly); + + /** + * The default size for SQLite database pages used by mozStorage for new + * databases. + */ + readonly attribute long defaultPageSize; + + /** + * Indicates if the connection is open and ready to use. This will be false + * if the connection failed to open, or it has been closed. + */ + readonly attribute boolean connectionReady; + + /** + * lastInsertRowID returns the row ID from the last INSERT + * operation. + */ + readonly attribute long long lastInsertRowID; + + /** + * affectedRows returns the number of database rows that were changed or + * inserted or deleted by last operation. + */ + readonly attribute long affectedRows; + + /** + * The last error SQLite error code. + */ + readonly attribute long lastError; + + /** + * The last SQLite error as a string (in english, straight from the + * sqlite library). + */ + readonly attribute AUTF8String lastErrorString; + + /** + * The schema version of the database. This should not be used until the + * database is ready. The schema will be reported as zero if it is not set. + */ + attribute long schemaVersion; + + ////////////////////////////////////////////////////////////////////////////// + //// Statement creation + + /** + * Create a mozIStorageStatement for the given SQL expression. The + * expression may use ? to indicate sequential numbered arguments, + * ?1, ?2 etc. to indicate specific numbered arguments or :name and + * $var to indicate named arguments. + * + * @param aSQLStatement + * The SQL statement to execute. + * @return a new mozIStorageStatement + */ + mozIStorageStatement createStatement(in AUTF8String aSQLStatement); + + /** + * Execute a SQL expression, expecting no arguments. + * + * @param aSQLStatement The SQL statement to execute + */ + void executeSimpleSQL(in AUTF8String aSQLStatement); + + /** + * Check if the given table exists. + * + * @param aTableName + * The table to check + * @return TRUE if table exists, FALSE otherwise. + */ + boolean tableExists(in AUTF8String aTableName); + + /** + * Check if the given index exists. + * + * @param aIndexName The index to check + * @return TRUE if the index exists, FALSE otherwise. + */ + boolean indexExists(in AUTF8String aIndexName); + + ////////////////////////////////////////////////////////////////////////////// + //// Transactions + + /** + * Returns true if a transaction is active on this connection. + */ + readonly attribute boolean transactionInProgress; + + /** + * Begin a new transaction. sqlite default transactions are deferred. + * If a transaction is active, throws an error. + */ + void beginTransaction(); + + /** + * Begins a new transaction with the given type. + */ + const int32_t TRANSACTION_DEFERRED = 0; + const int32_t TRANSACTION_IMMEDIATE = 1; + const int32_t TRANSACTION_EXCLUSIVE = 2; + void beginTransactionAs(in int32_t transactionType); + + /** + * Commits the current transaction. If no transaction is active, + * @throws NS_ERROR_UNEXPECTED. + * @throws NS_ERROR_NOT_INITIALIZED. + */ + void commitTransaction(); + + /** + * Rolls back the current transaction. If no transaction is active, + * @throws NS_ERROR_UNEXPECTED. + * @throws NS_ERROR_NOT_INITIALIZED. + */ + void rollbackTransaction(); + + ////////////////////////////////////////////////////////////////////////////// + //// Tables + + /** + * Create the table with the given name and schema. + * + * If the table already exists, NS_ERROR_FAILURE is thrown. + * (XXX at some point in the future it will check if the schema is + * the same as what is specified, but that doesn't happen currently.) + * + * @param aTableName + * The table name to be created, consisting of [A-Za-z0-9_], and + * beginning with a letter. + * @param aTableSchema + * The schema of the table; what would normally go between the parens + * in a CREATE TABLE statement: e.g., "foo INTEGER, bar STRING". + * + * @throws NS_ERROR_FAILURE + * If the table already exists or could not be created for any other + * reason. + */ + void createTable(in string aTableName, + in string aTableSchema); + + /** + * Controls SQLITE_FCNTL_CHUNK_SIZE setting in sqlite. This helps avoid fragmentation + * by growing/shrinking the database file in SQLITE_FCNTL_CHUNK_SIZE increments. To + * conserve memory on systems short on storage space, this function will have no effect + * on mobile devices or if less than 500MiB of space is left available. + * + * @param aIncrement + * The database file will grow in multiples of chunkSize. + * @param aDatabaseName + * Sqlite database name. "" means pass NULL for zDbName to sqlite3_file_control. + * See http://sqlite.org/c3ref/file_control.html for more details. + * @throws NS_ERROR_FILE_TOO_BIG + * If the system is short on storage space. + */ + void setGrowthIncrement(in int32_t aIncrement, in AUTF8String aDatabaseName); + + /** + * Enable a predefined virtual table implementation. + * + * @param aModuleName + * The module to enable. Only "filesystem" is currently supported. + * + * @throws NS_ERROR_FAILURE + * For unknown module names. + */ + [noscript] void enableModule(in ACString aModuleName); + + /** + * Get quota objects. + * + * @param[out] aDatabaseQuotaObject + * The QuotaObject associated with the database file. + * @param[out] aJournalQuotaObject + * The QuotaObject associated with the journal file. + * + * @throws NS_ERROR_NOT_INITIALIZED. + */ + [noscript] void getQuotaObjects(out QuotaObject aDatabaseQuotaObject, + out QuotaObject aJournalQuotaObject); +}; diff --git a/storage/mozIStorageError.idl b/storage/mozIStorageError.idl new file mode 100644 index 000000000..7707a81dc --- /dev/null +++ b/storage/mozIStorageError.idl @@ -0,0 +1,149 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 sts=2 expandtab + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" + +%{C++ +#ifdef ERROR +#undef ERROR +#endif +%} + +[scriptable, uuid(1f350f96-7023-434a-8864-40a1c493aac1)] +interface mozIStorageError : nsISupports { + + /** + * General SQL error or missing database. + */ + const long ERROR = 1; + + /** + * Internal logic error. + */ + const long INTERNAL = 2; + + /** + * Access permission denied. + */ + const long PERM = 3; + + /** + * A callback routine requested an abort. + */ + const long ABORT = 4; + + /** + * The database file is locked. + */ + const long BUSY = 5; + + /** + * A table in the database is locked. + */ + const long LOCKED = 6; + + /** + * An allocation failed. + */ + const long NOMEM = 7; + + /** + * Attempt to write to a readonly database. + */ + const long READONLY = 8; + + /** + * Operation was terminated by an interrupt. + */ + const long INTERRUPT = 9; + + /** + * Some kind of disk I/O error occurred. + */ + const long IOERR = 10; + + /** + * The database disk image is malformed. + */ + const long CORRUPT = 11; + + /** + * An insertion failed because the database is full. + */ + const long FULL = 13; + + /** + * Unable to open the database file. + */ + const long CANTOPEN = 14; + + /** + * The database is empty. + */ + const long EMPTY = 16; + + /** + * The database scheme changed. + */ + const long SCHEMA = 17; + + /** + * A string or blob exceeds the size limit. + */ + const long TOOBIG = 18; + + /** + * Abort due to a constraint violation. + */ + const long CONSTRAINT = 19; + + /** + * Data type mismatch. + */ + const long MISMATCH = 20; + + /** + * Library used incorrectly. + */ + const long MISUSE = 21; + + /** + * Uses OS features not supported on the host system. + */ + const long NOLFS = 22; + + /** + * Authorization denied. + */ + const long AUTH = 23; + + /** + * Auxiliary database format error. + */ + const long FORMAT = 24; + + /** + * Attempt to bind a parameter using an out-of-range index or nonexistent + * named parameter name. + */ + const long RANGE = 25; + + /** + * File opened that is not a database file. + */ + const long NOTADB = 26; + + + /** + * Indicates what type of error occurred. + */ + readonly attribute long result; + + /** + * An error string the gives more details, if available. + */ + readonly attribute AUTF8String message; +}; diff --git a/storage/mozIStorageFunction.idl b/storage/mozIStorageFunction.idl new file mode 100644 index 000000000..7f9878377 --- /dev/null +++ b/storage/mozIStorageFunction.idl @@ -0,0 +1,40 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" + +#include "mozIStorageValueArray.idl" + +interface mozIStorageConnection; +interface nsIArray; +interface nsIVariant; + +/** + * mozIStorageFunction is to be implemented by storage consumers that + * wish to receive callbacks during the request execution. + * + * SQL can apply functions to values from tables. Examples of + * such functions are MIN(a1,a2) or SQRT(num). Many functions are + * implemented in SQL engine. + * + * This interface allows consumers to implement their own, + * problem-specific functions. + * These functions can be called from triggers, too. + * + */ +[scriptable, function, uuid(9ff02465-21cb-49f3-b975-7d5b38ceec73)] +interface mozIStorageFunction : nsISupports { + /** + * onFunctionCall is called when execution of a custom + * function should occur. + * + * @param aNumArguments The number of arguments + * @param aFunctionArguments The arguments passed in to the function + * + * @returns any value as Variant type. + */ + + nsIVariant onFunctionCall(in mozIStorageValueArray aFunctionArguments); +}; diff --git a/storage/mozIStoragePendingStatement.idl b/storage/mozIStoragePendingStatement.idl new file mode 100644 index 000000000..a72ce96b9 --- /dev/null +++ b/storage/mozIStoragePendingStatement.idl @@ -0,0 +1,20 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 sts=2 expandtab + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" + +[scriptable, uuid(00da7d20-3768-4398-bedc-e310c324b3f0)] +interface mozIStoragePendingStatement : nsISupports { + + /** + * Cancels a pending statement, if possible. This will only fail if you try + * cancel more than once. + * + * @note For read statements (such as SELECT), you will no longer receive any + * notifications about results once cancel is called. + */ + void cancel(); +}; diff --git a/storage/mozIStorageProgressHandler.idl b/storage/mozIStorageProgressHandler.idl new file mode 100644 index 000000000..14ebb55ed --- /dev/null +++ b/storage/mozIStorageProgressHandler.idl @@ -0,0 +1,26 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" + +interface mozIStorageConnection; + +/** + * mozIProgressHandler is to be implemented by storage consumers that + * wish to receive callbacks during the request execution. + */ +[scriptable, uuid(a3a6fcd4-bf89-4208-a837-bf2a73afd30c)] +interface mozIStorageProgressHandler : nsISupports { + /** + * onProgress is invoked periodically during long running calls. + * + * @param aConnection connection, for which progress handler is + * invoked. + * + * @return true to abort request, false to continue work. + */ + + boolean onProgress(in mozIStorageConnection aConnection); +}; diff --git a/storage/mozIStorageResultSet.idl b/storage/mozIStorageResultSet.idl new file mode 100644 index 000000000..de63b297b --- /dev/null +++ b/storage/mozIStorageResultSet.idl @@ -0,0 +1,21 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 sts=2 expandtab + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" +interface mozIStorageRow; + +[scriptable, uuid(18dd7953-076d-4598-8105-3e32ad26ab24)] +interface mozIStorageResultSet : nsISupports { + + /** + * Obtains the next row from the result set from the statement that was + * executed. + * + * @returns the next row from the result set. This will be null when there + * are no more results. + */ + mozIStorageRow getNextRow(); +}; diff --git a/storage/mozIStorageRow.idl b/storage/mozIStorageRow.idl new file mode 100644 index 000000000..ce12d77cc --- /dev/null +++ b/storage/mozIStorageRow.idl @@ -0,0 +1,33 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 sts=2 expandtab + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozIStorageValueArray.idl" +interface nsIVariant; + +[scriptable, uuid(62d1b6bd-cbfe-4f9b-aee1-0ead4af4e6dc)] +interface mozIStorageRow : mozIStorageValueArray { + + /** + * Obtains the result of a given column specified by aIndex. + * + * @param aIndex + * Zero-based index of the result to get from the tuple. + * @returns the result of the specified column. + */ + nsIVariant getResultByIndex(in unsigned long aIndex); + + /** + * Obtains the result of a given column specified by aName. + * + * @param aName + * Name of the result to get from the tuple. + * @returns the result of the specified column. + * @note The name of a result column is the value of the "AS" clause for that + * column. If there is no AS clause then the name of the column is + * unspecified and may change from one release to the next. + */ + nsIVariant getResultByName(in AUTF8String aName); +}; diff --git a/storage/mozIStorageService.idl b/storage/mozIStorageService.idl new file mode 100644 index 000000000..56d2a127e --- /dev/null +++ b/storage/mozIStorageService.idl @@ -0,0 +1,193 @@ +/* -*- Mode: idl; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" + +interface mozIStorageConnection; +interface nsIFile; +interface nsIFileURL; +interface nsIPropertyBag2; +interface nsIVariant; +interface mozIStorageCompletionCallback; + +/** + * The mozIStorageService interface is intended to be implemented by + * a service that can create storage connections (mozIStorageConnection) + * to either a well-known profile database or to a specific database file. + * + * This is the only way to open a database connection. + * + * @note The first reference to mozIStorageService must be made on the main + * thread. + */ +[scriptable, uuid(07b6b2f5-6d97-47b4-9584-e65bc467fe9e)] +interface mozIStorageService : nsISupports { + /** + * Open an asynchronous connection to a database. + * + * This method MUST be called from the main thread. The connection object + * returned by this function is not threadsafe. You MUST use it only from + * the main thread. + * + * If you have more than one connection to a file, you MUST use the EXACT + * SAME NAME for the file each time, including case. The sqlite code uses + * a simple string compare to see if there is already a connection. Opening + * a connection to "Foo.sqlite" and "foo.sqlite" will CORRUPT YOUR DATABASE. + * + * @param aDatabaseStore Either a nsIFile representing the file that contains + * the database or a special string to open a special database. The special + * string may be: + * - "memory" to open an in-memory database. + * + * @param aOptions A set of options (may be null). Options may contain: + * - bool shared (defaults to |false|). + * -- If |true|, opens the database with a shared-cache. The + * shared-cache mode is more memory-efficient when many + * connections to the same database are expected, though, the + * connections will contend the cache resource. In any cases + * where performance matter, working without a shared-cache will + * improve concurrency. @see openUnsharedDatabase + * + * - int growthIncrement (defaults to none). + * -- Set the growth increment for the main database. This hints SQLite to + * grow the database file by a given chunk size and may reduce + * filesystem fragmentation on large databases. + * @see mozIStorageConnection::setGrowthIncrement + * + * @param aCallback A callback that will receive the result of the operation. + * In case of error, it may receive as status: + * - NS_ERROR_OUT_OF_MEMORY if allocating a new storage object fails. + * - NS_ERROR_FILE_CORRUPTED if the database file is corrupted. + * In case of success, it receives as argument the new database + * connection, as an instance of |mozIStorageAsyncConnection|. + * + * @throws NS_ERROR_INVALID_ARG if |aDatabaseStore| is neither a file nor + * one of the special strings understood by this method, or if one of + * the options passed through |aOptions| does not have the right type. + * @throws NS_ERROR_NOT_SAME_THREAD if called from a thread other than the + * main thread. + */ + void openAsyncDatabase(in nsIVariant aDatabaseStore, + [optional] in nsIPropertyBag2 aOptions, + in mozIStorageCompletionCallback aCallback); + /** + * Get a connection to a named special database storage. + * + * @param aStorageKey a string key identifying the type of storage + * requested. Valid values include: "memory". + * + * @see openDatabase for restrictions on how database connections may be + * used. For the profile database, you should only access it from the main + * thread since other callers may also have connections. + * + * @returns a new mozIStorageConnection for the requested + * storage database. + * + * @throws NS_ERROR_INVALID_ARG if aStorageKey is invalid. + */ + mozIStorageConnection openSpecialDatabase(in string aStorageKey); + + /** + * Open a connection to the specified file. + * + * Consumers should check mozIStorageConnection::connectionReady to ensure + * that they can use the database. If this value is false, it is strongly + * recommended that the database be backed up with + * mozIStorageConnection::backupDB so user data is not lost. + * + * ========== + * DANGER + * ========== + * + * If you have more than one connection to a file, you MUST use the EXACT + * SAME NAME for the file each time, including case. The sqlite code uses + * a simple string compare to see if there is already a connection. Opening + * a connection to "Foo.sqlite" and "foo.sqlite" will CORRUPT YOUR DATABASE. + * + * The connection object returned by this function is not threadsafe. You must + * use it only from the thread you created it from. + * + * @param aDatabaseFile + * A nsIFile that represents the database that is to be opened.. + * + * @returns a mozIStorageConnection for the requested database file. + * + * @throws NS_ERROR_OUT_OF_MEMORY + * If allocating a new storage object fails. + * @throws NS_ERROR_FILE_CORRUPTED + * If the database file is corrupted. + */ + mozIStorageConnection openDatabase(in nsIFile aDatabaseFile); + + /** + * Open a connection to the specified file that doesn't share a sqlite cache. + * + * Without a shared-cache, each connection uses its own pages cache, which + * may be memory inefficient with a large number of connections, in such a + * case so you should use openDatabase instead. On the other side, if cache + * contention may be an issue, for instance when concurrency is important to + * ensure responsiveness, using unshared connections may be a performance win. + * + * ========== + * DANGER + * ========== + * + * If you have more than one connection to a file, you MUST use the EXACT + * SAME NAME for the file each time, including case. The sqlite code uses + * a simple string compare to see if there is already a connection. Opening + * a connection to "Foo.sqlite" and "foo.sqlite" will CORRUPT YOUR DATABASE. + * + * The connection object returned by this function is not threadsafe. You must + * use it only from the thread you created it from. + * + * @param aDatabaseFile + * A nsIFile that represents the database that is to be opened. + * + * @returns a mozIStorageConnection for the requested database file. + * + * @throws NS_ERROR_OUT_OF_MEMORY + * If allocating a new storage object fails. + * @throws NS_ERROR_FILE_CORRUPTED + * If the database file is corrupted. + */ + mozIStorageConnection openUnsharedDatabase(in nsIFile aDatabaseFile); + + /** + * See openDatabase(). Exactly the same only initialized with a file URL. + * Custom parameters can be passed to SQLite and VFS implementations through + * the query part of the URL. + * + * @param aURL + * A nsIFileURL that represents the database that is to be opened. + */ + mozIStorageConnection openDatabaseWithFileURL(in nsIFileURL aFileURL); + + /* + * Utilities + */ + + /** + * Copies the specified database file to the specified parent directory with + * the specified file name. If the parent directory is not specified, it + * places the backup in the same directory as the current file. This function + * ensures that the file being created is unique. + * + * @param aDBFile + * The database file that will be backed up. + * @param aBackupFileName + * The name of the new backup file to create. + * @param [optional] aBackupParentDirectory + * The directory you'd like the backup file to be placed. + * @return The nsIFile representing the backup file. + */ + nsIFile backupDatabaseFile(in nsIFile aDBFile, in AString aBackupFileName, + [optional] in nsIFile aBackupParentDirectory); +}; + +%{C++ + +#define MOZ_STORAGE_MEMORY_STORAGE_KEY "memory" + +%} diff --git a/storage/mozIStorageStatement.idl b/storage/mozIStorageStatement.idl new file mode 100644 index 000000000..a264cfdfa --- /dev/null +++ b/storage/mozIStorageStatement.idl @@ -0,0 +1,307 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 sts=2 expandtab + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozIStorageBaseStatement.idl" +%{C++ +#include "mozilla/DebugOnly.h" +%} + +[ptr] native octetPtr(uint8_t); + +/** + * A SQL statement that can be used for both synchronous and asynchronous + * purposes. + */ +[scriptable, uuid(5f567c35-6c32-4140-828c-683ea49cfd3a)] +interface mozIStorageStatement : mozIStorageBaseStatement { + /** + * Create a clone of this statement, by initializing a new statement + * with the same connection and same SQL statement as this one. It + * does not preserve statement state; that is, if a statement is + * being executed when it is cloned, the new statement will not be + * executing. + */ + mozIStorageStatement clone(); + + /* + * Number of parameters + */ + readonly attribute unsigned long parameterCount; + + /** + * Name of nth parameter, if given + */ + AUTF8String getParameterName(in unsigned long aParamIndex); + + /** + * Returns the index of the named parameter. + * + * @param aName + * The name of the parameter you want the index for. This does not + * include the leading ':'. + * @return the index of the named parameter. + */ + unsigned long getParameterIndex(in AUTF8String aName); + + /** + * Number of columns returned + */ + readonly attribute unsigned long columnCount; + + /** + * Name of nth column + */ + AUTF8String getColumnName(in unsigned long aColumnIndex); + + /** + * Obtains the index of the column with the specified name. + * + * @param aName + * The name of the column. + * @return The index of the column with the specified name. + */ + unsigned long getColumnIndex(in AUTF8String aName); + + /** + * Reset parameters/statement execution + */ + void reset(); + + /** + * Execute the query, ignoring any results. This is accomplished by + * calling executeStep() once, and then calling reset(). + * + * Error and last insert info, etc. are available from + * the mozStorageConnection. + */ + void execute(); + + /** + * Execute a query, using any currently-bound parameters. Reset + * must be called on the statement after the last call of + * executeStep. + * + * @return a boolean indicating whether there are more rows or not; + * row data may be accessed using mozIStorageValueArray methods on + * the statement. + */ + boolean executeStep(); + + /** + * Execute a query, using any currently-bound parameters. Reset is called + * when no more data is returned. This method is only available to JavaScript + * consumers. + * + * @deprecated As of Mozilla 1.9.2 in favor of executeStep(). + * + * @return a boolean indicating whether there are more rows or not. + * + * [deprecated] boolean step(); + */ + + /** + * Obtains the current list of named parameters, which are settable. This + * property is only available to JavaScript consumers. + * + * readonly attribute mozIStorageStatementParams params; + */ + + /** + * Obtains the current row, with access to all the data members by name. This + * property is only available to JavaScript consumers. + * + * readonly attribute mozIStorageStatementRow row; + */ + + ////////////////////////////////////////////////////////////////////////////// + //// Copied contents of mozIStorageValueArray + + /** + * These type values are returned by getTypeOfIndex + * to indicate what type of value is present at + * a given column. + */ + const long VALUE_TYPE_NULL = 0; + const long VALUE_TYPE_INTEGER = 1; + const long VALUE_TYPE_FLOAT = 2; + const long VALUE_TYPE_TEXT = 3; + const long VALUE_TYPE_BLOB = 4; + + /** + * The number of entries in the array (each corresponding to a column in the + * database row) + */ + readonly attribute unsigned long numEntries; + + /** + * Indicate the data type of the current result row for the the given column. + * SQLite will perform type conversion if you ask for a value as a different + * type than it is stored as. + * + * @param aIndex + * 0-based column index. + * @return The type of the value at the given column index; one of + * VALUE_TYPE_NULL, VALUE_TYPE_INTEGER, VALUE_TYPE_FLOAT, + * VALUE_TYPE_TEXT, VALUE_TYPE_BLOB. + */ + long getTypeOfIndex(in unsigned long aIndex); + + /** + * Retrieve the contents of a column from the current result row as an + * integer. + * + * @param aIndex + * 0-based colummn index. + * @return Column value interpreted as an integer per type conversion rules. + * @{ + */ + long getInt32(in unsigned long aIndex); + long long getInt64(in unsigned long aIndex); + /** @} */ + /** + * Retrieve the contents of a column from the current result row as a + * floating point double. + * + * @param aIndex + * 0-based colummn index. + * @return Column value interpreted as a double per type conversion rules. + */ + double getDouble(in unsigned long aIndex); + /** + * Retrieve the contents of a column from the current result row as a + * string. + * + * @param aIndex + * 0-based colummn index. + * @return The value for the result column interpreted as a string. If the + * stored value was NULL, you will get an empty string with IsVoid set + * to distinguish it from an explicitly set empty string. + * @{ + */ + AUTF8String getUTF8String(in unsigned long aIndex); + AString getString(in unsigned long aIndex); + /** @} */ + + /** + * Retrieve the contents of a column from the current result row as a + * blob. + * + * @param aIndex + * 0-based colummn index. + * @param[out] aDataSize + * The number of bytes in the blob. + * @param[out] aData + * The contents of the BLOB. This will be NULL if aDataSize == 0. + */ + void getBlob(in unsigned long aIndex, out unsigned long aDataSize, [array,size_is(aDataSize)] out octet aData); + + /** + * Retrieve the contents of a Blob column from the current result row as a + * string. + * + * @param aIndex + * 0-based colummn index. + * @return The value for the result Blob column interpreted as a String. + * No encoding conversion is performed. + */ + AString getBlobAsString(in unsigned long aIndex); + + /** + * Retrieve the contents of a Blob column from the current result row as a + * UTF8 string. + * + * @param aIndex + * 0-based colummn index. + * @return The value for the result Blob column interpreted as a UTF8 String. + * No encoding conversion is performed. + */ + AUTF8String getBlobAsUTF8String(in unsigned long aIndex); + + /** + * Check whether the given column in the current result row is NULL. + * + * @param aIndex + * 0-based colummn index. + * @return true if the value for the result column is null. + */ + boolean getIsNull(in unsigned long aIndex); + + /** + * Returns a shared string pointer + */ + [noscript] void getSharedUTF8String(in unsigned long aIndex, out unsigned long aLength, [shared,retval] out string aResult); + [noscript] void getSharedString(in unsigned long aIndex, out unsigned long aLength, [shared,retval] out wstring aResult); + [noscript] void getSharedBlob(in unsigned long aIndex, out unsigned long aLength, [shared,retval] out octetPtr aResult); + +%{C++ + /** + * Getters for native code that return their values as + * the return type, for convenience and sanity. + * + * Not virtual; no vtable bloat. + */ + + inline int32_t AsInt32(uint32_t idx) { + int32_t v = 0; + mozilla::DebugOnly<nsresult> rv = GetInt32(idx, &v); + MOZ_ASSERT(NS_SUCCEEDED(rv) || IsNull(idx), + "Getting value failed, wrong column index?"); + return v; + } + + inline int64_t AsInt64(uint32_t idx) { + int64_t v = 0; + mozilla::DebugOnly<nsresult> rv = GetInt64(idx, &v); + MOZ_ASSERT(NS_SUCCEEDED(rv) || IsNull(idx), + "Getting value failed, wrong column index?"); + return v; + } + + inline double AsDouble(uint32_t idx) { + double v = 0.0; + mozilla::DebugOnly<nsresult> rv = GetDouble(idx, &v); + MOZ_ASSERT(NS_SUCCEEDED(rv) || IsNull(idx), + "Getting value failed, wrong column index?"); + return v; + } + + inline const char* AsSharedUTF8String(uint32_t idx, uint32_t *len) { + const char *str = nullptr; + *len = 0; + mozilla::DebugOnly<nsresult> rv = GetSharedUTF8String(idx, len, &str); + MOZ_ASSERT(NS_SUCCEEDED(rv) || IsNull(idx), + "Getting value failed, wrong column index?"); + return str; + } + + inline const char16_t* AsSharedWString(uint32_t idx, uint32_t *len) { + const char16_t *str = nullptr; + *len = 0; + mozilla::DebugOnly<nsresult> rv = GetSharedString(idx, len, &str); + MOZ_ASSERT(NS_SUCCEEDED(rv) || IsNull(idx), + "Getting value failed, wrong column index?"); + return str; + } + + inline const uint8_t* AsSharedBlob(uint32_t idx, uint32_t *len) { + const uint8_t *blob = nullptr; + *len = 0; + mozilla::DebugOnly<nsresult> rv = GetSharedBlob(idx, len, &blob); + MOZ_ASSERT(NS_SUCCEEDED(rv) || IsNull(idx), + "Getting value failed, wrong column index?"); + return blob; + } + + inline bool IsNull(uint32_t idx) { + bool b = false; + mozilla::DebugOnly<nsresult> rv = GetIsNull(idx, &b); + MOZ_ASSERT(NS_SUCCEEDED(rv), + "Getting value failed, wrong column index?"); + return b; + } + +%} +}; diff --git a/storage/mozIStorageStatementCallback.idl b/storage/mozIStorageStatementCallback.idl new file mode 100644 index 000000000..3c7bd6f6f --- /dev/null +++ b/storage/mozIStorageStatementCallback.idl @@ -0,0 +1,48 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 sts=2 expandtab + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" + +interface mozIStorageResultSet; +interface mozIStorageError; + +[scriptable, uuid(29383d00-d8c4-4ddd-9f8b-c2feb0f2fcfa)] +interface mozIStorageStatementCallback : nsISupports { + + /** + * Called when some result is obtained from the database. This function can + * be called more than once with a different storageIResultSet each time for + * any given asynchronous statement. + * + * @param aResultSet + * The result set containing the data from the database. + */ + void handleResult(in mozIStorageResultSet aResultSet); + + /** + * Called when some error occurs while executing the statement. This function + * may be called more than once with a different storageIError each time for + * any given asynchronous statement. + * + * @param aError + * An object containing information about the error. + */ + void handleError(in mozIStorageError aError); + + /** + * Called when the statement has finished executing. This function will only + * be called once for any given asynchronous statement. + * + * @param aReason + * Indicates if the statement is no longer executing because it either + * finished (REASON_FINISHED), was canceled (REASON_CANCELED), or + * a fatal error occurred (REASON_ERROR). + */ + const unsigned short REASON_FINISHED = 0; + const unsigned short REASON_CANCELED = 1; + const unsigned short REASON_ERROR = 2; + void handleCompletion(in unsigned short aReason); +}; diff --git a/storage/mozIStorageStatementParams.idl b/storage/mozIStorageStatementParams.idl new file mode 100644 index 000000000..efeee9772 --- /dev/null +++ b/storage/mozIStorageStatementParams.idl @@ -0,0 +1,11 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" + +[scriptable, uuid(e65fe6e2-2643-463c-97e2-27665efe2386)] +interface mozIStorageStatementParams : nsISupports { + // Magic interface for parameter setting that implements nsIXPCScriptable. +}; diff --git a/storage/mozIStorageStatementRow.idl b/storage/mozIStorageStatementRow.idl new file mode 100644 index 000000000..8be1da7f1 --- /dev/null +++ b/storage/mozIStorageStatementRow.idl @@ -0,0 +1,12 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" + +[scriptable, uuid(02eeaf95-c3db-4182-9340-222c29f68f02)] +interface mozIStorageStatementRow : nsISupports { + // Magic interface we return that implements nsIXPCScriptable, to allow + // for by-name access to rows. +}; diff --git a/storage/mozIStorageVacuumParticipant.idl b/storage/mozIStorageVacuumParticipant.idl new file mode 100644 index 000000000..a4e0f3a71 --- /dev/null +++ b/storage/mozIStorageVacuumParticipant.idl @@ -0,0 +1,57 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" + +interface mozIStorageConnection; + +/** + * This interface contains the information that the Storage service needs to + * vacuum a database. This interface is created as a service through the + * category manager with the category "vacuum-participant". + * Please see https://developer.mozilla.org/en/mozIStorageVacuumParticipant for + * more information. + */ +[scriptable, uuid(8f367508-1d9a-4d3f-be0c-ac11b6dd7dbf)] +interface mozIStorageVacuumParticipant : nsISupports { + /** + * The expected page size in bytes for the database. The vacuum manager will + * try to correct the page size during idle based on this value. + * + * @note If the database is using the WAL journal mode, the page size won't + * be changed to the requested value. See bug 634374. + * @note Valid page size values are powers of 2 between 512 and 65536. + * The suggested value is mozIStorageConnection::defaultPageSize. + */ + readonly attribute long expectedDatabasePageSize; + + /** + * Connection to the database file to be vacuumed. + */ + readonly attribute mozIStorageConnection databaseConnection; + + /** + * Notifies when a vacuum operation begins. Listeners should avoid using the + * database till onEndVacuum is received. + * + * @return true to proceed with the vacuum, false if the participant wants to + * opt-out for now, it will be retried later. Useful when participant + * is running some other heavy operation that can't be interrupted. + * + * @note When a vacuum operation starts or ends it will also dispatch a global + * "heavy-io-task" notification through the observer service with the + * data argument being either "vacuum-begin" or "vacuum-end". + */ + boolean onBeginVacuum(); + + /** + * Notifies when a vacuum operation ends. + * + * @param aSucceeded + * reports if the vacuum succeeded or failed. + */ + void onEndVacuum(in boolean aSucceeded); +}; diff --git a/storage/mozIStorageValueArray.idl b/storage/mozIStorageValueArray.idl new file mode 100644 index 000000000..3dbf75285 --- /dev/null +++ b/storage/mozIStorageValueArray.idl @@ -0,0 +1,145 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" +%{C++ +#include "mozilla/DebugOnly.h" +%} + +[ptr] native octetPtr(uint8_t); + +/** + * mozIStorageValueArray wraps an array of SQL values, such as a single database + * row. + */ +[scriptable, uuid(6e6306f4-ffa7-40f5-96ca-36159ce8f431)] +interface mozIStorageValueArray : nsISupports { + /** + * These type values are returned by getTypeOfIndex + * to indicate what type of value is present at + * a given column. + */ + const long VALUE_TYPE_NULL = 0; + const long VALUE_TYPE_INTEGER = 1; + const long VALUE_TYPE_FLOAT = 2; + const long VALUE_TYPE_TEXT = 3; + const long VALUE_TYPE_BLOB = 4; + + /** + * numEntries + * + * number of entries in the array (each corresponding to a column + * in the database row) + */ + readonly attribute unsigned long numEntries; + + /** + * Returns the type of the value at the given column index; + * one of VALUE_TYPE_NULL, VALUE_TYPE_INTEGER, VALUE_TYPE_FLOAT, + * VALUE_TYPE_TEXT, VALUE_TYPE_BLOB. + */ + long getTypeOfIndex(in unsigned long aIndex); + + /** + * Obtain a value for the given entry (column) index. + * Due to SQLite's type conversion rules, any of these are valid + * for any column regardless of the column's data type. However, + * if the specific type matters, getTypeOfIndex should be used + * first to identify the column type, and then the appropriate + * get method should be called. + * + * If you ask for a string value for a NULL column, you will get an empty + * string with IsVoid set to distinguish it from an explicitly set empty + * string. + */ + long getInt32(in unsigned long aIndex); + long long getInt64(in unsigned long aIndex); + double getDouble(in unsigned long aIndex); + AUTF8String getUTF8String(in unsigned long aIndex); + AString getString(in unsigned long aIndex); + + // data will be NULL if dataSize = 0 + void getBlob(in unsigned long aIndex, out unsigned long aDataSize, [array,size_is(aDataSize)] out octet aData); + AString getBlobAsString(in unsigned long aIndex); + AUTF8String getBlobAsUTF8String(in unsigned long aIndex); + boolean getIsNull(in unsigned long aIndex); + + /** + * Returns a shared string pointer + */ + [noscript] void getSharedUTF8String(in unsigned long aIndex, out unsigned long aLength, [shared,retval] out string aResult); + [noscript] void getSharedString(in unsigned long aIndex, out unsigned long aLength, [shared,retval] out wstring aResult); + [noscript] void getSharedBlob(in unsigned long aIndex, out unsigned long aLength, [shared,retval] out octetPtr aResult); + +%{C++ + /** + * Getters for native code that return their values as + * the return type, for convenience and sanity. + * + * Not virtual; no vtable bloat. + */ + + inline int32_t AsInt32(uint32_t idx) { + int32_t v = 0; + mozilla::DebugOnly<nsresult> rv = GetInt32(idx, &v); + MOZ_ASSERT(NS_SUCCEEDED(rv) || IsNull(idx), + "Getting value failed, wrong column index?"); + return v; + } + + inline int64_t AsInt64(uint32_t idx) { + int64_t v = 0; + mozilla::DebugOnly<nsresult> rv = GetInt64(idx, &v); + MOZ_ASSERT(NS_SUCCEEDED(rv) || IsNull(idx), + "Getting value failed, wrong column index?"); + return v; + } + + inline double AsDouble(uint32_t idx) { + double v = 0.0; + mozilla::DebugOnly<nsresult> rv = GetDouble(idx, &v); + MOZ_ASSERT(NS_SUCCEEDED(rv) || IsNull(idx), + "Getting value failed, wrong column index?"); + return v; + } + + inline const char* AsSharedUTF8String(uint32_t idx, uint32_t *len) { + const char *str = nullptr; + *len = 0; + mozilla::DebugOnly<nsresult> rv = GetSharedUTF8String(idx, len, &str); + MOZ_ASSERT(NS_SUCCEEDED(rv) || IsNull(idx), + "Getting value failed, wrong column index?"); + return str; + } + + inline const char16_t* AsSharedWString(uint32_t idx, uint32_t *len) { + const char16_t *str = nullptr; + *len = 0; + mozilla::DebugOnly<nsresult> rv = GetSharedString(idx, len, &str); + MOZ_ASSERT(NS_SUCCEEDED(rv) || IsNull(idx), + "Getting value failed, wrong column index?"); + return str; + } + + inline const uint8_t* AsSharedBlob(uint32_t idx, uint32_t *len) { + const uint8_t *blob = nullptr; + *len = 0; + mozilla::DebugOnly<nsresult> rv = GetSharedBlob(idx, len, &blob); + MOZ_ASSERT(NS_SUCCEEDED(rv) || IsNull(idx), + "Getting value failed, wrong column index?"); + return blob; + } + + inline bool IsNull(uint32_t idx) { + bool b = false; + mozilla::DebugOnly<nsresult> rv = GetIsNull(idx, &b); + MOZ_ASSERT(NS_SUCCEEDED(rv), + "Getting value failed, wrong column index?"); + return b; + } + +%} + +}; diff --git a/storage/mozStorageArgValueArray.cpp b/storage/mozStorageArgValueArray.cpp new file mode 100644 index 000000000..40d67a4cd --- /dev/null +++ b/storage/mozStorageArgValueArray.cpp @@ -0,0 +1,213 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsError.h" +#include "nsMemory.h" +#include "nsString.h" + +#include "mozStoragePrivateHelpers.h" +#include "mozStorageArgValueArray.h" + +namespace mozilla { +namespace storage { + +//////////////////////////////////////////////////////////////////////////////// +//// ArgValueArray + +ArgValueArray::ArgValueArray(int32_t aArgc, + sqlite3_value **aArgv) +: mArgc(aArgc) +, mArgv(aArgv) +{ +} + +NS_IMPL_ISUPPORTS( + ArgValueArray, + mozIStorageValueArray +) + +//////////////////////////////////////////////////////////////////////////////// +//// mozIStorageValueArray + +NS_IMETHODIMP +ArgValueArray::GetNumEntries(uint32_t *_size) +{ + *_size = mArgc; + return NS_OK; +} + +NS_IMETHODIMP +ArgValueArray::GetTypeOfIndex(uint32_t aIndex, + int32_t *_type) +{ + ENSURE_INDEX_VALUE(aIndex, mArgc); + + int t = ::sqlite3_value_type(mArgv[aIndex]); + switch (t) { + case SQLITE_INTEGER: + *_type = VALUE_TYPE_INTEGER; + break; + case SQLITE_FLOAT: + *_type = VALUE_TYPE_FLOAT; + break; + case SQLITE_TEXT: + *_type = VALUE_TYPE_TEXT; + break; + case SQLITE_BLOB: + *_type = VALUE_TYPE_BLOB; + break; + case SQLITE_NULL: + *_type = VALUE_TYPE_NULL; + break; + default: + return NS_ERROR_FAILURE; + } + + return NS_OK; +} + +NS_IMETHODIMP +ArgValueArray::GetInt32(uint32_t aIndex, + int32_t *_value) +{ + ENSURE_INDEX_VALUE(aIndex, mArgc); + + *_value = ::sqlite3_value_int(mArgv[aIndex]); + return NS_OK; +} + +NS_IMETHODIMP +ArgValueArray::GetInt64(uint32_t aIndex, + int64_t *_value) +{ + ENSURE_INDEX_VALUE(aIndex, mArgc); + + *_value = ::sqlite3_value_int64(mArgv[aIndex]); + return NS_OK; +} + +NS_IMETHODIMP +ArgValueArray::GetDouble(uint32_t aIndex, + double *_value) +{ + ENSURE_INDEX_VALUE(aIndex, mArgc); + + *_value = ::sqlite3_value_double(mArgv[aIndex]); + return NS_OK; +} + +NS_IMETHODIMP +ArgValueArray::GetUTF8String(uint32_t aIndex, + nsACString &_value) +{ + ENSURE_INDEX_VALUE(aIndex, mArgc); + + if (::sqlite3_value_type(mArgv[aIndex]) == SQLITE_NULL) { + // NULL columns should have IsVoid set to distinguish them from an empty + // string. + _value.SetIsVoid(true); + } + else { + _value.Assign(reinterpret_cast<const char *>(::sqlite3_value_text(mArgv[aIndex])), + ::sqlite3_value_bytes(mArgv[aIndex])); + } + return NS_OK; +} + +NS_IMETHODIMP +ArgValueArray::GetString(uint32_t aIndex, + nsAString &_value) +{ + ENSURE_INDEX_VALUE(aIndex, mArgc); + + if (::sqlite3_value_type(mArgv[aIndex]) == SQLITE_NULL) { + // NULL columns should have IsVoid set to distinguish them from an empty + // string. + _value.SetIsVoid(true); + } else { + _value.Assign(static_cast<const char16_t *>(::sqlite3_value_text16(mArgv[aIndex])), + ::sqlite3_value_bytes16(mArgv[aIndex]) / 2); + } + return NS_OK; +} + +NS_IMETHODIMP +ArgValueArray::GetBlob(uint32_t aIndex, + uint32_t *_size, + uint8_t **_blob) +{ + ENSURE_INDEX_VALUE(aIndex, mArgc); + + int size = ::sqlite3_value_bytes(mArgv[aIndex]); + void *blob = nsMemory::Clone(::sqlite3_value_blob(mArgv[aIndex]), size); + NS_ENSURE_TRUE(blob, NS_ERROR_OUT_OF_MEMORY); + + *_blob = static_cast<uint8_t *>(blob); + *_size = size; + return NS_OK; +} + +NS_IMETHODIMP +ArgValueArray::GetBlobAsString(uint32_t aIndex, nsAString& aValue) +{ + return DoGetBlobAsString(this, aIndex, aValue); +} + +NS_IMETHODIMP +ArgValueArray::GetBlobAsUTF8String(uint32_t aIndex, nsACString& aValue) +{ + return DoGetBlobAsString(this, aIndex, aValue); +} + +NS_IMETHODIMP +ArgValueArray::GetIsNull(uint32_t aIndex, + bool *_isNull) +{ + // GetTypeOfIndex will check aIndex for us, so we don't have to. + int32_t type; + nsresult rv = GetTypeOfIndex(aIndex, &type); + NS_ENSURE_SUCCESS(rv, rv); + + *_isNull = (type == VALUE_TYPE_NULL); + return NS_OK; +} + +NS_IMETHODIMP +ArgValueArray::GetSharedUTF8String(uint32_t aIndex, + uint32_t *_length, + const char **_string) +{ + if (_length) + *_length = ::sqlite3_value_bytes(mArgv[aIndex]); + + *_string = reinterpret_cast<const char *>(::sqlite3_value_text(mArgv[aIndex])); + return NS_OK; +} + +NS_IMETHODIMP +ArgValueArray::GetSharedString(uint32_t aIndex, + uint32_t *_length, + const char16_t **_string) +{ + if (_length) + *_length = ::sqlite3_value_bytes(mArgv[aIndex]); + + *_string = static_cast<const char16_t *>(::sqlite3_value_text16(mArgv[aIndex])); + return NS_OK; +} + +NS_IMETHODIMP +ArgValueArray::GetSharedBlob(uint32_t aIndex, + uint32_t *_size, + const uint8_t **_blob) +{ + *_size = ::sqlite3_value_bytes(mArgv[aIndex]); + *_blob = static_cast<const uint8_t *>(::sqlite3_value_blob(mArgv[aIndex])); + return NS_OK; +} + +} // namespace storage +} // namespace mozilla diff --git a/storage/mozStorageArgValueArray.h b/storage/mozStorageArgValueArray.h new file mode 100644 index 000000000..5a14957ba --- /dev/null +++ b/storage/mozStorageArgValueArray.h @@ -0,0 +1,36 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozStorageArgValueArray_h +#define mozStorageArgValueArray_h + +#include "mozIStorageValueArray.h" +#include "mozilla/Attributes.h" + +#include "sqlite3.h" + +namespace mozilla { +namespace storage { + +class ArgValueArray final : public mozIStorageValueArray +{ +public: + ArgValueArray(int32_t aArgc, sqlite3_value **aArgv); + + NS_DECL_ISUPPORTS + NS_DECL_MOZISTORAGEVALUEARRAY + +private: + ~ArgValueArray() {} + + uint32_t mArgc; + sqlite3_value **mArgv; +}; + +} // namespace storage +} // namespace mozilla + +#endif // mozStorageArgValueArray_h diff --git a/storage/mozStorageAsyncStatement.cpp b/storage/mozStorageAsyncStatement.cpp new file mode 100644 index 000000000..d0a3eec04 --- /dev/null +++ b/storage/mozStorageAsyncStatement.cpp @@ -0,0 +1,383 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include <limits.h> +#include <stdio.h> + +#include "nsError.h" +#include "nsMemory.h" +#include "nsProxyRelease.h" +#include "nsThreadUtils.h" +#include "nsIClassInfoImpl.h" +#include "Variant.h" + +#include "mozIStorageError.h" + +#include "mozStorageBindingParams.h" +#include "mozStorageConnection.h" +#include "mozStorageAsyncStatementJSHelper.h" +#include "mozStorageAsyncStatementParams.h" +#include "mozStoragePrivateHelpers.h" +#include "mozStorageStatementRow.h" +#include "mozStorageStatement.h" +#include "nsDOMClassInfo.h" + +#include "mozilla/Logging.h" + +extern mozilla::LazyLogModule gStorageLog; + +namespace mozilla { +namespace storage { + +//////////////////////////////////////////////////////////////////////////////// +//// nsIClassInfo + +NS_IMPL_CI_INTERFACE_GETTER(AsyncStatement, + mozIStorageAsyncStatement, + mozIStorageBaseStatement, + mozIStorageBindingParams, + mozilla::storage::StorageBaseStatementInternal) + +class AsyncStatementClassInfo : public nsIClassInfo +{ +public: + constexpr AsyncStatementClassInfo() {} + + NS_DECL_ISUPPORTS_INHERITED + + NS_IMETHOD + GetInterfaces(uint32_t *_count, nsIID ***_array) override + { + return NS_CI_INTERFACE_GETTER_NAME(AsyncStatement)(_count, _array); + } + + NS_IMETHOD + GetScriptableHelper(nsIXPCScriptable **_helper) override + { + static AsyncStatementJSHelper sJSHelper; + *_helper = &sJSHelper; + return NS_OK; + } + + NS_IMETHOD + GetContractID(char **_contractID) override + { + *_contractID = nullptr; + return NS_OK; + } + + NS_IMETHOD + GetClassDescription(char **_desc) override + { + *_desc = nullptr; + return NS_OK; + } + + NS_IMETHOD + GetClassID(nsCID **_id) override + { + *_id = nullptr; + return NS_OK; + } + + NS_IMETHOD + GetFlags(uint32_t *_flags) override + { + *_flags = 0; + return NS_OK; + } + + NS_IMETHOD + GetClassIDNoAlloc(nsCID *_cid) override + { + return NS_ERROR_NOT_AVAILABLE; + } +}; + +NS_IMETHODIMP_(MozExternalRefCountType) AsyncStatementClassInfo::AddRef() { return 2; } +NS_IMETHODIMP_(MozExternalRefCountType) AsyncStatementClassInfo::Release() { return 1; } +NS_IMPL_QUERY_INTERFACE(AsyncStatementClassInfo, nsIClassInfo) + +static AsyncStatementClassInfo sAsyncStatementClassInfo; + +//////////////////////////////////////////////////////////////////////////////// +//// AsyncStatement + +AsyncStatement::AsyncStatement() +: StorageBaseStatementInternal() +, mFinalized(false) +{ +} + +nsresult +AsyncStatement::initialize(Connection *aDBConnection, + sqlite3 *aNativeConnection, + const nsACString &aSQLStatement) +{ + MOZ_ASSERT(aDBConnection, "No database connection given!"); + MOZ_ASSERT(!aDBConnection->isClosed(), "Database connection should be valid"); + MOZ_ASSERT(aNativeConnection, "No native connection given!"); + + mDBConnection = aDBConnection; + mNativeConnection = aNativeConnection; + mSQLString = aSQLStatement; + + MOZ_LOG(gStorageLog, LogLevel::Debug, ("Inited async statement '%s' (0x%p)", + mSQLString.get())); + +#ifdef DEBUG + // We want to try and test for LIKE and that consumers are using + // escapeStringForLIKE instead of just trusting user input. The idea to + // check to see if they are binding a parameter after like instead of just + // using a string. We only do this in debug builds because it's expensive! + const nsCaseInsensitiveCStringComparator c; + nsACString::const_iterator start, end, e; + aSQLStatement.BeginReading(start); + aSQLStatement.EndReading(end); + e = end; + while (::FindInReadable(NS_LITERAL_CSTRING(" LIKE"), start, e, c)) { + // We have a LIKE in here, so we perform our tests + // FindInReadable moves the iterator, so we have to get a new one for + // each test we perform. + nsACString::const_iterator s1, s2, s3; + s1 = s2 = s3 = start; + + if (!(::FindInReadable(NS_LITERAL_CSTRING(" LIKE ?"), s1, end, c) || + ::FindInReadable(NS_LITERAL_CSTRING(" LIKE :"), s2, end, c) || + ::FindInReadable(NS_LITERAL_CSTRING(" LIKE @"), s3, end, c))) { + // At this point, we didn't find a LIKE statement followed by ?, :, + // or @, all of which are valid characters for binding a parameter. + // We will warn the consumer that they may not be safely using LIKE. + NS_WARNING("Unsafe use of LIKE detected! Please ensure that you " + "are using mozIStorageAsyncStatement::escapeStringForLIKE " + "and that you are binding that result to the statement " + "to prevent SQL injection attacks."); + } + + // resetting start and e + start = e; + e = end; + } +#endif + + return NS_OK; +} + +mozIStorageBindingParams * +AsyncStatement::getParams() +{ + nsresult rv; + + // If we do not have an array object yet, make it. + if (!mParamsArray) { + nsCOMPtr<mozIStorageBindingParamsArray> array; + rv = NewBindingParamsArray(getter_AddRefs(array)); + NS_ENSURE_SUCCESS(rv, nullptr); + + mParamsArray = static_cast<BindingParamsArray *>(array.get()); + } + + // If there isn't already any rows added, we'll have to add one to use. + if (mParamsArray->length() == 0) { + RefPtr<AsyncBindingParams> params(new AsyncBindingParams(mParamsArray)); + NS_ENSURE_TRUE(params, nullptr); + + rv = mParamsArray->AddParams(params); + NS_ENSURE_SUCCESS(rv, nullptr); + + // We have to unlock our params because AddParams locks them. This is safe + // because no reference to the params object was, or ever will be given out. + params->unlock(nullptr); + + // We also want to lock our array at this point - we don't want anything to + // be added to it. + mParamsArray->lock(); + } + + return *mParamsArray->begin(); +} + +/** + * If we are here then we know there are no pending async executions relying on + * us (StatementData holds a reference to us; this also goes for our own + * AsyncStatementFinalizer which proxies its release to the calling thread) and + * so it is always safe to destroy our sqlite3_stmt if one exists. We can be + * destroyed on the caller thread by garbage-collection/reference counting or on + * the async thread by the last execution of a statement that already lost its + * main-thread refs. + */ +AsyncStatement::~AsyncStatement() +{ + destructorAsyncFinalize(); + + // If we are getting destroyed on the wrong thread, proxy the connection + // release to the right thread. I'm not sure why we do this. + bool onCallingThread = false; + (void)mDBConnection->threadOpenedOn->IsOnCurrentThread(&onCallingThread); + if (!onCallingThread) { + // NS_ProxyRelase only magic forgets for us if mDBConnection is an + // nsCOMPtr. Which it is not; it's an nsRefPtr. + nsCOMPtr<nsIThread> targetThread(mDBConnection->threadOpenedOn); + NS_ProxyRelease(targetThread, mDBConnection.forget()); + } +} + +//////////////////////////////////////////////////////////////////////////////// +//// nsISupports + +NS_IMPL_ADDREF(AsyncStatement) +NS_IMPL_RELEASE(AsyncStatement) + +NS_INTERFACE_MAP_BEGIN(AsyncStatement) + NS_INTERFACE_MAP_ENTRY(mozIStorageAsyncStatement) + NS_INTERFACE_MAP_ENTRY(mozIStorageBaseStatement) + NS_INTERFACE_MAP_ENTRY(mozIStorageBindingParams) + NS_INTERFACE_MAP_ENTRY(mozilla::storage::StorageBaseStatementInternal) + if (aIID.Equals(NS_GET_IID(nsIClassInfo))) { + foundInterface = static_cast<nsIClassInfo *>(&sAsyncStatementClassInfo); + } + else + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, mozIStorageAsyncStatement) +NS_INTERFACE_MAP_END + + +//////////////////////////////////////////////////////////////////////////////// +//// StorageBaseStatementInternal + +Connection * +AsyncStatement::getOwner() +{ + return mDBConnection; +} + +int +AsyncStatement::getAsyncStatement(sqlite3_stmt **_stmt) +{ +#ifdef DEBUG + // Make sure we are never called on the connection's owning thread. + bool onOpenedThread = false; + (void)mDBConnection->threadOpenedOn->IsOnCurrentThread(&onOpenedThread); + NS_ASSERTION(!onOpenedThread, + "We should only be called on the async thread!"); +#endif + + if (!mAsyncStatement) { + int rc = mDBConnection->prepareStatement(mNativeConnection, mSQLString, + &mAsyncStatement); + if (rc != SQLITE_OK) { + MOZ_LOG(gStorageLog, LogLevel::Error, + ("Sqlite statement prepare error: %d '%s'", rc, + ::sqlite3_errmsg(mNativeConnection))); + MOZ_LOG(gStorageLog, LogLevel::Error, + ("Statement was: '%s'", mSQLString.get())); + *_stmt = nullptr; + return rc; + } + MOZ_LOG(gStorageLog, LogLevel::Debug, ("Initialized statement '%s' (0x%p)", + mSQLString.get(), + mAsyncStatement)); + } + + *_stmt = mAsyncStatement; + return SQLITE_OK; +} + +nsresult +AsyncStatement::getAsynchronousStatementData(StatementData &_data) +{ + if (mFinalized) + return NS_ERROR_UNEXPECTED; + + // Pass null for the sqlite3_stmt; it will be requested on demand from the + // async thread. + _data = StatementData(nullptr, bindingParamsArray(), this); + + return NS_OK; +} + +already_AddRefed<mozIStorageBindingParams> +AsyncStatement::newBindingParams(mozIStorageBindingParamsArray *aOwner) +{ + if (mFinalized) + return nullptr; + + nsCOMPtr<mozIStorageBindingParams> params(new AsyncBindingParams(aOwner)); + return params.forget(); +} + + +//////////////////////////////////////////////////////////////////////////////// +//// mozIStorageAsyncStatement + +// (nothing is specific to mozIStorageAsyncStatement) + +//////////////////////////////////////////////////////////////////////////////// +//// StorageBaseStatementInternal + +// proxy to StorageBaseStatementInternal using its define helper. +MIXIN_IMPL_STORAGEBASESTATEMENTINTERNAL( + AsyncStatement, + if (mFinalized) return NS_ERROR_UNEXPECTED;) + +NS_IMETHODIMP +AsyncStatement::Finalize() +{ + if (mFinalized) + return NS_OK; + + mFinalized = true; + + MOZ_LOG(gStorageLog, LogLevel::Debug, ("Finalizing statement '%s'", + mSQLString.get())); + + asyncFinalize(); + + // Release the params holder, so it can release the reference to us. + mStatementParamsHolder = nullptr; + + return NS_OK; +} + +NS_IMETHODIMP +AsyncStatement::BindParameters(mozIStorageBindingParamsArray *aParameters) +{ + if (mFinalized) + return NS_ERROR_UNEXPECTED; + + BindingParamsArray *array = static_cast<BindingParamsArray *>(aParameters); + if (array->getOwner() != this) + return NS_ERROR_UNEXPECTED; + + if (array->length() == 0) + return NS_ERROR_UNEXPECTED; + + mParamsArray = array; + mParamsArray->lock(); + + return NS_OK; +} + +NS_IMETHODIMP +AsyncStatement::GetState(int32_t *_state) +{ + if (mFinalized) + *_state = MOZ_STORAGE_STATEMENT_INVALID; + else + *_state = MOZ_STORAGE_STATEMENT_READY; + + return NS_OK; +} + +//////////////////////////////////////////////////////////////////////////////// +//// mozIStorageBindingParams + +BOILERPLATE_BIND_PROXIES( + AsyncStatement, + if (mFinalized) return NS_ERROR_UNEXPECTED; +) + +} // namespace storage +} // namespace mozilla diff --git a/storage/mozStorageAsyncStatement.h b/storage/mozStorageAsyncStatement.h new file mode 100644 index 000000000..4fac36d30 --- /dev/null +++ b/storage/mozStorageAsyncStatement.h @@ -0,0 +1,107 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_storage_mozStorageAsyncStatement_h_ +#define mozilla_storage_mozStorageAsyncStatement_h_ + +#include "nsAutoPtr.h" +#include "nsString.h" + +#include "nsTArray.h" + +#include "mozStorageBindingParamsArray.h" +#include "mozStorageStatementData.h" +#include "mozIStorageAsyncStatement.h" +#include "StorageBaseStatementInternal.h" +#include "mozilla/Attributes.h" + +class nsIXPConnectJSObjectHolder; + +namespace mozilla { +namespace storage { + +class AsyncStatementJSHelper; +class Connection; + +class AsyncStatement final : public mozIStorageAsyncStatement + , public StorageBaseStatementInternal +{ +public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_MOZISTORAGEASYNCSTATEMENT + NS_DECL_MOZISTORAGEBASESTATEMENT + NS_DECL_MOZISTORAGEBINDINGPARAMS + NS_DECL_STORAGEBASESTATEMENTINTERNAL + + AsyncStatement(); + + /** + * Initializes the object on aDBConnection by preparing the SQL statement + * given by aSQLStatement. + * + * @param aDBConnection + * The Connection object this statement is associated with. + * @param aNativeConnection + * The native Sqlite connection this statement is associated with. + * @param aSQLStatement + * The SQL statement to prepare that this object will represent. + */ + nsresult initialize(Connection *aDBConnection, + sqlite3 *aNativeConnection, + const nsACString &aSQLStatement); + + /** + * Obtains and transfers ownership of the array of parameters that are bound + * to this statment. This can be null. + */ + inline already_AddRefed<BindingParamsArray> bindingParamsArray() + { + return mParamsArray.forget(); + } + + +private: + ~AsyncStatement(); + + /** + * @return a pointer to the BindingParams object to use with our Bind* + * method. + */ + mozIStorageBindingParams *getParams(); + + /** + * The SQL string as passed by the user. We store it because we create the + * async statement on-demand on the async thread. + */ + nsCString mSQLString; + + /** + * Holds the array of parameters to bind to this statement when we execute + * it asynchronously. + */ + RefPtr<BindingParamsArray> mParamsArray; + + /** + * Caches the JS 'params' helper for this statement. + */ + nsMainThreadPtrHandle<nsIXPConnectJSObjectHolder> mStatementParamsHolder; + + /** + * Have we been explicitly finalized by the user? + */ + bool mFinalized; + + /** + * Required for access to private mStatementParamsHolder field by + * AsyncStatementJSHelper::getParams. + */ + friend class AsyncStatementJSHelper; +}; + +} // namespace storage +} // namespace mozilla + +#endif // mozilla_storage_mozStorageAsyncStatement_h_ diff --git a/storage/mozStorageAsyncStatementExecution.cpp b/storage/mozStorageAsyncStatementExecution.cpp new file mode 100644 index 000000000..add32131a --- /dev/null +++ b/storage/mozStorageAsyncStatementExecution.cpp @@ -0,0 +1,649 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsAutoPtr.h" + +#include "sqlite3.h" + +#include "mozIStorageStatementCallback.h" +#include "mozStorageBindingParams.h" +#include "mozStorageHelper.h" +#include "mozStorageResultSet.h" +#include "mozStorageRow.h" +#include "mozStorageConnection.h" +#include "mozStorageError.h" +#include "mozStoragePrivateHelpers.h" +#include "mozStorageStatementData.h" +#include "mozStorageAsyncStatementExecution.h" + +#include "mozilla/DebugOnly.h" +#include "mozilla/Telemetry.h" + +namespace mozilla { +namespace storage { + +/** + * The following constants help batch rows into result sets. + * MAX_MILLISECONDS_BETWEEN_RESULTS was chosen because any user-based task that + * takes less than 200 milliseconds is considered to feel instantaneous to end + * users. MAX_ROWS_PER_RESULT was arbitrarily chosen to reduce the number of + * dispatches to calling thread, while also providing reasonably-sized sets of + * data for consumers. Both of these constants are used because we assume that + * consumers are trying to avoid blocking their execution thread for long + * periods of time, and dispatching many small events to the calling thread will + * end up blocking it. + */ +#define MAX_MILLISECONDS_BETWEEN_RESULTS 75 +#define MAX_ROWS_PER_RESULT 15 + +//////////////////////////////////////////////////////////////////////////////// +//// Local Classes + +namespace { + +typedef AsyncExecuteStatements::ExecutionState ExecutionState; +typedef AsyncExecuteStatements::StatementDataArray StatementDataArray; + +/** + * Notifies a callback with a result set. + */ +class CallbackResultNotifier : public Runnable +{ +public: + CallbackResultNotifier(mozIStorageStatementCallback *aCallback, + mozIStorageResultSet *aResults, + AsyncExecuteStatements *aEventStatus) : + mCallback(aCallback) + , mResults(aResults) + , mEventStatus(aEventStatus) + { + } + + NS_IMETHOD Run() override + { + NS_ASSERTION(mCallback, "Trying to notify about results without a callback!"); + + if (mEventStatus->shouldNotify()) { + // Hold a strong reference to the callback while notifying it, so that if + // it spins the event loop, the callback won't be released and freed out + // from under us. + nsCOMPtr<mozIStorageStatementCallback> callback = mCallback; + + (void)callback->HandleResult(mResults); + } + + return NS_OK; + } + +private: + mozIStorageStatementCallback *mCallback; + nsCOMPtr<mozIStorageResultSet> mResults; + RefPtr<AsyncExecuteStatements> mEventStatus; +}; + +/** + * Notifies the calling thread that an error has occurred. + */ +class ErrorNotifier : public Runnable +{ +public: + ErrorNotifier(mozIStorageStatementCallback *aCallback, + mozIStorageError *aErrorObj, + AsyncExecuteStatements *aEventStatus) : + mCallback(aCallback) + , mErrorObj(aErrorObj) + , mEventStatus(aEventStatus) + { + } + + NS_IMETHOD Run() override + { + if (mEventStatus->shouldNotify() && mCallback) { + // Hold a strong reference to the callback while notifying it, so that if + // it spins the event loop, the callback won't be released and freed out + // from under us. + nsCOMPtr<mozIStorageStatementCallback> callback = mCallback; + + (void)callback->HandleError(mErrorObj); + } + + return NS_OK; + } + +private: + mozIStorageStatementCallback *mCallback; + nsCOMPtr<mozIStorageError> mErrorObj; + RefPtr<AsyncExecuteStatements> mEventStatus; +}; + +/** + * Notifies the calling thread that the statement has finished executing. Takes + * ownership of the StatementData so it is released on the proper thread. + */ +class CompletionNotifier : public Runnable +{ +public: + /** + * This takes ownership of the callback and the StatementData. They are + * released on the thread this is dispatched to (which should always be the + * calling thread). + */ + CompletionNotifier(mozIStorageStatementCallback *aCallback, + ExecutionState aReason) + : mCallback(aCallback) + , mReason(aReason) + { + } + + NS_IMETHOD Run() override + { + if (mCallback) { + (void)mCallback->HandleCompletion(mReason); + NS_RELEASE(mCallback); + } + + return NS_OK; + } + +private: + mozIStorageStatementCallback *mCallback; + ExecutionState mReason; +}; + +} // namespace + +//////////////////////////////////////////////////////////////////////////////// +//// AsyncExecuteStatements + +/* static */ +nsresult +AsyncExecuteStatements::execute(StatementDataArray &aStatements, + Connection *aConnection, + sqlite3 *aNativeConnection, + mozIStorageStatementCallback *aCallback, + mozIStoragePendingStatement **_stmt) +{ + // Create our event to run in the background + RefPtr<AsyncExecuteStatements> event = + new AsyncExecuteStatements(aStatements, aConnection, aNativeConnection, + aCallback); + NS_ENSURE_TRUE(event, NS_ERROR_OUT_OF_MEMORY); + + // Dispatch it to the background + nsIEventTarget *target = aConnection->getAsyncExecutionTarget(); + + // If we don't have a valid target, this is a bug somewhere else. In the past, + // this assert found cases where a Run method would schedule a new statement + // without checking if asyncClose had been called. The caller must prevent + // that from happening or, if the work is not critical, just avoid creating + // the new statement during shutdown. See bug 718449 for an example. + MOZ_ASSERT(target); + if (!target) { + return NS_ERROR_NOT_AVAILABLE; + } + + nsresult rv = target->Dispatch(event, NS_DISPATCH_NORMAL); + NS_ENSURE_SUCCESS(rv, rv); + + // Return it as the pending statement object and track it. + event.forget(_stmt); + return NS_OK; +} + +AsyncExecuteStatements::AsyncExecuteStatements(StatementDataArray &aStatements, + Connection *aConnection, + sqlite3 *aNativeConnection, + mozIStorageStatementCallback *aCallback) +: mConnection(aConnection) +, mNativeConnection(aNativeConnection) +, mHasTransaction(false) +, mCallback(aCallback) +, mCallingThread(::do_GetCurrentThread()) +, mMaxWait(TimeDuration::FromMilliseconds(MAX_MILLISECONDS_BETWEEN_RESULTS)) +, mIntervalStart(TimeStamp::Now()) +, mState(PENDING) +, mCancelRequested(false) +, mMutex(aConnection->sharedAsyncExecutionMutex) +, mDBMutex(aConnection->sharedDBMutex) + , mRequestStartDate(TimeStamp::Now()) +{ + (void)mStatements.SwapElements(aStatements); + NS_ASSERTION(mStatements.Length(), "We weren't given any statements!"); + NS_IF_ADDREF(mCallback); +} + +AsyncExecuteStatements::~AsyncExecuteStatements() +{ + MOZ_ASSERT(!mHasTransaction, "There should be no transaction at this point"); +} + +bool +AsyncExecuteStatements::shouldNotify() +{ +#ifdef DEBUG + mMutex.AssertNotCurrentThreadOwns(); + + bool onCallingThread = false; + (void)mCallingThread->IsOnCurrentThread(&onCallingThread); + NS_ASSERTION(onCallingThread, "runEvent not running on the calling thread!"); +#endif + + // We do not need to acquire mMutex here because it can only ever be written + // to on the calling thread, and the only thread that can call us is the + // calling thread, so we know that our access is serialized. + return !mCancelRequested; +} + +bool +AsyncExecuteStatements::bindExecuteAndProcessStatement(StatementData &aData, + bool aLastStatement) +{ + mMutex.AssertNotCurrentThreadOwns(); + + sqlite3_stmt *aStatement = nullptr; + // This cannot fail; we are only called if it's available. + (void)aData.getSqliteStatement(&aStatement); + NS_ASSERTION(aStatement, "You broke the code; do not call here like that!"); + BindingParamsArray *paramsArray(aData); + + // Iterate through all of our parameters, bind them, and execute. + bool continueProcessing = true; + BindingParamsArray::iterator itr = paramsArray->begin(); + BindingParamsArray::iterator end = paramsArray->end(); + while (itr != end && continueProcessing) { + // Bind the data to our statement. + nsCOMPtr<IStorageBindingParamsInternal> bindingInternal = + do_QueryInterface(*itr); + nsCOMPtr<mozIStorageError> error = bindingInternal->bind(aStatement); + if (error) { + // Set our error state. + mState = ERROR; + + // And notify. + (void)notifyError(error); + return false; + } + + // Advance our iterator, execute, and then process the statement. + itr++; + bool lastStatement = aLastStatement && itr == end; + continueProcessing = executeAndProcessStatement(aStatement, lastStatement); + + // Always reset our statement. + (void)::sqlite3_reset(aStatement); + } + + return continueProcessing; +} + +bool +AsyncExecuteStatements::executeAndProcessStatement(sqlite3_stmt *aStatement, + bool aLastStatement) +{ + mMutex.AssertNotCurrentThreadOwns(); + + // Execute our statement + bool hasResults; + do { + hasResults = executeStatement(aStatement); + + // If we had an error, bail. + if (mState == ERROR) + return false; + + // If we have been canceled, there is no point in going on... + { + MutexAutoLock lockedScope(mMutex); + if (mCancelRequested) { + mState = CANCELED; + return false; + } + } + + // Build our result set and notify if we got anything back and have a + // callback to notify. + if (mCallback && hasResults && + NS_FAILED(buildAndNotifyResults(aStatement))) { + // We had an error notifying, so we notify on error and stop processing. + mState = ERROR; + + // Notify, and stop processing statements. + (void)notifyError(mozIStorageError::ERROR, + "An error occurred while notifying about results"); + + return false; + } + } while (hasResults); + +#ifndef MOZ_STORAGE_SORTWARNING_SQL_DUMP + if (MOZ_LOG_TEST(gStorageLog, LogLevel::Warning)) +#endif + { + // Check to make sure that this statement was smart about what it did. + checkAndLogStatementPerformance(aStatement); + } + + // If we are done, we need to set our state accordingly while we still hold + // our mutex. We would have already returned if we were canceled or had + // an error at this point. + if (aLastStatement) + mState = COMPLETED; + + return true; +} + +bool +AsyncExecuteStatements::executeStatement(sqlite3_stmt *aStatement) +{ + mMutex.AssertNotCurrentThreadOwns(); + Telemetry::AutoTimer<Telemetry::MOZ_STORAGE_ASYNC_REQUESTS_MS> finallySendExecutionDuration(mRequestStartDate); + while (true) { + // lock the sqlite mutex so sqlite3_errmsg cannot change + SQLiteMutexAutoLock lockedScope(mDBMutex); + + int rc = mConnection->stepStatement(mNativeConnection, aStatement); + // Stop if we have no more results. + if (rc == SQLITE_DONE) + { + Telemetry::Accumulate(Telemetry::MOZ_STORAGE_ASYNC_REQUESTS_SUCCESS, true); + return false; + } + + // If we got results, we can return now. + if (rc == SQLITE_ROW) + { + Telemetry::Accumulate(Telemetry::MOZ_STORAGE_ASYNC_REQUESTS_SUCCESS, true); + return true; + } + + // Some errors are not fatal, and we can handle them and continue. + if (rc == SQLITE_BUSY) { + // Don't hold the lock while we call outside our module. + SQLiteMutexAutoUnlock unlockedScope(mDBMutex); + + // Yield, and try again + (void)::PR_Sleep(PR_INTERVAL_NO_WAIT); + continue; + } + + // Set an error state. + mState = ERROR; + Telemetry::Accumulate(Telemetry::MOZ_STORAGE_ASYNC_REQUESTS_SUCCESS, false); + + // Construct the error message before giving up the mutex (which we cannot + // hold during the call to notifyError). + nsCOMPtr<mozIStorageError> errorObj( + new Error(rc, ::sqlite3_errmsg(mNativeConnection)) + ); + // We cannot hold the DB mutex while calling notifyError. + SQLiteMutexAutoUnlock unlockedScope(mDBMutex); + (void)notifyError(errorObj); + + // Finally, indicate that we should stop processing. + return false; + } +} + +nsresult +AsyncExecuteStatements::buildAndNotifyResults(sqlite3_stmt *aStatement) +{ + NS_ASSERTION(mCallback, "Trying to dispatch results without a callback!"); + mMutex.AssertNotCurrentThreadOwns(); + + // Build result object if we need it. + if (!mResultSet) + mResultSet = new ResultSet(); + NS_ENSURE_TRUE(mResultSet, NS_ERROR_OUT_OF_MEMORY); + + RefPtr<Row> row(new Row()); + NS_ENSURE_TRUE(row, NS_ERROR_OUT_OF_MEMORY); + + nsresult rv = row->initialize(aStatement); + NS_ENSURE_SUCCESS(rv, rv); + + rv = mResultSet->add(row); + NS_ENSURE_SUCCESS(rv, rv); + + // If we have hit our maximum number of allowed results, or if we have hit + // the maximum amount of time we want to wait for results, notify the + // calling thread about it. + TimeStamp now = TimeStamp::Now(); + TimeDuration delta = now - mIntervalStart; + if (mResultSet->rows() >= MAX_ROWS_PER_RESULT || delta > mMaxWait) { + // Notify the caller + rv = notifyResults(); + if (NS_FAILED(rv)) + return NS_OK; // we'll try again with the next result + + // Reset our start time + mIntervalStart = now; + } + + return NS_OK; +} + +nsresult +AsyncExecuteStatements::notifyComplete() +{ + mMutex.AssertNotCurrentThreadOwns(); + NS_ASSERTION(mState != PENDING, + "Still in a pending state when calling Complete!"); + + // Reset our statements before we try to commit or rollback. If we are + // canceling and have statements that think they have pending work, the + // rollback will fail. + for (uint32_t i = 0; i < mStatements.Length(); i++) + mStatements[i].reset(); + + // Release references to the statement data as soon as possible. If this + // is the last reference, statements will be finalized immediately on the + // async thread, hence avoiding several bounces between threads and possible + // race conditions with AsyncClose(). + mStatements.Clear(); + + // Handle our transaction, if we have one + if (mHasTransaction) { + if (mState == COMPLETED) { + nsresult rv = mConnection->commitTransactionInternal(mNativeConnection); + if (NS_FAILED(rv)) { + mState = ERROR; + (void)notifyError(mozIStorageError::ERROR, + "Transaction failed to commit"); + } + } + else { + DebugOnly<nsresult> rv = + mConnection->rollbackTransactionInternal(mNativeConnection); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Transaction failed to rollback"); + } + mHasTransaction = false; + } + + // Always generate a completion notification; it is what guarantees that our + // destruction does not happen here on the async thread. + RefPtr<CompletionNotifier> completionEvent = + new CompletionNotifier(mCallback, mState); + + // We no longer own mCallback (the CompletionNotifier takes ownership). + mCallback = nullptr; + + (void)mCallingThread->Dispatch(completionEvent, NS_DISPATCH_NORMAL); + + return NS_OK; +} + +nsresult +AsyncExecuteStatements::notifyError(int32_t aErrorCode, + const char *aMessage) +{ + mMutex.AssertNotCurrentThreadOwns(); + mDBMutex.assertNotCurrentThreadOwns(); + + if (!mCallback) + return NS_OK; + + nsCOMPtr<mozIStorageError> errorObj(new Error(aErrorCode, aMessage)); + NS_ENSURE_TRUE(errorObj, NS_ERROR_OUT_OF_MEMORY); + + return notifyError(errorObj); +} + +nsresult +AsyncExecuteStatements::notifyError(mozIStorageError *aError) +{ + mMutex.AssertNotCurrentThreadOwns(); + mDBMutex.assertNotCurrentThreadOwns(); + + if (!mCallback) + return NS_OK; + + RefPtr<ErrorNotifier> notifier = + new ErrorNotifier(mCallback, aError, this); + NS_ENSURE_TRUE(notifier, NS_ERROR_OUT_OF_MEMORY); + + return mCallingThread->Dispatch(notifier, NS_DISPATCH_NORMAL); +} + +nsresult +AsyncExecuteStatements::notifyResults() +{ + mMutex.AssertNotCurrentThreadOwns(); + NS_ASSERTION(mCallback, "notifyResults called without a callback!"); + + RefPtr<CallbackResultNotifier> notifier = + new CallbackResultNotifier(mCallback, mResultSet, this); + NS_ENSURE_TRUE(notifier, NS_ERROR_OUT_OF_MEMORY); + + nsresult rv = mCallingThread->Dispatch(notifier, NS_DISPATCH_NORMAL); + if (NS_SUCCEEDED(rv)) + mResultSet = nullptr; // we no longer own it on success + return rv; +} + +NS_IMPL_ISUPPORTS( + AsyncExecuteStatements, + nsIRunnable, + mozIStoragePendingStatement +) + +bool +AsyncExecuteStatements::statementsNeedTransaction() +{ + // If there is more than one write statement, run in a transaction. + // Additionally, if we have only one statement but it needs a transaction, due + // to multiple BindingParams, we will wrap it in one. + for (uint32_t i = 0, transactionsCount = 0; i < mStatements.Length(); ++i) { + transactionsCount += mStatements[i].needsTransaction(); + if (transactionsCount > 1) { + return true; + } + } + return false; +} + +//////////////////////////////////////////////////////////////////////////////// +//// mozIStoragePendingStatement + +NS_IMETHODIMP +AsyncExecuteStatements::Cancel() +{ +#ifdef DEBUG + bool onCallingThread = false; + (void)mCallingThread->IsOnCurrentThread(&onCallingThread); + NS_ASSERTION(onCallingThread, "Not canceling from the calling thread!"); +#endif + + // If we have already canceled, we have an error, but always indicate that + // we are trying to cancel. + NS_ENSURE_FALSE(mCancelRequested, NS_ERROR_UNEXPECTED); + + { + MutexAutoLock lockedScope(mMutex); + + // We need to indicate that we want to try and cancel now. + mCancelRequested = true; + } + + return NS_OK; +} + +//////////////////////////////////////////////////////////////////////////////// +//// nsIRunnable + +NS_IMETHODIMP +AsyncExecuteStatements::Run() +{ + MOZ_ASSERT(!mConnection->isClosed()); + + // Do not run if we have been canceled. + { + MutexAutoLock lockedScope(mMutex); + if (mCancelRequested) + mState = CANCELED; + } + if (mState == CANCELED) + return notifyComplete(); + + if (statementsNeedTransaction() && mConnection->getAutocommit()) { + if (NS_SUCCEEDED(mConnection->beginTransactionInternal(mNativeConnection, + mozIStorageConnection::TRANSACTION_IMMEDIATE))) { + mHasTransaction = true; + } +#ifdef DEBUG + else { + NS_WARNING("Unable to create a transaction for async execution."); + } +#endif + } + + // Execute each statement, giving the callback results if it returns any. + for (uint32_t i = 0; i < mStatements.Length(); i++) { + bool finished = (i == (mStatements.Length() - 1)); + + sqlite3_stmt *stmt; + { // lock the sqlite mutex so sqlite3_errmsg cannot change + SQLiteMutexAutoLock lockedScope(mDBMutex); + + int rc = mStatements[i].getSqliteStatement(&stmt); + if (rc != SQLITE_OK) { + // Set our error state. + mState = ERROR; + + // Build the error object; can't call notifyError with the lock held + nsCOMPtr<mozIStorageError> errorObj( + new Error(rc, ::sqlite3_errmsg(mNativeConnection)) + ); + { + // We cannot hold the DB mutex and call notifyError. + SQLiteMutexAutoUnlock unlockedScope(mDBMutex); + (void)notifyError(errorObj); + } + break; + } + } + + // If we have parameters to bind, bind them, execute, and process. + if (mStatements[i].hasParametersToBeBound()) { + if (!bindExecuteAndProcessStatement(mStatements[i], finished)) + break; + } + // Otherwise, just execute and process the statement. + else if (!executeAndProcessStatement(stmt, finished)) { + break; + } + } + + // If we still have results that we haven't notified about, take care of + // them now. + if (mResultSet) + (void)notifyResults(); + + // Notify about completion + return notifyComplete(); +} + +} // namespace storage +} // namespace mozilla diff --git a/storage/mozStorageAsyncStatementExecution.h b/storage/mozStorageAsyncStatementExecution.h new file mode 100644 index 000000000..c8493fd77 --- /dev/null +++ b/storage/mozStorageAsyncStatementExecution.h @@ -0,0 +1,242 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozStorageAsyncStatementExecution_h +#define mozStorageAsyncStatementExecution_h + +#include "nscore.h" +#include "nsTArray.h" +#include "nsAutoPtr.h" +#include "mozilla/Mutex.h" +#include "mozilla/TimeStamp.h" +#include "mozilla/Attributes.h" +#include "nsIRunnable.h" + +#include "SQLiteMutex.h" +#include "mozIStoragePendingStatement.h" +#include "mozIStorageStatementCallback.h" +#include "mozStorageHelper.h" + +struct sqlite3_stmt; + +namespace mozilla { +namespace storage { + +class Connection; +class ResultSet; +class StatementData; + +class AsyncExecuteStatements final : public nsIRunnable + , public mozIStoragePendingStatement +{ +public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIRUNNABLE + NS_DECL_MOZISTORAGEPENDINGSTATEMENT + + /** + * Describes the state of execution. + */ + enum ExecutionState { + PENDING = -1, + COMPLETED = mozIStorageStatementCallback::REASON_FINISHED, + CANCELED = mozIStorageStatementCallback::REASON_CANCELED, + ERROR = mozIStorageStatementCallback::REASON_ERROR + }; + + typedef nsTArray<StatementData> StatementDataArray; + + /** + * Executes a statement in the background, and passes results back to the + * caller. + * + * @param aStatements + * The statements to execute and possibly bind in the background. + * Ownership is transfered from the caller. + * @param aConnection + * The connection that created the statements to execute. + * @param aNativeConnection + * The native Sqlite connection that created the statements to execute. + * @param aCallback + * The callback that is notified of results, completion, and errors. + * @param _stmt + * The handle to control the execution of the statements. + */ + static nsresult execute(StatementDataArray &aStatements, + Connection *aConnection, + sqlite3 *aNativeConnection, + mozIStorageStatementCallback *aCallback, + mozIStoragePendingStatement **_stmt); + + /** + * Indicates when events on the calling thread should run or not. Certain + * events posted back to the calling thread should call this see if they + * should run or not. + * + * @pre mMutex is not held + * + * @returns true if the event should notify still, false otherwise. + */ + bool shouldNotify(); + +private: + AsyncExecuteStatements(StatementDataArray &aStatements, + Connection *aConnection, + sqlite3 *aNativeConnection, + mozIStorageStatementCallback *aCallback); + ~AsyncExecuteStatements(); + + /** + * Binds and then executes a given statement until completion, an error + * occurs, or we are canceled. If aLastStatement is true, we should set + * mState accordingly. + * + * @pre mMutex is not held + * + * @param aData + * The StatementData to bind, execute, and then process. + * @param aLastStatement + * Indicates if this is the last statement or not. If it is, we have + * to set the proper state. + * @returns true if we should continue to process statements, false otherwise. + */ + bool bindExecuteAndProcessStatement(StatementData &aData, + bool aLastStatement); + + /** + * Executes a given statement until completion, an error occurs, or we are + * canceled. If aLastStatement is true, we should set mState accordingly. + * + * @pre mMutex is not held + * + * @param aStatement + * The statement to execute and then process. + * @param aLastStatement + * Indicates if this is the last statement or not. If it is, we have + * to set the proper state. + * @returns true if we should continue to process statements, false otherwise. + */ + bool executeAndProcessStatement(sqlite3_stmt *aStatement, + bool aLastStatement); + + /** + * Executes a statement to completion, properly handling any error conditions. + * + * @pre mMutex is not held + * + * @param aStatement + * The statement to execute to completion. + * @returns true if results were obtained, false otherwise. + */ + bool executeStatement(sqlite3_stmt *aStatement); + + /** + * Builds a result set up with a row from a given statement. If we meet the + * right criteria, go ahead and notify about this results too. + * + * @pre mMutex is not held + * + * @param aStatement + * The statement to get the row data from. + */ + nsresult buildAndNotifyResults(sqlite3_stmt *aStatement); + + /** + * Notifies callback about completion, and does any necessary cleanup. + * + * @pre mMutex is not held + */ + nsresult notifyComplete(); + + /** + * Notifies callback about an error. + * + * @pre mMutex is not held + * @pre mDBMutex is not held + * + * @param aErrorCode + * The error code defined in mozIStorageError for the error. + * @param aMessage + * The error string, if any. + * @param aError + * The error object to notify the caller with. + */ + nsresult notifyError(int32_t aErrorCode, const char *aMessage); + nsresult notifyError(mozIStorageError *aError); + + /** + * Notifies the callback about a result set. + * + * @pre mMutex is not held + */ + nsresult notifyResults(); + + /** + * Tests whether the current statements should be wrapped in an explicit + * transaction. + * + * @return true if an explicit transaction is needed, false otherwise. + */ + bool statementsNeedTransaction(); + + StatementDataArray mStatements; + RefPtr<Connection> mConnection; + sqlite3 *mNativeConnection; + bool mHasTransaction; + mozIStorageStatementCallback *mCallback; + nsCOMPtr<nsIThread> mCallingThread; + RefPtr<ResultSet> mResultSet; + + /** + * The maximum amount of time we want to wait between results. Defined by + * MAX_MILLISECONDS_BETWEEN_RESULTS and set at construction. + */ + const TimeDuration mMaxWait; + + /** + * The start time since our last set of results. + */ + TimeStamp mIntervalStart; + + /** + * Indicates our state of execution. + */ + ExecutionState mState; + + /** + * Indicates if we should try to cancel at a cancelation point. + */ + bool mCancelRequested; + + /** + * This is the mutex that protects our state from changing between threads. + * This includes the following variables: + * - mCancelRequested is only set on the calling thread while the lock is + * held. It is always read from within the lock on the background thread, + * but not on the calling thread (see shouldNotify for why). + */ + Mutex &mMutex; + + /** + * The wrapped SQLite recursive connection mutex. We use it whenever we call + * sqlite3_step and care about having reliable error messages. By taking it + * prior to the call and holding it until the point where we no longer care + * about the error message, the user gets reliable error messages. + */ + SQLiteMutex &mDBMutex; + + /** + * The instant at which the request was started. + * + * Used by telemetry. + */ + TimeStamp mRequestStartDate; +}; + +} // namespace storage +} // namespace mozilla + +#endif // mozStorageAsyncStatementExecution_h diff --git a/storage/mozStorageAsyncStatementJSHelper.cpp b/storage/mozStorageAsyncStatementJSHelper.cpp new file mode 100644 index 000000000..321d37884 --- /dev/null +++ b/storage/mozStorageAsyncStatementJSHelper.cpp @@ -0,0 +1,153 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsIXPConnect.h" +#include "mozStorageAsyncStatement.h" +#include "mozStorageService.h" + +#include "nsMemory.h" +#include "nsString.h" +#include "nsServiceManagerUtils.h" + +#include "mozStorageAsyncStatementJSHelper.h" + +#include "mozStorageAsyncStatementParams.h" + +#include "jsapi.h" + +#include "xpc_make_class.h" + +namespace mozilla { +namespace storage { + +//////////////////////////////////////////////////////////////////////////////// +//// AsyncStatementJSHelper + +nsresult +AsyncStatementJSHelper::getParams(AsyncStatement *aStatement, + JSContext *aCtx, + JSObject *aScopeObj, + JS::Value *_params) +{ + MOZ_ASSERT(NS_IsMainThread()); + nsresult rv; + +#ifdef DEBUG + int32_t state; + (void)aStatement->GetState(&state); + NS_ASSERTION(state == mozIStorageAsyncStatement::MOZ_STORAGE_STATEMENT_READY, + "Invalid state to get the params object - all calls will fail!"); +#endif + + if (!aStatement->mStatementParamsHolder) { + nsCOMPtr<mozIStorageStatementParams> params = + new AsyncStatementParams(aStatement); + NS_ENSURE_TRUE(params, NS_ERROR_OUT_OF_MEMORY); + + JS::RootedObject scope(aCtx, aScopeObj); + nsCOMPtr<nsIXPConnectJSObjectHolder> holder; + nsCOMPtr<nsIXPConnect> xpc(Service::getXPConnect()); + rv = xpc->WrapNativeHolder( + aCtx, + ::JS_GetGlobalForObject(aCtx, scope), + params, + NS_GET_IID(mozIStorageStatementParams), + getter_AddRefs(holder) + ); + NS_ENSURE_SUCCESS(rv, rv); + RefPtr<AsyncStatementParamsHolder> paramsHolder = + new AsyncStatementParamsHolder(holder); + aStatement->mStatementParamsHolder = + new nsMainThreadPtrHolder<nsIXPConnectJSObjectHolder>(paramsHolder); + } + + JS::Rooted<JSObject*> obj(aCtx); + obj = aStatement->mStatementParamsHolder->GetJSObject(); + NS_ENSURE_STATE(obj); + + _params->setObject(*obj); + return NS_OK; +} + +NS_IMETHODIMP_(MozExternalRefCountType) AsyncStatementJSHelper::AddRef() { return 2; } +NS_IMETHODIMP_(MozExternalRefCountType) AsyncStatementJSHelper::Release() { return 1; } +NS_INTERFACE_MAP_BEGIN(AsyncStatementJSHelper) + NS_INTERFACE_MAP_ENTRY(nsIXPCScriptable) + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +//////////////////////////////////////////////////////////////////////////////// +//// nsIXPCScriptable + +#define XPC_MAP_CLASSNAME AsyncStatementJSHelper +#define XPC_MAP_QUOTED_CLASSNAME "AsyncStatementJSHelper" +#define XPC_MAP_WANT_GETPROPERTY +#define XPC_MAP_FLAGS nsIXPCScriptable::ALLOW_PROP_MODS_DURING_RESOLVE +#include "xpc_map_end.h" + +NS_IMETHODIMP +AsyncStatementJSHelper::GetProperty(nsIXPConnectWrappedNative *aWrapper, + JSContext *aCtx, + JSObject *aScopeObj, + jsid aId, + JS::Value *_result, + bool *_retval) +{ + if (!JSID_IS_STRING(aId)) + return NS_OK; + + // Cast to async via mozI* since direct from nsISupports is ambiguous. + JS::RootedObject scope(aCtx, aScopeObj); + JS::RootedId id(aCtx, aId); + mozIStorageAsyncStatement *iAsyncStmt = + static_cast<mozIStorageAsyncStatement *>(aWrapper->Native()); + AsyncStatement *stmt = static_cast<AsyncStatement *>(iAsyncStmt); + +#ifdef DEBUG + { + nsISupports *supp = aWrapper->Native(); + nsCOMPtr<mozIStorageAsyncStatement> isStatement(do_QueryInterface(supp)); + NS_ASSERTION(isStatement, "How is this not an async statement?!"); + } +#endif + + if (::JS_FlatStringEqualsAscii(JSID_TO_FLAT_STRING(id), "params")) + return getParams(stmt, aCtx, scope, _result); + + return NS_OK; +} + +//////////////////////////////////////////////////////////////////////////////// +//// AsyncStatementParamsHolder + +NS_IMPL_ISUPPORTS(AsyncStatementParamsHolder, nsIXPConnectJSObjectHolder); + +JSObject* +AsyncStatementParamsHolder::GetJSObject() +{ + return mHolder->GetJSObject(); +} + +AsyncStatementParamsHolder::AsyncStatementParamsHolder(nsIXPConnectJSObjectHolder* aHolder) + : mHolder(aHolder) +{ + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(mHolder); +} + +AsyncStatementParamsHolder::~AsyncStatementParamsHolder() +{ + MOZ_ASSERT(NS_IsMainThread()); + // We are considered dead at this point, so any wrappers for row or params + // need to lose their reference to the statement. + nsCOMPtr<nsIXPConnectWrappedNative> wrapper = do_QueryInterface(mHolder); + nsCOMPtr<mozIStorageStatementParams> iObj = do_QueryWrappedNative(wrapper); + AsyncStatementParams *obj = static_cast<AsyncStatementParams *>(iObj.get()); + obj->mStatement = nullptr; +} + +} // namespace storage +} // namespace mozilla diff --git a/storage/mozStorageAsyncStatementJSHelper.h b/storage/mozStorageAsyncStatementJSHelper.h new file mode 100644 index 000000000..df28225de --- /dev/null +++ b/storage/mozStorageAsyncStatementJSHelper.h @@ -0,0 +1,54 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_storage_mozStorageAsyncStatementJSHelper_h_ +#define mozilla_storage_mozStorageAsyncStatementJSHelper_h_ + +#include "nsIXPCScriptable.h" +#include "nsIXPConnect.h" + +class AsyncStatement; + +namespace mozilla { +namespace storage { + +/** + * A modified version of StatementJSHelper that only exposes the async-specific + * 'params' helper. We do not expose 'row' or 'step' as they do not apply to + * us. + */ +class AsyncStatementJSHelper : public nsIXPCScriptable +{ +public: + NS_DECL_ISUPPORTS + NS_DECL_NSIXPCSCRIPTABLE + +private: + nsresult getParams(AsyncStatement *, JSContext *, JSObject *, JS::Value *); +}; + +/** + * Wrapper used to clean up the references JS helpers hold to the statement. + * For cycle-avoidance reasons they do not hold reference-counted references, + * so it is important we do this. + */ +class AsyncStatementParamsHolder final : public nsIXPConnectJSObjectHolder +{ +public: + NS_DECL_ISUPPORTS + NS_DECL_NSIXPCONNECTJSOBJECTHOLDER + + explicit AsyncStatementParamsHolder(nsIXPConnectJSObjectHolder* aHolder); + +private: + virtual ~AsyncStatementParamsHolder(); + nsCOMPtr<nsIXPConnectJSObjectHolder> mHolder; +}; + +} // namespace storage +} // namespace mozilla + +#endif // mozilla_storage_mozStorageAsyncStatementJSHelper_h_ diff --git a/storage/mozStorageAsyncStatementParams.cpp b/storage/mozStorageAsyncStatementParams.cpp new file mode 100644 index 000000000..5e2d8c604 --- /dev/null +++ b/storage/mozStorageAsyncStatementParams.cpp @@ -0,0 +1,131 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsMemory.h" +#include "nsString.h" +#include "nsCOMPtr.h" +#include "nsJSUtils.h" + +#include "jsapi.h" + +#include "mozStoragePrivateHelpers.h" +#include "mozStorageAsyncStatement.h" +#include "mozStorageAsyncStatementParams.h" +#include "mozIStorageStatement.h" + +#include "xpc_make_class.h" + +namespace mozilla { +namespace storage { + +//////////////////////////////////////////////////////////////////////////////// +//// AsyncStatementParams + +AsyncStatementParams::AsyncStatementParams(AsyncStatement *aStatement) +: mStatement(aStatement) +{ + NS_ASSERTION(mStatement != nullptr, "mStatement is null"); +} + +NS_IMPL_ISUPPORTS( + AsyncStatementParams +, mozIStorageStatementParams +, nsIXPCScriptable +) + +//////////////////////////////////////////////////////////////////////////////// +//// nsIXPCScriptable + +#define XPC_MAP_CLASSNAME AsyncStatementParams +#define XPC_MAP_QUOTED_CLASSNAME "AsyncStatementParams" +#define XPC_MAP_WANT_SETPROPERTY +#define XPC_MAP_WANT_RESOLVE +#define XPC_MAP_FLAGS nsIXPCScriptable::ALLOW_PROP_MODS_DURING_RESOLVE +#include "xpc_map_end.h" + +NS_IMETHODIMP +AsyncStatementParams::SetProperty( + nsIXPConnectWrappedNative *aWrapper, + JSContext *aCtx, + JSObject *aScopeObj, + jsid aId, + JS::Value *_vp, + bool *_retval +) +{ + NS_ENSURE_TRUE(mStatement, NS_ERROR_NOT_INITIALIZED); + + if (JSID_IS_INT(aId)) { + int idx = JSID_TO_INT(aId); + + nsCOMPtr<nsIVariant> variant(convertJSValToVariant(aCtx, *_vp)); + NS_ENSURE_TRUE(variant, NS_ERROR_UNEXPECTED); + nsresult rv = mStatement->BindByIndex(idx, variant); + NS_ENSURE_SUCCESS(rv, rv); + } + else if (JSID_IS_STRING(aId)) { + JSString *str = JSID_TO_STRING(aId); + nsAutoJSString autoStr; + if (!autoStr.init(aCtx, str)) { + return NS_ERROR_FAILURE; + } + + NS_ConvertUTF16toUTF8 name(autoStr); + + nsCOMPtr<nsIVariant> variant(convertJSValToVariant(aCtx, *_vp)); + NS_ENSURE_TRUE(variant, NS_ERROR_UNEXPECTED); + nsresult rv = mStatement->BindByName(name, variant); + NS_ENSURE_SUCCESS(rv, rv); + } + else { + return NS_ERROR_INVALID_ARG; + } + + *_retval = true; + return NS_OK; +} + +NS_IMETHODIMP +AsyncStatementParams::Resolve(nsIXPConnectWrappedNative *aWrapper, + JSContext *aCtx, + JSObject *aScopeObj, + jsid aId, + bool *aResolvedp, + bool *_retval) +{ + JS::Rooted<JSObject*> scopeObj(aCtx, aScopeObj); + + NS_ENSURE_TRUE(mStatement, NS_ERROR_NOT_INITIALIZED); + // We do not throw at any point after this because we want to allow the + // prototype chain to be checked for the property. + + bool resolved = false; + bool ok = true; + if (JSID_IS_INT(aId)) { + uint32_t idx = JSID_TO_INT(aId); + // All indexes are good because we don't know how many parameters there + // really are. + ok = ::JS_DefineElement(aCtx, scopeObj, idx, JS::UndefinedHandleValue, + JSPROP_RESOLVING); + resolved = true; + } + else if (JSID_IS_STRING(aId)) { + // We are unable to tell if there's a parameter with this name and so + // we must assume that there is. This screws the rest of the prototype + // chain, but people really shouldn't be depending on this anyways. + JS::Rooted<jsid> id(aCtx, aId); + ok = ::JS_DefinePropertyById(aCtx, scopeObj, id, JS::UndefinedHandleValue, + JSPROP_RESOLVING); + resolved = true; + } + + *_retval = ok; + *aResolvedp = resolved && ok; + return NS_OK; +} + +} // namespace storage +} // namespace mozilla diff --git a/storage/mozStorageAsyncStatementParams.h b/storage/mozStorageAsyncStatementParams.h new file mode 100644 index 000000000..f753c6399 --- /dev/null +++ b/storage/mozStorageAsyncStatementParams.h @@ -0,0 +1,45 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_storage_mozStorageAsyncStatementParams_h_ +#define mozilla_storage_mozStorageAsyncStatementParams_h_ + +#include "mozIStorageStatementParams.h" +#include "nsIXPCScriptable.h" +#include "mozilla/Attributes.h" + +namespace mozilla { +namespace storage { + +class AsyncStatement; + +/* + * Since mozIStorageStatementParams is just a tagging interface we do not have + * an async variant. + */ +class AsyncStatementParams final : public mozIStorageStatementParams + , public nsIXPCScriptable +{ +public: + explicit AsyncStatementParams(AsyncStatement *aStatement); + + // interfaces + NS_DECL_ISUPPORTS + NS_DECL_MOZISTORAGESTATEMENTPARAMS + NS_DECL_NSIXPCSCRIPTABLE + +protected: + virtual ~AsyncStatementParams() {} + + AsyncStatement *mStatement; + + friend class AsyncStatementParamsHolder; +}; + +} // namespace storage +} // namespace mozilla + +#endif // mozilla_storage_mozStorageAsyncStatementParams_h_ diff --git a/storage/mozStorageBindingParams.cpp b/storage/mozStorageBindingParams.cpp new file mode 100644 index 000000000..98e114420 --- /dev/null +++ b/storage/mozStorageBindingParams.cpp @@ -0,0 +1,525 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include <limits.h> + +#include "mozilla/UniquePtrExtensions.h" +#include "nsString.h" + +#include "mozStorageError.h" +#include "mozStoragePrivateHelpers.h" +#include "mozStorageBindingParams.h" +#include "mozStorageBindingParamsArray.h" +#include "Variant.h" + +namespace mozilla { +namespace storage { + +//////////////////////////////////////////////////////////////////////////////// +//// Local Helper Objects + +namespace { + +struct BindingColumnData +{ + BindingColumnData(sqlite3_stmt *aStmt, + int aColumn) + : stmt(aStmt) + , column(aColumn) + { + } + sqlite3_stmt *stmt; + int column; +}; + +//////////////////////////////////////////////////////////////////////////////// +//// Variant Specialization Functions (variantToSQLiteT) + +int +sqlite3_T_int(BindingColumnData aData, + int aValue) +{ + return ::sqlite3_bind_int(aData.stmt, aData.column + 1, aValue); +} + +int +sqlite3_T_int64(BindingColumnData aData, + sqlite3_int64 aValue) +{ + return ::sqlite3_bind_int64(aData.stmt, aData.column + 1, aValue); +} + +int +sqlite3_T_double(BindingColumnData aData, + double aValue) +{ + return ::sqlite3_bind_double(aData.stmt, aData.column + 1, aValue); +} + +int +sqlite3_T_text(BindingColumnData aData, + const nsCString& aValue) +{ + return ::sqlite3_bind_text(aData.stmt, + aData.column + 1, + aValue.get(), + aValue.Length(), + SQLITE_TRANSIENT); +} + +int +sqlite3_T_text16(BindingColumnData aData, + const nsString& aValue) +{ + return ::sqlite3_bind_text16(aData.stmt, + aData.column + 1, + aValue.get(), + aValue.Length() * 2, // Length in bytes! + SQLITE_TRANSIENT); +} + +int +sqlite3_T_null(BindingColumnData aData) +{ + return ::sqlite3_bind_null(aData.stmt, aData.column + 1); +} + +int +sqlite3_T_blob(BindingColumnData aData, + const void *aBlob, + int aSize) +{ + return ::sqlite3_bind_blob(aData.stmt, aData.column + 1, aBlob, aSize, + free); + +} + +#include "variantToSQLiteT_impl.h" + +} // namespace + +//////////////////////////////////////////////////////////////////////////////// +//// BindingParams + +BindingParams::BindingParams(mozIStorageBindingParamsArray *aOwningArray, + Statement *aOwningStatement) +: mLocked(false) +, mOwningArray(aOwningArray) +, mOwningStatement(aOwningStatement) +, mParamCount(0) +{ + (void)mOwningStatement->GetParameterCount(&mParamCount); + mParameters.SetCapacity(mParamCount); +} + +BindingParams::BindingParams(mozIStorageBindingParamsArray *aOwningArray) +: mLocked(false) +, mOwningArray(aOwningArray) +, mOwningStatement(nullptr) +, mParamCount(0) +{ +} + +AsyncBindingParams::AsyncBindingParams( + mozIStorageBindingParamsArray *aOwningArray +) +: BindingParams(aOwningArray) +{ +} + +void +BindingParams::lock() +{ + NS_ASSERTION(mLocked == false, "Parameters have already been locked!"); + mLocked = true; + + // We no longer need to hold a reference to our statement or our owning array. + // The array owns us at this point, and it will own a reference to the + // statement. + mOwningStatement = nullptr; + mOwningArray = nullptr; +} + +void +BindingParams::unlock(Statement *aOwningStatement) +{ + NS_ASSERTION(mLocked == true, "Parameters were not yet locked!"); + mLocked = false; + mOwningStatement = aOwningStatement; +} + +const mozIStorageBindingParamsArray * +BindingParams::getOwner() const +{ + return mOwningArray; +} + +//////////////////////////////////////////////////////////////////////////////// +//// nsISupports + +NS_IMPL_ISUPPORTS( + BindingParams +, mozIStorageBindingParams +, IStorageBindingParamsInternal +) + + +//////////////////////////////////////////////////////////////////////////////// +//// IStorageBindingParamsInternal + +already_AddRefed<mozIStorageError> +BindingParams::bind(sqlite3_stmt *aStatement) +{ + // Iterate through all of our stored data, and bind it. + for (size_t i = 0; i < mParameters.Length(); i++) { + int rc = variantToSQLiteT(BindingColumnData(aStatement, i), mParameters[i]); + if (rc != SQLITE_OK) { + // We had an error while trying to bind. Now we need to create an error + // object with the right message. Note that we special case + // SQLITE_MISMATCH, but otherwise get the message from SQLite. + const char *msg = "Could not covert nsIVariant to SQLite type."; + if (rc != SQLITE_MISMATCH) + msg = ::sqlite3_errmsg(::sqlite3_db_handle(aStatement)); + + nsCOMPtr<mozIStorageError> err(new Error(rc, msg)); + return err.forget(); + } + } + + return nullptr; +} + +already_AddRefed<mozIStorageError> +AsyncBindingParams::bind(sqlite3_stmt * aStatement) +{ + // We should bind by index using the super-class if there is nothing in our + // hashtable. + if (!mNamedParameters.Count()) + return BindingParams::bind(aStatement); + + nsCOMPtr<mozIStorageError> err; + + for (auto iter = mNamedParameters.Iter(); !iter.Done(); iter.Next()) { + const nsACString &key = iter.Key(); + + // We do not accept any forms of names other than ":name", but we need to + // add the colon for SQLite. + nsAutoCString name(":"); + name.Append(key); + int oneIdx = ::sqlite3_bind_parameter_index(aStatement, name.get()); + + if (oneIdx == 0) { + nsAutoCString errMsg(key); + errMsg.AppendLiteral(" is not a valid named parameter."); + err = new Error(SQLITE_RANGE, errMsg.get()); + break; + } + + // XPCVariant's AddRef and Release are not thread-safe and so we must not + // do anything that would invoke them here on the async thread. As such we + // can't cram aValue into mParameters using ReplaceObjectAt so that + // we can freeload off of the BindingParams::Bind implementation. + int rc = variantToSQLiteT(BindingColumnData(aStatement, oneIdx - 1), + iter.UserData()); + if (rc != SQLITE_OK) { + // We had an error while trying to bind. Now we need to create an error + // object with the right message. Note that we special case + // SQLITE_MISMATCH, but otherwise get the message from SQLite. + const char *msg = "Could not covert nsIVariant to SQLite type."; + if (rc != SQLITE_MISMATCH) { + msg = ::sqlite3_errmsg(::sqlite3_db_handle(aStatement)); + } + err = new Error(rc, msg); + break; + } + } + + return err.forget(); +} + + +/////////////////////////////////////////////////////////////////////////////// +//// mozIStorageBindingParams + +NS_IMETHODIMP +BindingParams::BindByName(const nsACString &aName, + nsIVariant *aValue) +{ + NS_ENSURE_FALSE(mLocked, NS_ERROR_UNEXPECTED); + + // Get the column index that we need to store this at. + uint32_t index; + nsresult rv = mOwningStatement->GetParameterIndex(aName, &index); + NS_ENSURE_SUCCESS(rv, rv); + + return BindByIndex(index, aValue); +} + +NS_IMETHODIMP +AsyncBindingParams::BindByName(const nsACString &aName, + nsIVariant *aValue) +{ + NS_ENSURE_FALSE(mLocked, NS_ERROR_UNEXPECTED); + + RefPtr<Variant_base> variant = convertVariantToStorageVariant(aValue); + if (!variant) + return NS_ERROR_UNEXPECTED; + + mNamedParameters.Put(aName, variant); + return NS_OK; +} + + +NS_IMETHODIMP +BindingParams::BindUTF8StringByName(const nsACString &aName, + const nsACString &aValue) +{ + nsCOMPtr<nsIVariant> value(new UTF8TextVariant(aValue)); + NS_ENSURE_TRUE(value, NS_ERROR_OUT_OF_MEMORY); + + return BindByName(aName, value); +} + +NS_IMETHODIMP +BindingParams::BindStringByName(const nsACString &aName, + const nsAString &aValue) +{ + nsCOMPtr<nsIVariant> value(new TextVariant(aValue)); + NS_ENSURE_TRUE(value, NS_ERROR_OUT_OF_MEMORY); + + return BindByName(aName, value); +} + +NS_IMETHODIMP +BindingParams::BindDoubleByName(const nsACString &aName, + double aValue) +{ + nsCOMPtr<nsIVariant> value(new FloatVariant(aValue)); + NS_ENSURE_TRUE(value, NS_ERROR_OUT_OF_MEMORY); + + return BindByName(aName, value); +} + +NS_IMETHODIMP +BindingParams::BindInt32ByName(const nsACString &aName, + int32_t aValue) +{ + nsCOMPtr<nsIVariant> value(new IntegerVariant(aValue)); + NS_ENSURE_TRUE(value, NS_ERROR_OUT_OF_MEMORY); + + return BindByName(aName, value); +} + +NS_IMETHODIMP +BindingParams::BindInt64ByName(const nsACString &aName, + int64_t aValue) +{ + nsCOMPtr<nsIVariant> value(new IntegerVariant(aValue)); + NS_ENSURE_TRUE(value, NS_ERROR_OUT_OF_MEMORY); + + return BindByName(aName, value); +} + +NS_IMETHODIMP +BindingParams::BindNullByName(const nsACString &aName) +{ + nsCOMPtr<nsIVariant> value(new NullVariant()); + NS_ENSURE_TRUE(value, NS_ERROR_OUT_OF_MEMORY); + + return BindByName(aName, value); +} + +NS_IMETHODIMP +BindingParams::BindBlobByName(const nsACString &aName, + const uint8_t *aValue, + uint32_t aValueSize) +{ + NS_ENSURE_ARG_MAX(aValueSize, INT_MAX); + std::pair<const void *, int> data( + static_cast<const void *>(aValue), + int(aValueSize) + ); + nsCOMPtr<nsIVariant> value(new BlobVariant(data)); + NS_ENSURE_TRUE(value, NS_ERROR_OUT_OF_MEMORY); + + return BindByName(aName, value); +} + +NS_IMETHODIMP +BindingParams::BindStringAsBlobByName(const nsACString& aName, + const nsAString& aValue) +{ + return DoBindStringAsBlobByName(this, aName, aValue); +} + +NS_IMETHODIMP +BindingParams::BindUTF8StringAsBlobByName(const nsACString& aName, + const nsACString& aValue) +{ + return DoBindStringAsBlobByName(this, aName, aValue); +} + + +NS_IMETHODIMP +BindingParams::BindAdoptedBlobByName(const nsACString &aName, + uint8_t *aValue, + uint32_t aValueSize) +{ + UniqueFreePtr<uint8_t> uniqueValue(aValue); + NS_ENSURE_ARG_MAX(aValueSize, INT_MAX); + std::pair<uint8_t *, int> data(uniqueValue.release(), int(aValueSize)); + nsCOMPtr<nsIVariant> value(new AdoptedBlobVariant(data)); + + return BindByName(aName, value); +} + +NS_IMETHODIMP +BindingParams::BindByIndex(uint32_t aIndex, + nsIVariant *aValue) +{ + NS_ENSURE_FALSE(mLocked, NS_ERROR_UNEXPECTED); + ENSURE_INDEX_VALUE(aIndex, mParamCount); + + // Store the variant for later use. + RefPtr<Variant_base> variant = convertVariantToStorageVariant(aValue); + if (!variant) + return NS_ERROR_UNEXPECTED; + if (mParameters.Length() <= aIndex) { + (void)mParameters.SetLength(aIndex); + (void)mParameters.AppendElement(variant); + } + else { + NS_ENSURE_TRUE(mParameters.ReplaceElementAt(aIndex, variant), + NS_ERROR_OUT_OF_MEMORY); + } + return NS_OK; +} + +NS_IMETHODIMP +AsyncBindingParams::BindByIndex(uint32_t aIndex, + nsIVariant *aValue) +{ + NS_ENSURE_FALSE(mLocked, NS_ERROR_UNEXPECTED); + // In the asynchronous case we do not know how many parameters there are to + // bind to, so we cannot check the validity of aIndex. + + RefPtr<Variant_base> variant = convertVariantToStorageVariant(aValue); + if (!variant) + return NS_ERROR_UNEXPECTED; + if (mParameters.Length() <= aIndex) { + mParameters.SetLength(aIndex); + mParameters.AppendElement(variant); + } + else { + NS_ENSURE_TRUE(mParameters.ReplaceElementAt(aIndex, variant), + NS_ERROR_OUT_OF_MEMORY); + } + return NS_OK; +} + +NS_IMETHODIMP +BindingParams::BindUTF8StringByIndex(uint32_t aIndex, + const nsACString &aValue) +{ + nsCOMPtr<nsIVariant> value(new UTF8TextVariant(aValue)); + NS_ENSURE_TRUE(value, NS_ERROR_OUT_OF_MEMORY); + + return BindByIndex(aIndex, value); +} + +NS_IMETHODIMP +BindingParams::BindStringByIndex(uint32_t aIndex, + const nsAString &aValue) +{ + nsCOMPtr<nsIVariant> value(new TextVariant(aValue)); + NS_ENSURE_TRUE(value, NS_ERROR_OUT_OF_MEMORY); + + return BindByIndex(aIndex, value); +} + +NS_IMETHODIMP +BindingParams::BindDoubleByIndex(uint32_t aIndex, + double aValue) +{ + nsCOMPtr<nsIVariant> value(new FloatVariant(aValue)); + NS_ENSURE_TRUE(value, NS_ERROR_OUT_OF_MEMORY); + + return BindByIndex(aIndex, value); +} + +NS_IMETHODIMP +BindingParams::BindInt32ByIndex(uint32_t aIndex, + int32_t aValue) +{ + nsCOMPtr<nsIVariant> value(new IntegerVariant(aValue)); + NS_ENSURE_TRUE(value, NS_ERROR_OUT_OF_MEMORY); + + return BindByIndex(aIndex, value); +} + +NS_IMETHODIMP +BindingParams::BindInt64ByIndex(uint32_t aIndex, + int64_t aValue) +{ + nsCOMPtr<nsIVariant> value(new IntegerVariant(aValue)); + NS_ENSURE_TRUE(value, NS_ERROR_OUT_OF_MEMORY); + + return BindByIndex(aIndex, value); +} + +NS_IMETHODIMP +BindingParams::BindNullByIndex(uint32_t aIndex) +{ + nsCOMPtr<nsIVariant> value(new NullVariant()); + NS_ENSURE_TRUE(value, NS_ERROR_OUT_OF_MEMORY); + + return BindByIndex(aIndex, value); +} + +NS_IMETHODIMP +BindingParams::BindBlobByIndex(uint32_t aIndex, + const uint8_t *aValue, + uint32_t aValueSize) +{ + NS_ENSURE_ARG_MAX(aValueSize, INT_MAX); + std::pair<const void *, int> data( + static_cast<const void *>(aValue), + int(aValueSize) + ); + nsCOMPtr<nsIVariant> value(new BlobVariant(data)); + NS_ENSURE_TRUE(value, NS_ERROR_OUT_OF_MEMORY); + + return BindByIndex(aIndex, value); +} + +NS_IMETHODIMP +BindingParams::BindStringAsBlobByIndex(uint32_t aIndex, const nsAString& aValue) +{ + return DoBindStringAsBlobByIndex(this, aIndex, aValue); +} + +NS_IMETHODIMP +BindingParams::BindUTF8StringAsBlobByIndex(uint32_t aIndex, + const nsACString& aValue) +{ + return DoBindStringAsBlobByIndex(this, aIndex, aValue); +} + +NS_IMETHODIMP +BindingParams::BindAdoptedBlobByIndex(uint32_t aIndex, + uint8_t *aValue, + uint32_t aValueSize) +{ + UniqueFreePtr<uint8_t> uniqueValue(aValue); + NS_ENSURE_ARG_MAX(aValueSize, INT_MAX); + std::pair<uint8_t *, int> data(uniqueValue.release(), int(aValueSize)); + nsCOMPtr<nsIVariant> value(new AdoptedBlobVariant(data)); + + return BindByIndex(aIndex, value); +} + +} // namespace storage +} // namespace mozilla diff --git a/storage/mozStorageBindingParams.h b/storage/mozStorageBindingParams.h new file mode 100644 index 000000000..86d00b02b --- /dev/null +++ b/storage/mozStorageBindingParams.h @@ -0,0 +1,113 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozStorageBindingParams_h +#define mozStorageBindingParams_h + +#include "nsCOMArray.h" +#include "nsIVariant.h" +#include "nsInterfaceHashtable.h" + +#include "mozStorageBindingParamsArray.h" +#include "mozStorageStatement.h" +#include "mozStorageAsyncStatement.h" +#include "Variant.h" + +#include "mozIStorageBindingParams.h" +#include "IStorageBindingParamsInternal.h" + +namespace mozilla { +namespace storage { + +class BindingParams : public mozIStorageBindingParams + , public IStorageBindingParamsInternal +{ +public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_MOZISTORAGEBINDINGPARAMS + NS_DECL_ISTORAGEBINDINGPARAMSINTERNAL + + /** + * Locks the parameters and prevents further modification to it (such as + * binding more elements to it). + */ + void lock(); + + /** + * Unlocks the parameters and allows modification to it again. + * + * @param aOwningStatement + * The statement that owns us. We cleared this when we were locked, + * and our invariant requires us to have this, so you need to tell us + * again. + */ + void unlock(Statement *aOwningStatement); + + /** + * @returns the pointer to the owning BindingParamsArray. Used by a + * BindingParamsArray to verify that we belong to it when added. + */ + const mozIStorageBindingParamsArray *getOwner() const; + + BindingParams(mozIStorageBindingParamsArray *aOwningArray, + Statement *aOwningStatement); +protected: + virtual ~BindingParams() {} + + explicit BindingParams(mozIStorageBindingParamsArray *aOwningArray); + // Note that this is managed as a sparse array, so particular caution should + // be used for out-of-bounds usage. + nsTArray<RefPtr<Variant_base> > mParameters; + bool mLocked; + +private: + + /** + * Track the BindingParamsArray that created us until we are added to it. + * (Once we are added we are locked and no one needs to look up our owner.) + * Ref-counted since there is no invariant that guarantees it stays alive + * otherwise. This keeps mOwningStatement alive for us too since the array + * also holds a reference. + */ + nsCOMPtr<mozIStorageBindingParamsArray> mOwningArray; + /** + * Used in the synchronous binding case to map parameter names to indices. + * Not reference-counted because this is only non-null as long as mOwningArray + * is non-null and mOwningArray also holds a statement reference. + */ + Statement *mOwningStatement; + uint32_t mParamCount; +}; + +/** + * Adds late resolution of named parameters so they don't get resolved until we + * try and bind the parameters on the async thread. We also stop checking + * parameter indices for being too big since we just just don't know how many + * there are. + * + * We support *either* binding by name or binding by index. Trying to do both + * results in only binding by name at sqlite3_stmt bind time. + */ +class AsyncBindingParams : public BindingParams +{ +public: + NS_IMETHOD BindByName(const nsACString & aName, + nsIVariant *aValue); + NS_IMETHOD BindByIndex(uint32_t aIndex, nsIVariant *aValue); + + virtual already_AddRefed<mozIStorageError> bind(sqlite3_stmt * aStatement); + + explicit AsyncBindingParams(mozIStorageBindingParamsArray *aOwningArray); + virtual ~AsyncBindingParams() {} + +private: + nsInterfaceHashtable<nsCStringHashKey, nsIVariant> mNamedParameters; +}; + +} // namespace storage +} // namespace mozilla + +#endif // mozStorageBindingParams_h diff --git a/storage/mozStorageBindingParamsArray.cpp b/storage/mozStorageBindingParamsArray.cpp new file mode 100644 index 000000000..fb7c9f14a --- /dev/null +++ b/storage/mozStorageBindingParamsArray.cpp @@ -0,0 +1,90 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozStorageBindingParamsArray.h" +#include "mozStorageBindingParams.h" +#include "StorageBaseStatementInternal.h" + +namespace mozilla { +namespace storage { + +//////////////////////////////////////////////////////////////////////////////// +//// BindingParamsArray + +BindingParamsArray::BindingParamsArray( + StorageBaseStatementInternal *aOwningStatement +) +: mOwningStatement(aOwningStatement) +, mLocked(false) +{ +} + +void +BindingParamsArray::lock() +{ + NS_ASSERTION(mLocked == false, "Array has already been locked!"); + mLocked = true; + + // We also no longer need to hold a reference to our statement since it owns + // us. + mOwningStatement = nullptr; +} + +const StorageBaseStatementInternal * +BindingParamsArray::getOwner() const +{ + return mOwningStatement; +} + +NS_IMPL_ISUPPORTS( + BindingParamsArray, + mozIStorageBindingParamsArray +) + +/////////////////////////////////////////////////////////////////////////////// +//// mozIStorageBindingParamsArray + +NS_IMETHODIMP +BindingParamsArray::NewBindingParams(mozIStorageBindingParams **_params) +{ + NS_ENSURE_FALSE(mLocked, NS_ERROR_UNEXPECTED); + + nsCOMPtr<mozIStorageBindingParams> params( + mOwningStatement->newBindingParams(this)); + NS_ENSURE_TRUE(params, NS_ERROR_UNEXPECTED); + + params.forget(_params); + return NS_OK; +} + +NS_IMETHODIMP +BindingParamsArray::AddParams(mozIStorageBindingParams *aParameters) +{ + NS_ENSURE_FALSE(mLocked, NS_ERROR_UNEXPECTED); + + BindingParams *params = static_cast<BindingParams *>(aParameters); + + // Check to make sure that this set of parameters was created with us. + if (params->getOwner() != this) + return NS_ERROR_UNEXPECTED; + + NS_ENSURE_TRUE(mArray.AppendElement(params), NS_ERROR_OUT_OF_MEMORY); + + // Lock the parameters only after we've successfully added them. + params->lock(); + + return NS_OK; +} + +NS_IMETHODIMP +BindingParamsArray::GetLength(uint32_t *_length) +{ + *_length = length(); + return NS_OK; +} + +} // namespace storage +} // namespace mozilla diff --git a/storage/mozStorageBindingParamsArray.h b/storage/mozStorageBindingParamsArray.h new file mode 100644 index 000000000..4626ab55f --- /dev/null +++ b/storage/mozStorageBindingParamsArray.h @@ -0,0 +1,116 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozStorageBindingParamsArray_h +#define mozStorageBindingParamsArray_h + +#include "nsAutoPtr.h" +#include "nsTArray.h" +#include "mozilla/Attributes.h" + +#include "mozIStorageBindingParamsArray.h" + +namespace mozilla { +namespace storage { + +class StorageBaseStatementInternal; + +class BindingParamsArray final : public mozIStorageBindingParamsArray +{ + typedef nsTArray< nsCOMPtr<mozIStorageBindingParams> > array_type; + + ~BindingParamsArray() {} + +public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_MOZISTORAGEBINDINGPARAMSARRAY + + explicit BindingParamsArray(StorageBaseStatementInternal *aOwningStatement); + + typedef array_type::size_type size_type; + + /** + * Locks the array and prevents further modification to it (such as adding + * more elements to it). + */ + void lock(); + + /** + * @return the pointer to the owning BindingParamsArray. + */ + const StorageBaseStatementInternal *getOwner() const; + + /** + * @return the number of elemets the array contains. + */ + size_type length() const { return mArray.Length(); } + + class iterator { + public: + iterator(BindingParamsArray *aArray, + uint32_t aIndex) + : mArray(aArray) + , mIndex(aIndex) + { + } + + iterator &operator++(int) + { + mIndex++; + return *this; + } + + bool operator==(const iterator &aOther) const + { + return mIndex == aOther.mIndex; + } + bool operator!=(const iterator &aOther) const + { + return !(*this == aOther); + } + mozIStorageBindingParams *operator*() + { + NS_ASSERTION(mIndex < mArray->length(), + "Dereferenceing an invalid value!"); + return mArray->mArray[mIndex].get(); + } + private: + void operator--() { } + BindingParamsArray *mArray; + uint32_t mIndex; + }; + + /** + * Obtains an iterator pointing to the beginning of the array. + */ + inline iterator begin() + { + NS_ASSERTION(length() != 0, + "Obtaining an iterator to the beginning with no elements!"); + return iterator(this, 0); + } + + /** + * Obtains an iterator pointing to the end of the array. + */ + inline iterator end() + { + NS_ASSERTION(mLocked, + "Obtaining an iterator to the end when we are not locked!"); + return iterator(this, length()); + } +private: + nsCOMPtr<StorageBaseStatementInternal> mOwningStatement; + array_type mArray; + bool mLocked; + + friend class iterator; +}; + +} // namespace storage +} // namespace mozilla + +#endif // mozStorageBindingParamsArray_h diff --git a/storage/mozStorageConnection.cpp b/storage/mozStorageConnection.cpp new file mode 100644 index 000000000..10297fca1 --- /dev/null +++ b/storage/mozStorageConnection.cpp @@ -0,0 +1,2040 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include <stdio.h> + +#include "nsError.h" +#include "nsIMutableArray.h" +#include "nsAutoPtr.h" +#include "nsIMemoryReporter.h" +#include "nsThreadUtils.h" +#include "nsIFile.h" +#include "nsIFileURL.h" +#include "mozilla/Telemetry.h" +#include "mozilla/Mutex.h" +#include "mozilla/CondVar.h" +#include "mozilla/Attributes.h" +#include "mozilla/ErrorNames.h" +#include "mozilla/Unused.h" +#include "mozilla/dom/quota/QuotaObject.h" + +#include "mozIStorageAggregateFunction.h" +#include "mozIStorageCompletionCallback.h" +#include "mozIStorageFunction.h" + +#include "mozStorageAsyncStatementExecution.h" +#include "mozStorageSQLFunctions.h" +#include "mozStorageConnection.h" +#include "mozStorageService.h" +#include "mozStorageStatement.h" +#include "mozStorageAsyncStatement.h" +#include "mozStorageArgValueArray.h" +#include "mozStoragePrivateHelpers.h" +#include "mozStorageStatementData.h" +#include "StorageBaseStatementInternal.h" +#include "SQLCollations.h" +#include "FileSystemModule.h" +#include "mozStorageHelper.h" +#include "GeckoProfiler.h" + +#include "mozilla/Logging.h" +#include "prprf.h" +#include "nsProxyRelease.h" +#include <algorithm> + +#define MIN_AVAILABLE_BYTES_PER_CHUNKED_GROWTH 524288000 // 500 MiB + +// Maximum size of the pages cache per connection. +#define MAX_CACHE_SIZE_KIBIBYTES 2048 // 2 MiB + +mozilla::LazyLogModule gStorageLog("mozStorage"); + +// Checks that the protected code is running on the main-thread only if the +// connection was also opened on it. +#ifdef DEBUG +#define CHECK_MAINTHREAD_ABUSE() \ + do { \ + nsCOMPtr<nsIThread> mainThread = do_GetMainThread(); \ + NS_WARNING_ASSERTION( \ + threadOpenedOn == mainThread || !NS_IsMainThread(), \ + "Using Storage synchronous API on main-thread, but the connection was " \ + "opened on another thread."); \ + } while(0) +#else +#define CHECK_MAINTHREAD_ABUSE() do { /* Nothing */ } while(0) +#endif + +namespace mozilla { +namespace storage { + +using mozilla::dom::quota::QuotaObject; + +namespace { + +int +nsresultToSQLiteResult(nsresult aXPCOMResultCode) +{ + if (NS_SUCCEEDED(aXPCOMResultCode)) { + return SQLITE_OK; + } + + switch (aXPCOMResultCode) { + case NS_ERROR_FILE_CORRUPTED: + return SQLITE_CORRUPT; + case NS_ERROR_FILE_ACCESS_DENIED: + return SQLITE_CANTOPEN; + case NS_ERROR_STORAGE_BUSY: + return SQLITE_BUSY; + case NS_ERROR_FILE_IS_LOCKED: + return SQLITE_LOCKED; + case NS_ERROR_FILE_READ_ONLY: + return SQLITE_READONLY; + case NS_ERROR_STORAGE_IOERR: + return SQLITE_IOERR; + case NS_ERROR_FILE_NO_DEVICE_SPACE: + return SQLITE_FULL; + case NS_ERROR_OUT_OF_MEMORY: + return SQLITE_NOMEM; + case NS_ERROR_UNEXPECTED: + return SQLITE_MISUSE; + case NS_ERROR_ABORT: + return SQLITE_ABORT; + case NS_ERROR_STORAGE_CONSTRAINT: + return SQLITE_CONSTRAINT; + default: + return SQLITE_ERROR; + } + + MOZ_MAKE_COMPILER_ASSUME_IS_UNREACHABLE("Must return in switch above!"); +} + +//////////////////////////////////////////////////////////////////////////////// +//// Variant Specialization Functions (variantToSQLiteT) + +int +sqlite3_T_int(sqlite3_context *aCtx, + int aValue) +{ + ::sqlite3_result_int(aCtx, aValue); + return SQLITE_OK; +} + +int +sqlite3_T_int64(sqlite3_context *aCtx, + sqlite3_int64 aValue) +{ + ::sqlite3_result_int64(aCtx, aValue); + return SQLITE_OK; +} + +int +sqlite3_T_double(sqlite3_context *aCtx, + double aValue) +{ + ::sqlite3_result_double(aCtx, aValue); + return SQLITE_OK; +} + +int +sqlite3_T_text(sqlite3_context *aCtx, + const nsCString &aValue) +{ + ::sqlite3_result_text(aCtx, + aValue.get(), + aValue.Length(), + SQLITE_TRANSIENT); + return SQLITE_OK; +} + +int +sqlite3_T_text16(sqlite3_context *aCtx, + const nsString &aValue) +{ + ::sqlite3_result_text16(aCtx, + aValue.get(), + aValue.Length() * 2, // Number of bytes. + SQLITE_TRANSIENT); + return SQLITE_OK; +} + +int +sqlite3_T_null(sqlite3_context *aCtx) +{ + ::sqlite3_result_null(aCtx); + return SQLITE_OK; +} + +int +sqlite3_T_blob(sqlite3_context *aCtx, + const void *aData, + int aSize) +{ + ::sqlite3_result_blob(aCtx, aData, aSize, free); + return SQLITE_OK; +} + +#include "variantToSQLiteT_impl.h" + +//////////////////////////////////////////////////////////////////////////////// +//// Modules + +struct Module +{ + const char* name; + int (*registerFunc)(sqlite3*, const char*); +}; + +Module gModules[] = { + { "filesystem", RegisterFileSystemModule } +}; + +//////////////////////////////////////////////////////////////////////////////// +//// Local Functions + +int tracefunc (unsigned aReason, void *aClosure, void *aP, void *aX) +{ + switch (aReason) { + case SQLITE_TRACE_STMT: { + // aP is a pointer to the prepared statement. + sqlite3_stmt* stmt = static_cast<sqlite3_stmt*>(aP); + // aX is a pointer to a string containing the unexpanded SQL or a comment, + // starting with "--"" in case of a trigger. + char* expanded = static_cast<char*>(aX); + // Simulate what sqlite_trace was doing. + if (!::strncmp(expanded, "--", 2)) { + MOZ_LOG(gStorageLog, LogLevel::Debug, + ("TRACE_STMT on %p: '%s'", aClosure, expanded)); + } else { + char* sql = ::sqlite3_expanded_sql(stmt); + MOZ_LOG(gStorageLog, LogLevel::Debug, + ("TRACE_STMT on %p: '%s'", aClosure, sql)); + ::sqlite3_free(sql); + } + break; + } + case SQLITE_TRACE_PROFILE: { + // aX is pointer to a 64bit integer containing nanoseconds it took to + // execute the last command. + sqlite_int64 time = *(static_cast<sqlite_int64*>(aX)) / 1000000; + if (time > 0) { + MOZ_LOG(gStorageLog, LogLevel::Debug, + ("TRACE_TIME on %p: %dms", aClosure, time)); + } + break; + } + } + return 0; +} + +void +basicFunctionHelper(sqlite3_context *aCtx, + int aArgc, + sqlite3_value **aArgv) +{ + void *userData = ::sqlite3_user_data(aCtx); + + mozIStorageFunction *func = static_cast<mozIStorageFunction *>(userData); + + RefPtr<ArgValueArray> arguments(new ArgValueArray(aArgc, aArgv)); + if (!arguments) + return; + + nsCOMPtr<nsIVariant> result; + nsresult rv = func->OnFunctionCall(arguments, getter_AddRefs(result)); + if (NS_FAILED(rv)) { + nsAutoCString errorMessage; + GetErrorName(rv, errorMessage); + errorMessage.InsertLiteral("User function returned ", 0); + errorMessage.Append('!'); + + NS_WARNING(errorMessage.get()); + + ::sqlite3_result_error(aCtx, errorMessage.get(), -1); + ::sqlite3_result_error_code(aCtx, nsresultToSQLiteResult(rv)); + return; + } + int retcode = variantToSQLiteT(aCtx, result); + if (retcode == SQLITE_IGNORE) { + ::sqlite3_result_int(aCtx, SQLITE_IGNORE); + } else if (retcode != SQLITE_OK) { + NS_WARNING("User function returned invalid data type!"); + ::sqlite3_result_error(aCtx, + "User function returned invalid data type", + -1); + } +} + +void +aggregateFunctionStepHelper(sqlite3_context *aCtx, + int aArgc, + sqlite3_value **aArgv) +{ + void *userData = ::sqlite3_user_data(aCtx); + mozIStorageAggregateFunction *func = + static_cast<mozIStorageAggregateFunction *>(userData); + + RefPtr<ArgValueArray> arguments(new ArgValueArray(aArgc, aArgv)); + if (!arguments) + return; + + if (NS_FAILED(func->OnStep(arguments))) + NS_WARNING("User aggregate step function returned error code!"); +} + +void +aggregateFunctionFinalHelper(sqlite3_context *aCtx) +{ + void *userData = ::sqlite3_user_data(aCtx); + mozIStorageAggregateFunction *func = + static_cast<mozIStorageAggregateFunction *>(userData); + + RefPtr<nsIVariant> result; + if (NS_FAILED(func->OnFinal(getter_AddRefs(result)))) { + NS_WARNING("User aggregate final function returned error code!"); + ::sqlite3_result_error(aCtx, + "User aggregate final function returned error code", + -1); + return; + } + + if (variantToSQLiteT(aCtx, result) != SQLITE_OK) { + NS_WARNING("User aggregate final function returned invalid data type!"); + ::sqlite3_result_error(aCtx, + "User aggregate final function returned invalid data type", + -1); + } +} + +/** + * This code is heavily based on the sample at: + * http://www.sqlite.org/unlock_notify.html + */ +class UnlockNotification +{ +public: + UnlockNotification() + : mMutex("UnlockNotification mMutex") + , mCondVar(mMutex, "UnlockNotification condVar") + , mSignaled(false) + { + } + + void Wait() + { + MutexAutoLock lock(mMutex); + while (!mSignaled) { + (void)mCondVar.Wait(); + } + } + + void Signal() + { + MutexAutoLock lock(mMutex); + mSignaled = true; + (void)mCondVar.Notify(); + } + +private: + Mutex mMutex; + CondVar mCondVar; + bool mSignaled; +}; + +void +UnlockNotifyCallback(void **aArgs, + int aArgsSize) +{ + for (int i = 0; i < aArgsSize; i++) { + UnlockNotification *notification = + static_cast<UnlockNotification *>(aArgs[i]); + notification->Signal(); + } +} + +int +WaitForUnlockNotify(sqlite3* aDatabase) +{ + UnlockNotification notification; + int srv = ::sqlite3_unlock_notify(aDatabase, UnlockNotifyCallback, + ¬ification); + MOZ_ASSERT(srv == SQLITE_LOCKED || srv == SQLITE_OK); + if (srv == SQLITE_OK) { + notification.Wait(); + } + + return srv; +} + +} // namespace + +//////////////////////////////////////////////////////////////////////////////// +//// Local Classes + +namespace { + +class AsyncCloseConnection final: public Runnable +{ +public: + AsyncCloseConnection(Connection *aConnection, + sqlite3 *aNativeConnection, + nsIRunnable *aCallbackEvent, + already_AddRefed<nsIThread> aAsyncExecutionThread) + : mConnection(aConnection) + , mNativeConnection(aNativeConnection) + , mCallbackEvent(aCallbackEvent) + , mAsyncExecutionThread(aAsyncExecutionThread) + { + } + + NS_IMETHOD Run() override + { +#ifdef DEBUG + // This code is executed on the background thread + bool onAsyncThread = false; + (void)mAsyncExecutionThread->IsOnCurrentThread(&onAsyncThread); + MOZ_ASSERT(onAsyncThread); +#endif // DEBUG + + nsCOMPtr<nsIRunnable> event = NewRunnableMethod<nsCOMPtr<nsIThread>> + (mConnection, &Connection::shutdownAsyncThread, mAsyncExecutionThread); + (void)NS_DispatchToMainThread(event); + + // Internal close. + (void)mConnection->internalClose(mNativeConnection); + + // Callback + if (mCallbackEvent) { + nsCOMPtr<nsIThread> thread; + (void)NS_GetMainThread(getter_AddRefs(thread)); + (void)thread->Dispatch(mCallbackEvent, NS_DISPATCH_NORMAL); + } + + return NS_OK; + } + + ~AsyncCloseConnection() { + NS_ReleaseOnMainThread(mConnection.forget()); + NS_ReleaseOnMainThread(mCallbackEvent.forget()); + } +private: + RefPtr<Connection> mConnection; + sqlite3 *mNativeConnection; + nsCOMPtr<nsIRunnable> mCallbackEvent; + nsCOMPtr<nsIThread> mAsyncExecutionThread; +}; + +/** + * An event used to initialize the clone of a connection. + * + * Must be executed on the clone's async execution thread. + */ +class AsyncInitializeClone final: public Runnable +{ +public: + /** + * @param aConnection The connection being cloned. + * @param aClone The clone. + * @param aReadOnly If |true|, the clone is read only. + * @param aCallback A callback to trigger once initialization + * is complete. This event will be called on + * aClone->threadOpenedOn. + */ + AsyncInitializeClone(Connection* aConnection, + Connection* aClone, + const bool aReadOnly, + mozIStorageCompletionCallback* aCallback) + : mConnection(aConnection) + , mClone(aClone) + , mReadOnly(aReadOnly) + , mCallback(aCallback) + { + MOZ_ASSERT(NS_IsMainThread()); + } + + NS_IMETHOD Run() override { + MOZ_ASSERT (NS_GetCurrentThread() == mConnection->getAsyncExecutionTarget()); + + nsresult rv = mConnection->initializeClone(mClone, mReadOnly); + if (NS_FAILED(rv)) { + return Dispatch(rv, nullptr); + } + return Dispatch(NS_OK, + NS_ISUPPORTS_CAST(mozIStorageAsyncConnection*, mClone)); + } + +private: + nsresult Dispatch(nsresult aResult, nsISupports* aValue) { + RefPtr<CallbackComplete> event = new CallbackComplete(aResult, + aValue, + mCallback.forget()); + return mClone->threadOpenedOn->Dispatch(event, NS_DISPATCH_NORMAL); + } + + ~AsyncInitializeClone() { + nsCOMPtr<nsIThread> thread; + DebugOnly<nsresult> rv = NS_GetMainThread(getter_AddRefs(thread)); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + + // Handle ambiguous nsISupports inheritance. + NS_ProxyRelease(thread, mConnection.forget()); + NS_ProxyRelease(thread, mClone.forget()); + + // Generally, the callback will be released by CallbackComplete. + // However, if for some reason Run() is not executed, we still + // need to ensure that it is released here. + NS_ProxyRelease(thread, mCallback.forget()); + } + + RefPtr<Connection> mConnection; + RefPtr<Connection> mClone; + const bool mReadOnly; + nsCOMPtr<mozIStorageCompletionCallback> mCallback; +}; + +} // namespace + +//////////////////////////////////////////////////////////////////////////////// +//// Connection + +Connection::Connection(Service *aService, + int aFlags, + bool aAsyncOnly, + bool aIgnoreLockingMode) +: sharedAsyncExecutionMutex("Connection::sharedAsyncExecutionMutex") +, sharedDBMutex("Connection::sharedDBMutex") +, threadOpenedOn(do_GetCurrentThread()) +, mDBConn(nullptr) +, mAsyncExecutionThreadShuttingDown(false) +#ifdef DEBUG +, mAsyncExecutionThreadIsAlive(false) +#endif +, mConnectionClosed(false) +, mTransactionInProgress(false) +, mProgressHandler(nullptr) +, mFlags(aFlags) +, mIgnoreLockingMode(aIgnoreLockingMode) +, mStorageService(aService) +, mAsyncOnly(aAsyncOnly) +{ + MOZ_ASSERT(!mIgnoreLockingMode || mFlags & SQLITE_OPEN_READONLY, + "Can't ignore locking for a non-readonly connection!"); + mStorageService->registerConnection(this); +} + +Connection::~Connection() +{ + (void)Close(); + + MOZ_ASSERT(!mAsyncExecutionThread, + "AsyncClose has not been invoked on this connection!"); + MOZ_ASSERT(!mAsyncExecutionThreadIsAlive, + "The async execution thread should have been shutdown!"); +} + +NS_IMPL_ADDREF(Connection) + +NS_INTERFACE_MAP_BEGIN(Connection) + NS_INTERFACE_MAP_ENTRY(mozIStorageAsyncConnection) + NS_INTERFACE_MAP_ENTRY(nsIInterfaceRequestor) + NS_INTERFACE_MAP_ENTRY_CONDITIONAL(mozIStorageConnection, !mAsyncOnly) + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, mozIStorageConnection) +NS_INTERFACE_MAP_END + +// This is identical to what NS_IMPL_RELEASE provides, but with the +// extra |1 == count| case. +NS_IMETHODIMP_(MozExternalRefCountType) Connection::Release(void) +{ + NS_PRECONDITION(0 != mRefCnt, "dup release"); + nsrefcnt count = --mRefCnt; + NS_LOG_RELEASE(this, count, "Connection"); + if (1 == count) { + // If the refcount is 1, the single reference must be from + // gService->mConnections (in class |Service|). Which means we can + // unregister it safely. + mStorageService->unregisterConnection(this); + } else if (0 == count) { + mRefCnt = 1; /* stabilize */ +#if 0 /* enable this to find non-threadsafe destructors: */ + NS_ASSERT_OWNINGTHREAD(Connection); +#endif + delete (this); + return 0; + } + return count; +} + +int32_t +Connection::getSqliteRuntimeStatus(int32_t aStatusOption, int32_t* aMaxValue) +{ + MOZ_ASSERT(mDBConn, "A connection must exist at this point"); + int curr = 0, max = 0; + DebugOnly<int> rc = ::sqlite3_db_status(mDBConn, aStatusOption, &curr, &max, 0); + MOZ_ASSERT(NS_SUCCEEDED(convertResultCode(rc))); + if (aMaxValue) + *aMaxValue = max; + return curr; +} + +nsIEventTarget * +Connection::getAsyncExecutionTarget() +{ + MutexAutoLock lockedScope(sharedAsyncExecutionMutex); + + // If we are shutting down the asynchronous thread, don't hand out any more + // references to the thread. + if (mAsyncExecutionThreadShuttingDown) + return nullptr; + + if (!mAsyncExecutionThread) { + nsresult rv = ::NS_NewThread(getter_AddRefs(mAsyncExecutionThread)); + if (NS_FAILED(rv)) { + NS_WARNING("Failed to create async thread."); + return nullptr; + } + static nsThreadPoolNaming naming; + naming.SetThreadPoolName(NS_LITERAL_CSTRING("mozStorage"), + mAsyncExecutionThread); + } + +#ifdef DEBUG + mAsyncExecutionThreadIsAlive = true; +#endif + + return mAsyncExecutionThread; +} + +nsresult +Connection::initialize() +{ + NS_ASSERTION (!mDBConn, "Initialize called on already opened database!"); + MOZ_ASSERT(!mIgnoreLockingMode, "Can't ignore locking on an in-memory db."); + PROFILER_LABEL("mozStorageConnection", "initialize", + js::ProfileEntry::Category::STORAGE); + + // in memory database requested, sqlite uses a magic file name + int srv = ::sqlite3_open_v2(":memory:", &mDBConn, mFlags, nullptr); + if (srv != SQLITE_OK) { + mDBConn = nullptr; + return convertResultCode(srv); + } + + // Do not set mDatabaseFile or mFileURL here since this is a "memory" + // database. + + nsresult rv = initializeInternal(); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +nsresult +Connection::initialize(nsIFile *aDatabaseFile) +{ + NS_ASSERTION (aDatabaseFile, "Passed null file!"); + NS_ASSERTION (!mDBConn, "Initialize called on already opened database!"); + PROFILER_LABEL("mozStorageConnection", "initialize", + js::ProfileEntry::Category::STORAGE); + + mDatabaseFile = aDatabaseFile; + + nsAutoString path; + nsresult rv = aDatabaseFile->GetPath(path); + NS_ENSURE_SUCCESS(rv, rv); + +#ifdef XP_WIN + static const char* sIgnoreLockingVFS = "win32-none"; +#else + static const char* sIgnoreLockingVFS = "unix-none"; +#endif + const char* vfs = mIgnoreLockingMode ? sIgnoreLockingVFS : nullptr; + + int srv = ::sqlite3_open_v2(NS_ConvertUTF16toUTF8(path).get(), &mDBConn, + mFlags, vfs); + if (srv != SQLITE_OK) { + mDBConn = nullptr; + return convertResultCode(srv); + } + + // Do not set mFileURL here since this is database does not have an associated + // URL. + mDatabaseFile = aDatabaseFile; + + rv = initializeInternal(); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +nsresult +Connection::initialize(nsIFileURL *aFileURL) +{ + NS_ASSERTION (aFileURL, "Passed null file URL!"); + NS_ASSERTION (!mDBConn, "Initialize called on already opened database!"); + PROFILER_LABEL("mozStorageConnection", "initialize", + js::ProfileEntry::Category::STORAGE); + + nsCOMPtr<nsIFile> databaseFile; + nsresult rv = aFileURL->GetFile(getter_AddRefs(databaseFile)); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoCString spec; + rv = aFileURL->GetSpec(spec); + NS_ENSURE_SUCCESS(rv, rv); + + int srv = ::sqlite3_open_v2(spec.get(), &mDBConn, mFlags, nullptr); + if (srv != SQLITE_OK) { + mDBConn = nullptr; + return convertResultCode(srv); + } + + // Set both mDatabaseFile and mFileURL here. + mFileURL = aFileURL; + mDatabaseFile = databaseFile; + + rv = initializeInternal(); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +nsresult +Connection::initializeInternal() +{ + MOZ_ASSERT(mDBConn); + + if (mFileURL) { + const char* dbPath = ::sqlite3_db_filename(mDBConn, "main"); + MOZ_ASSERT(dbPath); + + const char* telemetryFilename = + ::sqlite3_uri_parameter(dbPath, "telemetryFilename"); + if (telemetryFilename) { + if (NS_WARN_IF(*telemetryFilename == '\0')) { + return NS_ERROR_INVALID_ARG; + } + mTelemetryFilename = telemetryFilename; + } + } + + if (mTelemetryFilename.IsEmpty()) { + mTelemetryFilename = getFilename(); + MOZ_ASSERT(!mTelemetryFilename.IsEmpty()); + } + + // Properly wrap the database handle's mutex. + sharedDBMutex.initWithMutex(sqlite3_db_mutex(mDBConn)); + + // SQLite tracing can slow down queries (especially long queries) + // significantly. Don't trace unless the user is actively monitoring SQLite. + if (MOZ_LOG_TEST(gStorageLog, LogLevel::Debug)) { + ::sqlite3_trace_v2(mDBConn, + SQLITE_TRACE_STMT | SQLITE_TRACE_PROFILE, + tracefunc, this); + + MOZ_LOG(gStorageLog, LogLevel::Debug, ("Opening connection to '%s' (%p)", + mTelemetryFilename.get(), this)); + } + + int64_t pageSize = Service::getDefaultPageSize(); + + // Set page_size to the preferred default value. This is effective only if + // the database has just been created, otherwise, if the database does not + // use WAL journal mode, a VACUUM operation will updated its page_size. + nsAutoCString pageSizeQuery(MOZ_STORAGE_UNIQUIFY_QUERY_STR + "PRAGMA page_size = "); + pageSizeQuery.AppendInt(pageSize); + nsresult rv = ExecuteSimpleSQL(pageSizeQuery); + NS_ENSURE_SUCCESS(rv, rv); + + // Setting the cache_size forces the database open, verifying if it is valid + // or corrupt. So this is executed regardless it being actually needed. + // The cache_size is calculated from the actual page_size, to save memory. + nsAutoCString cacheSizeQuery(MOZ_STORAGE_UNIQUIFY_QUERY_STR + "PRAGMA cache_size = "); + cacheSizeQuery.AppendInt(-MAX_CACHE_SIZE_KIBIBYTES); + int srv = executeSql(mDBConn, cacheSizeQuery.get()); + if (srv != SQLITE_OK) { + ::sqlite3_close(mDBConn); + mDBConn = nullptr; + return convertResultCode(srv); + } + +#if defined(MOZ_MEMORY_TEMP_STORE_PRAGMA) + (void)ExecuteSimpleSQL(NS_LITERAL_CSTRING("PRAGMA temp_store = 2;")); +#endif + + // Register our built-in SQL functions. + srv = registerFunctions(mDBConn); + if (srv != SQLITE_OK) { + ::sqlite3_close(mDBConn); + mDBConn = nullptr; + return convertResultCode(srv); + } + + // Register our built-in SQL collating sequences. + srv = registerCollations(mDBConn, mStorageService); + if (srv != SQLITE_OK) { + ::sqlite3_close(mDBConn); + mDBConn = nullptr; + return convertResultCode(srv); + } + + // Set the synchronous PRAGMA, according to the preference. + switch (Service::getSynchronousPref()) { + case 2: + (void)ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "PRAGMA synchronous = FULL;")); + break; + case 0: + (void)ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "PRAGMA synchronous = OFF;")); + break; + case 1: + default: + (void)ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "PRAGMA synchronous = NORMAL;")); + break; + } + + return NS_OK; +} + +nsresult +Connection::databaseElementExists(enum DatabaseElementType aElementType, + const nsACString &aElementName, + bool *_exists) +{ + if (!mDBConn) return NS_ERROR_NOT_INITIALIZED; + + // When constructing the query, make sure to SELECT the correct db's sqlite_master + // if the user is prefixing the element with a specific db. ex: sample.test + nsCString query("SELECT name FROM (SELECT * FROM "); + nsDependentCSubstring element; + int32_t ind = aElementName.FindChar('.'); + if (ind == kNotFound) { + element.Assign(aElementName); + } + else { + nsDependentCSubstring db(Substring(aElementName, 0, ind + 1)); + element.Assign(Substring(aElementName, ind + 1, aElementName.Length())); + query.Append(db); + } + query.AppendLiteral("sqlite_master UNION ALL SELECT * FROM sqlite_temp_master) WHERE type = '"); + + switch (aElementType) { + case INDEX: + query.AppendLiteral("index"); + break; + case TABLE: + query.AppendLiteral("table"); + break; + } + query.AppendLiteral("' AND name ='"); + query.Append(element); + query.Append('\''); + + sqlite3_stmt *stmt; + int srv = prepareStatement(mDBConn, query, &stmt); + if (srv != SQLITE_OK) + return convertResultCode(srv); + + srv = stepStatement(mDBConn, stmt); + // we just care about the return value from step + (void)::sqlite3_finalize(stmt); + + if (srv == SQLITE_ROW) { + *_exists = true; + return NS_OK; + } + if (srv == SQLITE_DONE) { + *_exists = false; + return NS_OK; + } + + return convertResultCode(srv); +} + +bool +Connection::findFunctionByInstance(nsISupports *aInstance) +{ + sharedDBMutex.assertCurrentThreadOwns(); + + for (auto iter = mFunctions.Iter(); !iter.Done(); iter.Next()) { + if (iter.UserData().function == aInstance) { + return true; + } + } + return false; +} + +/* static */ int +Connection::sProgressHelper(void *aArg) +{ + Connection *_this = static_cast<Connection *>(aArg); + return _this->progressHandler(); +} + +int +Connection::progressHandler() +{ + sharedDBMutex.assertCurrentThreadOwns(); + if (mProgressHandler) { + bool result; + nsresult rv = mProgressHandler->OnProgress(this, &result); + if (NS_FAILED(rv)) return 0; // Don't break request + return result ? 1 : 0; + } + return 0; +} + +nsresult +Connection::setClosedState() +{ + // Ensure that we are on the correct thread to close the database. + bool onOpenedThread; + nsresult rv = threadOpenedOn->IsOnCurrentThread(&onOpenedThread); + NS_ENSURE_SUCCESS(rv, rv); + if (!onOpenedThread) { + NS_ERROR("Must close the database on the thread that you opened it with!"); + return NS_ERROR_UNEXPECTED; + } + + // Flag that we are shutting down the async thread, so that + // getAsyncExecutionTarget knows not to expose/create the async thread. + { + MutexAutoLock lockedScope(sharedAsyncExecutionMutex); + NS_ENSURE_FALSE(mAsyncExecutionThreadShuttingDown, NS_ERROR_UNEXPECTED); + mAsyncExecutionThreadShuttingDown = true; + } + + // Set the property to null before closing the connection, otherwise the other + // functions in the module may try to use the connection after it is closed. + mDBConn = nullptr; + + return NS_OK; +} + +bool +Connection::connectionReady() +{ + return mDBConn != nullptr; +} + +bool +Connection::isClosing() +{ + bool shuttingDown = false; + { + MutexAutoLock lockedScope(sharedAsyncExecutionMutex); + shuttingDown = mAsyncExecutionThreadShuttingDown; + } + return shuttingDown && !isClosed(); +} + +bool +Connection::isClosed() +{ + MutexAutoLock lockedScope(sharedAsyncExecutionMutex); + return mConnectionClosed; +} + +void +Connection::shutdownAsyncThread(nsIThread *aThread) { + MOZ_ASSERT(!mAsyncExecutionThread); + MOZ_ASSERT(mAsyncExecutionThreadIsAlive); + MOZ_ASSERT(mAsyncExecutionThreadShuttingDown); + + DebugOnly<nsresult> rv = aThread->Shutdown(); + MOZ_ASSERT(NS_SUCCEEDED(rv)); +#ifdef DEBUG + mAsyncExecutionThreadIsAlive = false; +#endif +} + +nsresult +Connection::internalClose(sqlite3 *aNativeConnection) +{ + // Sanity checks to make sure we are in the proper state before calling this. + // aNativeConnection can be null if OpenAsyncDatabase failed and is now just + // cleaning up the async thread. + MOZ_ASSERT(!isClosed()); + +#ifdef DEBUG + { // Make sure we have marked our async thread as shutting down. + MutexAutoLock lockedScope(sharedAsyncExecutionMutex); + NS_ASSERTION(mAsyncExecutionThreadShuttingDown, + "Did not call setClosedState!"); + } +#endif // DEBUG + + if (MOZ_LOG_TEST(gStorageLog, LogLevel::Debug)) { + nsAutoCString leafName(":memory"); + if (mDatabaseFile) + (void)mDatabaseFile->GetNativeLeafName(leafName); + MOZ_LOG(gStorageLog, LogLevel::Debug, ("Closing connection to '%s'", + leafName.get())); + } + + // At this stage, we may still have statements that need to be + // finalized. Attempt to close the database connection. This will + // always disconnect any virtual tables and cleanly finalize their + // internal statements. Once this is done, closing may fail due to + // unfinalized client statements, in which case we need to finalize + // these statements and close again. + { + MutexAutoLock lockedScope(sharedAsyncExecutionMutex); + mConnectionClosed = true; + } + + // Nothing else needs to be done if we don't have a connection here. + if (!aNativeConnection) + return NS_OK; + + int srv = sqlite3_close(aNativeConnection); + + if (srv == SQLITE_BUSY) { + // We still have non-finalized statements. Finalize them. + + sqlite3_stmt *stmt = nullptr; + while ((stmt = ::sqlite3_next_stmt(aNativeConnection, stmt))) { + MOZ_LOG(gStorageLog, LogLevel::Debug, + ("Auto-finalizing SQL statement '%s' (%x)", + ::sqlite3_sql(stmt), + stmt)); + +#ifdef DEBUG + char *msg = ::PR_smprintf("SQL statement '%s' (%x) should have been finalized before closing the connection", + ::sqlite3_sql(stmt), + stmt); + NS_WARNING(msg); + ::PR_smprintf_free(msg); + msg = nullptr; +#endif // DEBUG + + srv = ::sqlite3_finalize(stmt); + +#ifdef DEBUG + if (srv != SQLITE_OK) { + msg = ::PR_smprintf("Could not finalize SQL statement '%s' (%x)", + ::sqlite3_sql(stmt), + stmt); + NS_WARNING(msg); + ::PR_smprintf_free(msg); + msg = nullptr; + } +#endif // DEBUG + + // Ensure that the loop continues properly, whether closing has succeeded + // or not. + if (srv == SQLITE_OK) { + stmt = nullptr; + } + } + + // Now that all statements have been finalized, we + // should be able to close. + srv = ::sqlite3_close(aNativeConnection); + + } + + if (srv != SQLITE_OK) { + MOZ_ASSERT(srv == SQLITE_OK, + "sqlite3_close failed. There are probably outstanding statements that are listed above!"); + } + + return convertResultCode(srv); +} + +nsCString +Connection::getFilename() +{ + nsCString leafname(":memory:"); + if (mDatabaseFile) { + (void)mDatabaseFile->GetNativeLeafName(leafname); + } + return leafname; +} + +int +Connection::stepStatement(sqlite3 *aNativeConnection, sqlite3_stmt *aStatement) +{ + MOZ_ASSERT(aStatement); + bool checkedMainThread = false; + TimeStamp startTime = TimeStamp::Now(); + + // The connection may have been closed if the executing statement has been + // created and cached after a call to asyncClose() but before the actual + // sqlite3_close(). This usually happens when other tasks using cached + // statements are asynchronously scheduled for execution and any of them ends + // up after asyncClose. See bug 728653 for details. + if (isClosed()) + return SQLITE_MISUSE; + + (void)::sqlite3_extended_result_codes(aNativeConnection, 1); + + int srv; + while ((srv = ::sqlite3_step(aStatement)) == SQLITE_LOCKED_SHAREDCACHE) { + if (!checkedMainThread) { + checkedMainThread = true; + if (::NS_IsMainThread()) { + NS_WARNING("We won't allow blocking on the main thread!"); + break; + } + } + + srv = WaitForUnlockNotify(aNativeConnection); + if (srv != SQLITE_OK) { + break; + } + + ::sqlite3_reset(aStatement); + } + + // Report very slow SQL statements to Telemetry + TimeDuration duration = TimeStamp::Now() - startTime; + const uint32_t threshold = + NS_IsMainThread() ? Telemetry::kSlowSQLThresholdForMainThread + : Telemetry::kSlowSQLThresholdForHelperThreads; + if (duration.ToMilliseconds() >= threshold) { + nsDependentCString statementString(::sqlite3_sql(aStatement)); + Telemetry::RecordSlowSQLStatement(statementString, mTelemetryFilename, + duration.ToMilliseconds()); + } + + (void)::sqlite3_extended_result_codes(aNativeConnection, 0); + // Drop off the extended result bits of the result code. + return srv & 0xFF; +} + +int +Connection::prepareStatement(sqlite3 *aNativeConnection, const nsCString &aSQL, + sqlite3_stmt **_stmt) +{ + // We should not even try to prepare statements after the connection has + // been closed. + if (isClosed()) + return SQLITE_MISUSE; + + bool checkedMainThread = false; + + (void)::sqlite3_extended_result_codes(aNativeConnection, 1); + + int srv; + while((srv = ::sqlite3_prepare_v2(aNativeConnection, + aSQL.get(), + -1, + _stmt, + nullptr)) == SQLITE_LOCKED_SHAREDCACHE) { + if (!checkedMainThread) { + checkedMainThread = true; + if (::NS_IsMainThread()) { + NS_WARNING("We won't allow blocking on the main thread!"); + break; + } + } + + srv = WaitForUnlockNotify(aNativeConnection); + if (srv != SQLITE_OK) { + break; + } + } + + if (srv != SQLITE_OK) { + nsCString warnMsg; + warnMsg.AppendLiteral("The SQL statement '"); + warnMsg.Append(aSQL); + warnMsg.AppendLiteral("' could not be compiled due to an error: "); + warnMsg.Append(::sqlite3_errmsg(aNativeConnection)); + +#ifdef DEBUG + NS_WARNING(warnMsg.get()); +#endif + MOZ_LOG(gStorageLog, LogLevel::Error, ("%s", warnMsg.get())); + } + + (void)::sqlite3_extended_result_codes(aNativeConnection, 0); + // Drop off the extended result bits of the result code. + int rc = srv & 0xFF; + // sqlite will return OK on a comment only string and set _stmt to nullptr. + // The callers of this function are used to only checking the return value, + // so it is safer to return an error code. + if (rc == SQLITE_OK && *_stmt == nullptr) { + return SQLITE_MISUSE; + } + + return rc; +} + + +int +Connection::executeSql(sqlite3 *aNativeConnection, const char *aSqlString) +{ + if (isClosed()) + return SQLITE_MISUSE; + + TimeStamp startTime = TimeStamp::Now(); + int srv = ::sqlite3_exec(aNativeConnection, aSqlString, nullptr, nullptr, + nullptr); + + // Report very slow SQL statements to Telemetry + TimeDuration duration = TimeStamp::Now() - startTime; + const uint32_t threshold = + NS_IsMainThread() ? Telemetry::kSlowSQLThresholdForMainThread + : Telemetry::kSlowSQLThresholdForHelperThreads; + if (duration.ToMilliseconds() >= threshold) { + nsDependentCString statementString(aSqlString); + Telemetry::RecordSlowSQLStatement(statementString, mTelemetryFilename, + duration.ToMilliseconds()); + } + + return srv; +} + +//////////////////////////////////////////////////////////////////////////////// +//// nsIInterfaceRequestor + +NS_IMETHODIMP +Connection::GetInterface(const nsIID &aIID, + void **_result) +{ + if (aIID.Equals(NS_GET_IID(nsIEventTarget))) { + nsIEventTarget *background = getAsyncExecutionTarget(); + NS_IF_ADDREF(background); + *_result = background; + return NS_OK; + } + return NS_ERROR_NO_INTERFACE; +} + +//////////////////////////////////////////////////////////////////////////////// +//// mozIStorageConnection + +NS_IMETHODIMP +Connection::Close() +{ + if (!mDBConn) + return NS_ERROR_NOT_INITIALIZED; + + { // Make sure we have not executed any asynchronous statements. + // If this fails, the mDBConn will be left open, resulting in a leak. + // Ideally we'd schedule some code to destroy the mDBConn once all its + // async statements have finished executing; see bug 704030. + MutexAutoLock lockedScope(sharedAsyncExecutionMutex); + bool asyncCloseWasCalled = !mAsyncExecutionThread; + NS_ENSURE_TRUE(asyncCloseWasCalled, NS_ERROR_UNEXPECTED); + } + + // setClosedState nullifies our connection pointer, so we take a raw pointer + // off it, to pass it through the close procedure. + sqlite3 *nativeConn = mDBConn; + nsresult rv = setClosedState(); + NS_ENSURE_SUCCESS(rv, rv); + + return internalClose(nativeConn); +} + +NS_IMETHODIMP +Connection::AsyncClose(mozIStorageCompletionCallback *aCallback) +{ + if (!NS_IsMainThread()) { + return NS_ERROR_NOT_SAME_THREAD; + } + + // The two relevant factors at this point are whether we have a database + // connection and whether we have an async execution thread. Here's what the + // states mean and how we handle them: + // + // - (mDBConn && asyncThread): The expected case where we are either an + // async connection or a sync connection that has been used asynchronously. + // Either way the caller must call us and not Close(). Nothing surprising + // about this. We'll dispatch AsyncCloseConnection to the already-existing + // async thread. + // + // - (mDBConn && !asyncThread): A somewhat unusual case where the caller + // opened the connection synchronously and was planning to use it + // asynchronously, but never got around to using it asynchronously before + // needing to shutdown. This has been observed to happen for the cookie + // service in a case where Firefox shuts itself down almost immediately + // after startup (for unknown reasons). In the Firefox shutdown case, + // we may also fail to create a new async execution thread if one does not + // already exist. (nsThreadManager will refuse to create new threads when + // it has already been told to shutdown.) As such, we need to handle a + // failure to create the async execution thread by falling back to + // synchronous Close() and also dispatching the completion callback because + // at least Places likes to spin a nested event loop that depends on the + // callback being invoked. + // + // Note that we have considered not trying to spin up the async execution + // thread in this case if it does not already exist, but the overhead of + // thread startup (if successful) is significantly less expensive than the + // worst-case potential I/O hit of synchronously closing a database when we + // could close it asynchronously. + // + // - (!mDBConn && asyncThread): This happens in some but not all cases where + // OpenAsyncDatabase encountered a problem opening the database. If it + // happened in all cases AsyncInitDatabase would just shut down the thread + // directly and we would avoid this case. But it doesn't, so for simplicity + // and consistency AsyncCloseConnection knows how to handle this and we + // act like this was the (mDBConn && asyncThread) case in this method. + // + // - (!mDBConn && !asyncThread): The database was never successfully opened or + // Close() or AsyncClose() has already been called (at least) once. This is + // undeniably a misuse case by the caller. We could optimize for this + // case by adding an additional check of mAsyncExecutionThread without using + // getAsyncExecutionTarget() to avoid wastefully creating a thread just to + // shut it down. But this complicates the method for broken caller code + // whereas we're still correct and safe without the special-case. + nsIEventTarget *asyncThread = getAsyncExecutionTarget(); + + // Create our callback event if we were given a callback. This will + // eventually be dispatched in all cases, even if we fall back to Close() and + // the database wasn't open and we return an error. The rationale is that + // no existing consumer checks our return value and several of them like to + // spin nested event loops until the callback fires. Given that, it seems + // preferable for us to dispatch the callback in all cases. (Except the + // wrong thread misuse case we bailed on up above. But that's okay because + // that is statically wrong whereas these edge cases are dynamic.) + nsCOMPtr<nsIRunnable> completeEvent; + if (aCallback) { + completeEvent = newCompletionEvent(aCallback); + } + + if (!asyncThread) { + // We were unable to create an async thread, so we need to fall back to + // using normal Close(). Since there is no async thread, Close() will + // not complain about that. (Close() may, however, complain if the + // connection is closed, but that's okay.) + if (completeEvent) { + // Closing the database is more important than returning an error code + // about a failure to dispatch, especially because all existing native + // callers ignore our return value. + Unused << NS_DispatchToMainThread(completeEvent.forget()); + } + return Close(); + } + + // setClosedState nullifies our connection pointer, so we take a raw pointer + // off it, to pass it through the close procedure. + sqlite3 *nativeConn = mDBConn; + nsresult rv = setClosedState(); + NS_ENSURE_SUCCESS(rv, rv); + + // Create and dispatch our close event to the background thread. + nsCOMPtr<nsIRunnable> closeEvent; + { + // We need to lock because we're modifying mAsyncExecutionThread + MutexAutoLock lockedScope(sharedAsyncExecutionMutex); + closeEvent = new AsyncCloseConnection(this, + nativeConn, + completeEvent, + mAsyncExecutionThread.forget()); + } + + rv = asyncThread->Dispatch(closeEvent, NS_DISPATCH_NORMAL); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +NS_IMETHODIMP +Connection::AsyncClone(bool aReadOnly, + mozIStorageCompletionCallback *aCallback) +{ + PROFILER_LABEL("mozStorageConnection", "AsyncClone", + js::ProfileEntry::Category::STORAGE); + + if (!NS_IsMainThread()) { + return NS_ERROR_NOT_SAME_THREAD; + } + if (!mDBConn) + return NS_ERROR_NOT_INITIALIZED; + if (!mDatabaseFile) + return NS_ERROR_UNEXPECTED; + + int flags = mFlags; + if (aReadOnly) { + // Turn off SQLITE_OPEN_READWRITE, and set SQLITE_OPEN_READONLY. + flags = (~SQLITE_OPEN_READWRITE & flags) | SQLITE_OPEN_READONLY; + // Turn off SQLITE_OPEN_CREATE. + flags = (~SQLITE_OPEN_CREATE & flags); + } + + RefPtr<Connection> clone = new Connection(mStorageService, flags, + mAsyncOnly); + + RefPtr<AsyncInitializeClone> initEvent = + new AsyncInitializeClone(this, clone, aReadOnly, aCallback); + // Dispatch to our async thread, since the originating connection must remain + // valid and open for the whole cloning process. This also ensures we are + // properly serialized with a `close` operation, rather than race with it. + nsCOMPtr<nsIEventTarget> target = getAsyncExecutionTarget(); + if (!target) { + return NS_ERROR_UNEXPECTED; + } + return target->Dispatch(initEvent, NS_DISPATCH_NORMAL); +} + +nsresult +Connection::initializeClone(Connection* aClone, bool aReadOnly) +{ + nsresult rv = mFileURL ? aClone->initialize(mFileURL) + : aClone->initialize(mDatabaseFile); + if (NS_FAILED(rv)) { + return rv; + } + + // Re-attach on-disk databases that were attached to the original connection. + { + nsCOMPtr<mozIStorageStatement> stmt; + rv = CreateStatement(NS_LITERAL_CSTRING("PRAGMA database_list"), + getter_AddRefs(stmt)); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + bool hasResult = false; + while (stmt && NS_SUCCEEDED(stmt->ExecuteStep(&hasResult)) && hasResult) { + nsAutoCString name; + rv = stmt->GetUTF8String(1, name); + if (NS_SUCCEEDED(rv) && !name.Equals(NS_LITERAL_CSTRING("main")) && + !name.Equals(NS_LITERAL_CSTRING("temp"))) { + nsCString path; + rv = stmt->GetUTF8String(2, path); + if (NS_SUCCEEDED(rv) && !path.IsEmpty()) { + rv = aClone->ExecuteSimpleSQL(NS_LITERAL_CSTRING("ATTACH DATABASE '") + + path + NS_LITERAL_CSTRING("' AS ") + name); + MOZ_ASSERT(NS_SUCCEEDED(rv), "couldn't re-attach database to cloned connection"); + } + } + } + } + + // Copy over pragmas from the original connection. + static const char * pragmas[] = { + "cache_size", + "temp_store", + "foreign_keys", + "journal_size_limit", + "synchronous", + "wal_autocheckpoint", + "busy_timeout" + }; + for (uint32_t i = 0; i < ArrayLength(pragmas); ++i) { + // Read-only connections just need cache_size and temp_store pragmas. + if (aReadOnly && ::strcmp(pragmas[i], "cache_size") != 0 && + ::strcmp(pragmas[i], "temp_store") != 0) { + continue; + } + + nsAutoCString pragmaQuery("PRAGMA "); + pragmaQuery.Append(pragmas[i]); + nsCOMPtr<mozIStorageStatement> stmt; + rv = CreateStatement(pragmaQuery, getter_AddRefs(stmt)); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + bool hasResult = false; + if (stmt && NS_SUCCEEDED(stmt->ExecuteStep(&hasResult)) && hasResult) { + pragmaQuery.AppendLiteral(" = "); + pragmaQuery.AppendInt(stmt->AsInt32(0)); + rv = aClone->ExecuteSimpleSQL(pragmaQuery); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + } + } + + // Copy any functions that have been added to this connection. + SQLiteMutexAutoLock lockedScope(sharedDBMutex); + for (auto iter = mFunctions.Iter(); !iter.Done(); iter.Next()) { + const nsACString &key = iter.Key(); + Connection::FunctionInfo data = iter.UserData(); + + MOZ_ASSERT(data.type == Connection::FunctionInfo::SIMPLE || + data.type == Connection::FunctionInfo::AGGREGATE, + "Invalid function type!"); + + if (data.type == Connection::FunctionInfo::SIMPLE) { + mozIStorageFunction *function = + static_cast<mozIStorageFunction *>(data.function.get()); + rv = aClone->CreateFunction(key, data.numArgs, function); + if (NS_FAILED(rv)) { + NS_WARNING("Failed to copy function to cloned connection"); + } + + } else { + mozIStorageAggregateFunction *function = + static_cast<mozIStorageAggregateFunction *>(data.function.get()); + rv = aClone->CreateAggregateFunction(key, data.numArgs, function); + if (NS_FAILED(rv)) { + NS_WARNING("Failed to copy aggregate function to cloned connection"); + } + } + } + + return NS_OK; +} + +NS_IMETHODIMP +Connection::Clone(bool aReadOnly, + mozIStorageConnection **_connection) +{ + MOZ_ASSERT(threadOpenedOn == NS_GetCurrentThread()); + + PROFILER_LABEL("mozStorageConnection", "Clone", + js::ProfileEntry::Category::STORAGE); + + if (!mDBConn) + return NS_ERROR_NOT_INITIALIZED; + if (!mDatabaseFile) + return NS_ERROR_UNEXPECTED; + + int flags = mFlags; + if (aReadOnly) { + // Turn off SQLITE_OPEN_READWRITE, and set SQLITE_OPEN_READONLY. + flags = (~SQLITE_OPEN_READWRITE & flags) | SQLITE_OPEN_READONLY; + // Turn off SQLITE_OPEN_CREATE. + flags = (~SQLITE_OPEN_CREATE & flags); + } + + RefPtr<Connection> clone = new Connection(mStorageService, flags, + mAsyncOnly); + + nsresult rv = initializeClone(clone, aReadOnly); + if (NS_FAILED(rv)) { + return rv; + } + + NS_IF_ADDREF(*_connection = clone); + return NS_OK; +} + +NS_IMETHODIMP +Connection::GetDefaultPageSize(int32_t *_defaultPageSize) +{ + *_defaultPageSize = Service::getDefaultPageSize(); + return NS_OK; +} + +NS_IMETHODIMP +Connection::GetConnectionReady(bool *_ready) +{ + *_ready = connectionReady(); + return NS_OK; +} + +NS_IMETHODIMP +Connection::GetDatabaseFile(nsIFile **_dbFile) +{ + if (!mDBConn) return NS_ERROR_NOT_INITIALIZED; + + NS_IF_ADDREF(*_dbFile = mDatabaseFile); + + return NS_OK; +} + +NS_IMETHODIMP +Connection::GetLastInsertRowID(int64_t *_id) +{ + if (!mDBConn) return NS_ERROR_NOT_INITIALIZED; + + sqlite_int64 id = ::sqlite3_last_insert_rowid(mDBConn); + *_id = id; + + return NS_OK; +} + +NS_IMETHODIMP +Connection::GetAffectedRows(int32_t *_rows) +{ + if (!mDBConn) return NS_ERROR_NOT_INITIALIZED; + + *_rows = ::sqlite3_changes(mDBConn); + + return NS_OK; +} + +NS_IMETHODIMP +Connection::GetLastError(int32_t *_error) +{ + if (!mDBConn) return NS_ERROR_NOT_INITIALIZED; + + *_error = ::sqlite3_errcode(mDBConn); + + return NS_OK; +} + +NS_IMETHODIMP +Connection::GetLastErrorString(nsACString &_errorString) +{ + if (!mDBConn) return NS_ERROR_NOT_INITIALIZED; + + const char *serr = ::sqlite3_errmsg(mDBConn); + _errorString.Assign(serr); + + return NS_OK; +} + +NS_IMETHODIMP +Connection::GetSchemaVersion(int32_t *_version) +{ + if (!mDBConn) return NS_ERROR_NOT_INITIALIZED; + + nsCOMPtr<mozIStorageStatement> stmt; + (void)CreateStatement(NS_LITERAL_CSTRING("PRAGMA user_version"), + getter_AddRefs(stmt)); + NS_ENSURE_TRUE(stmt, NS_ERROR_OUT_OF_MEMORY); + + *_version = 0; + bool hasResult; + if (NS_SUCCEEDED(stmt->ExecuteStep(&hasResult)) && hasResult) + *_version = stmt->AsInt32(0); + + return NS_OK; +} + +NS_IMETHODIMP +Connection::SetSchemaVersion(int32_t aVersion) +{ + if (!mDBConn) return NS_ERROR_NOT_INITIALIZED; + + nsAutoCString stmt(NS_LITERAL_CSTRING("PRAGMA user_version = ")); + stmt.AppendInt(aVersion); + + return ExecuteSimpleSQL(stmt); +} + +NS_IMETHODIMP +Connection::CreateStatement(const nsACString &aSQLStatement, + mozIStorageStatement **_stmt) +{ + NS_ENSURE_ARG_POINTER(_stmt); + if (!mDBConn) return NS_ERROR_NOT_INITIALIZED; + + RefPtr<Statement> statement(new Statement()); + NS_ENSURE_TRUE(statement, NS_ERROR_OUT_OF_MEMORY); + + nsresult rv = statement->initialize(this, mDBConn, aSQLStatement); + NS_ENSURE_SUCCESS(rv, rv); + + Statement *rawPtr; + statement.forget(&rawPtr); + *_stmt = rawPtr; + return NS_OK; +} + +NS_IMETHODIMP +Connection::CreateAsyncStatement(const nsACString &aSQLStatement, + mozIStorageAsyncStatement **_stmt) +{ + NS_ENSURE_ARG_POINTER(_stmt); + if (!mDBConn) return NS_ERROR_NOT_INITIALIZED; + + RefPtr<AsyncStatement> statement(new AsyncStatement()); + NS_ENSURE_TRUE(statement, NS_ERROR_OUT_OF_MEMORY); + + nsresult rv = statement->initialize(this, mDBConn, aSQLStatement); + NS_ENSURE_SUCCESS(rv, rv); + + AsyncStatement *rawPtr; + statement.forget(&rawPtr); + *_stmt = rawPtr; + return NS_OK; +} + +NS_IMETHODIMP +Connection::ExecuteSimpleSQL(const nsACString &aSQLStatement) +{ + CHECK_MAINTHREAD_ABUSE(); + if (!mDBConn) return NS_ERROR_NOT_INITIALIZED; + + int srv = executeSql(mDBConn, PromiseFlatCString(aSQLStatement).get()); + return convertResultCode(srv); +} + +NS_IMETHODIMP +Connection::ExecuteAsync(mozIStorageBaseStatement **aStatements, + uint32_t aNumStatements, + mozIStorageStatementCallback *aCallback, + mozIStoragePendingStatement **_handle) +{ + nsTArray<StatementData> stmts(aNumStatements); + for (uint32_t i = 0; i < aNumStatements; i++) { + nsCOMPtr<StorageBaseStatementInternal> stmt = + do_QueryInterface(aStatements[i]); + + // Obtain our StatementData. + StatementData data; + nsresult rv = stmt->getAsynchronousStatementData(data); + NS_ENSURE_SUCCESS(rv, rv); + + NS_ASSERTION(stmt->getOwner() == this, + "Statement must be from this database connection!"); + + // Now append it to our array. + NS_ENSURE_TRUE(stmts.AppendElement(data), NS_ERROR_OUT_OF_MEMORY); + } + + // Dispatch to the background + return AsyncExecuteStatements::execute(stmts, this, mDBConn, aCallback, + _handle); +} + +NS_IMETHODIMP +Connection::ExecuteSimpleSQLAsync(const nsACString &aSQLStatement, + mozIStorageStatementCallback *aCallback, + mozIStoragePendingStatement **_handle) +{ + if (!NS_IsMainThread()) { + return NS_ERROR_NOT_SAME_THREAD; + } + + nsCOMPtr<mozIStorageAsyncStatement> stmt; + nsresult rv = CreateAsyncStatement(aSQLStatement, getter_AddRefs(stmt)); + if (NS_FAILED(rv)) { + return rv; + } + + nsCOMPtr<mozIStoragePendingStatement> pendingStatement; + rv = stmt->ExecuteAsync(aCallback, getter_AddRefs(pendingStatement)); + if (NS_FAILED(rv)) { + return rv; + } + + pendingStatement.forget(_handle); + return rv; +} + +NS_IMETHODIMP +Connection::TableExists(const nsACString &aTableName, + bool *_exists) +{ + return databaseElementExists(TABLE, aTableName, _exists); +} + +NS_IMETHODIMP +Connection::IndexExists(const nsACString &aIndexName, + bool* _exists) +{ + return databaseElementExists(INDEX, aIndexName, _exists); +} + +NS_IMETHODIMP +Connection::GetTransactionInProgress(bool *_inProgress) +{ + if (!mDBConn) return NS_ERROR_NOT_INITIALIZED; + + SQLiteMutexAutoLock lockedScope(sharedDBMutex); + *_inProgress = mTransactionInProgress; + return NS_OK; +} + +NS_IMETHODIMP +Connection::BeginTransaction() +{ + return BeginTransactionAs(mozIStorageConnection::TRANSACTION_DEFERRED); +} + +NS_IMETHODIMP +Connection::BeginTransactionAs(int32_t aTransactionType) +{ + if (!mDBConn) return NS_ERROR_NOT_INITIALIZED; + + return beginTransactionInternal(mDBConn, aTransactionType); +} + +nsresult +Connection::beginTransactionInternal(sqlite3 *aNativeConnection, + int32_t aTransactionType) +{ + SQLiteMutexAutoLock lockedScope(sharedDBMutex); + if (mTransactionInProgress) + return NS_ERROR_FAILURE; + nsresult rv; + switch(aTransactionType) { + case TRANSACTION_DEFERRED: + rv = convertResultCode(executeSql(aNativeConnection, "BEGIN DEFERRED")); + break; + case TRANSACTION_IMMEDIATE: + rv = convertResultCode(executeSql(aNativeConnection, "BEGIN IMMEDIATE")); + break; + case TRANSACTION_EXCLUSIVE: + rv = convertResultCode(executeSql(aNativeConnection, "BEGIN EXCLUSIVE")); + break; + default: + return NS_ERROR_ILLEGAL_VALUE; + } + if (NS_SUCCEEDED(rv)) + mTransactionInProgress = true; + return rv; +} + +NS_IMETHODIMP +Connection::CommitTransaction() +{ + if (!mDBConn) + return NS_ERROR_NOT_INITIALIZED; + + return commitTransactionInternal(mDBConn); +} + +nsresult +Connection::commitTransactionInternal(sqlite3 *aNativeConnection) +{ + SQLiteMutexAutoLock lockedScope(sharedDBMutex); + if (!mTransactionInProgress) + return NS_ERROR_UNEXPECTED; + nsresult rv = + convertResultCode(executeSql(aNativeConnection, "COMMIT TRANSACTION")); + if (NS_SUCCEEDED(rv)) + mTransactionInProgress = false; + return rv; +} + +NS_IMETHODIMP +Connection::RollbackTransaction() +{ + if (!mDBConn) + return NS_ERROR_NOT_INITIALIZED; + + return rollbackTransactionInternal(mDBConn); +} + +nsresult +Connection::rollbackTransactionInternal(sqlite3 *aNativeConnection) +{ + SQLiteMutexAutoLock lockedScope(sharedDBMutex); + if (!mTransactionInProgress) + return NS_ERROR_UNEXPECTED; + + nsresult rv = + convertResultCode(executeSql(aNativeConnection, "ROLLBACK TRANSACTION")); + if (NS_SUCCEEDED(rv)) + mTransactionInProgress = false; + return rv; +} + +NS_IMETHODIMP +Connection::CreateTable(const char *aTableName, + const char *aTableSchema) +{ + if (!mDBConn) return NS_ERROR_NOT_INITIALIZED; + + char *buf = ::PR_smprintf("CREATE TABLE %s (%s)", aTableName, aTableSchema); + if (!buf) + return NS_ERROR_OUT_OF_MEMORY; + + int srv = executeSql(mDBConn, buf); + ::PR_smprintf_free(buf); + + return convertResultCode(srv); +} + +NS_IMETHODIMP +Connection::CreateFunction(const nsACString &aFunctionName, + int32_t aNumArguments, + mozIStorageFunction *aFunction) +{ + if (!mDBConn) return NS_ERROR_NOT_INITIALIZED; + + // Check to see if this function is already defined. We only check the name + // because a function can be defined with the same body but different names. + SQLiteMutexAutoLock lockedScope(sharedDBMutex); + NS_ENSURE_FALSE(mFunctions.Get(aFunctionName, nullptr), NS_ERROR_FAILURE); + + int srv = ::sqlite3_create_function(mDBConn, + nsPromiseFlatCString(aFunctionName).get(), + aNumArguments, + SQLITE_ANY, + aFunction, + basicFunctionHelper, + nullptr, + nullptr); + if (srv != SQLITE_OK) + return convertResultCode(srv); + + FunctionInfo info = { aFunction, + Connection::FunctionInfo::SIMPLE, + aNumArguments }; + mFunctions.Put(aFunctionName, info); + + return NS_OK; +} + +NS_IMETHODIMP +Connection::CreateAggregateFunction(const nsACString &aFunctionName, + int32_t aNumArguments, + mozIStorageAggregateFunction *aFunction) +{ + if (!mDBConn) return NS_ERROR_NOT_INITIALIZED; + + // Check to see if this function name is already defined. + SQLiteMutexAutoLock lockedScope(sharedDBMutex); + NS_ENSURE_FALSE(mFunctions.Get(aFunctionName, nullptr), NS_ERROR_FAILURE); + + // Because aggregate functions depend on state across calls, you cannot have + // the same instance use the same name. We want to enumerate all functions + // and make sure this instance is not already registered. + NS_ENSURE_FALSE(findFunctionByInstance(aFunction), NS_ERROR_FAILURE); + + int srv = ::sqlite3_create_function(mDBConn, + nsPromiseFlatCString(aFunctionName).get(), + aNumArguments, + SQLITE_ANY, + aFunction, + nullptr, + aggregateFunctionStepHelper, + aggregateFunctionFinalHelper); + if (srv != SQLITE_OK) + return convertResultCode(srv); + + FunctionInfo info = { aFunction, + Connection::FunctionInfo::AGGREGATE, + aNumArguments }; + mFunctions.Put(aFunctionName, info); + + return NS_OK; +} + +NS_IMETHODIMP +Connection::RemoveFunction(const nsACString &aFunctionName) +{ + if (!mDBConn) return NS_ERROR_NOT_INITIALIZED; + + SQLiteMutexAutoLock lockedScope(sharedDBMutex); + NS_ENSURE_TRUE(mFunctions.Get(aFunctionName, nullptr), NS_ERROR_FAILURE); + + int srv = ::sqlite3_create_function(mDBConn, + nsPromiseFlatCString(aFunctionName).get(), + 0, + SQLITE_ANY, + nullptr, + nullptr, + nullptr, + nullptr); + if (srv != SQLITE_OK) + return convertResultCode(srv); + + mFunctions.Remove(aFunctionName); + + return NS_OK; +} + +NS_IMETHODIMP +Connection::SetProgressHandler(int32_t aGranularity, + mozIStorageProgressHandler *aHandler, + mozIStorageProgressHandler **_oldHandler) +{ + if (!mDBConn) return NS_ERROR_NOT_INITIALIZED; + + // Return previous one + SQLiteMutexAutoLock lockedScope(sharedDBMutex); + NS_IF_ADDREF(*_oldHandler = mProgressHandler); + + if (!aHandler || aGranularity <= 0) { + aHandler = nullptr; + aGranularity = 0; + } + mProgressHandler = aHandler; + ::sqlite3_progress_handler(mDBConn, aGranularity, sProgressHelper, this); + + return NS_OK; +} + +NS_IMETHODIMP +Connection::RemoveProgressHandler(mozIStorageProgressHandler **_oldHandler) +{ + if (!mDBConn) return NS_ERROR_NOT_INITIALIZED; + + // Return previous one + SQLiteMutexAutoLock lockedScope(sharedDBMutex); + NS_IF_ADDREF(*_oldHandler = mProgressHandler); + + mProgressHandler = nullptr; + ::sqlite3_progress_handler(mDBConn, 0, nullptr, nullptr); + + return NS_OK; +} + +NS_IMETHODIMP +Connection::SetGrowthIncrement(int32_t aChunkSize, const nsACString &aDatabaseName) +{ + // Bug 597215: Disk space is extremely limited on Android + // so don't preallocate space. This is also not effective + // on log structured file systems used by Android devices +#if !defined(ANDROID) && !defined(MOZ_PLATFORM_MAEMO) + // Don't preallocate if less than 500MiB is available. + int64_t bytesAvailable; + nsresult rv = mDatabaseFile->GetDiskSpaceAvailable(&bytesAvailable); + NS_ENSURE_SUCCESS(rv, rv); + if (bytesAvailable < MIN_AVAILABLE_BYTES_PER_CHUNKED_GROWTH) { + return NS_ERROR_FILE_TOO_BIG; + } + + (void)::sqlite3_file_control(mDBConn, + aDatabaseName.Length() ? nsPromiseFlatCString(aDatabaseName).get() + : nullptr, + SQLITE_FCNTL_CHUNK_SIZE, + &aChunkSize); +#endif + return NS_OK; +} + +NS_IMETHODIMP +Connection::EnableModule(const nsACString& aModuleName) +{ + if (!mDBConn) return NS_ERROR_NOT_INITIALIZED; + + for (size_t i = 0; i < ArrayLength(gModules); i++) { + struct Module* m = &gModules[i]; + if (aModuleName.Equals(m->name)) { + int srv = m->registerFunc(mDBConn, m->name); + if (srv != SQLITE_OK) + return convertResultCode(srv); + + return NS_OK; + } + } + + return NS_ERROR_FAILURE; +} + +// Implemented in TelemetryVFS.cpp +already_AddRefed<QuotaObject> +GetQuotaObjectForFile(sqlite3_file *pFile); + +NS_IMETHODIMP +Connection::GetQuotaObjects(QuotaObject** aDatabaseQuotaObject, + QuotaObject** aJournalQuotaObject) +{ + MOZ_ASSERT(aDatabaseQuotaObject); + MOZ_ASSERT(aJournalQuotaObject); + + if (!mDBConn) { + return NS_ERROR_NOT_INITIALIZED; + } + + sqlite3_file* file; + int srv = ::sqlite3_file_control(mDBConn, + nullptr, + SQLITE_FCNTL_FILE_POINTER, + &file); + if (srv != SQLITE_OK) { + return convertResultCode(srv); + } + + RefPtr<QuotaObject> databaseQuotaObject = GetQuotaObjectForFile(file); + + srv = ::sqlite3_file_control(mDBConn, + nullptr, + SQLITE_FCNTL_JOURNAL_POINTER, + &file); + if (srv != SQLITE_OK) { + return convertResultCode(srv); + } + + RefPtr<QuotaObject> journalQuotaObject = GetQuotaObjectForFile(file); + + databaseQuotaObject.forget(aDatabaseQuotaObject); + journalQuotaObject.forget(aJournalQuotaObject); + return NS_OK; +} + +} // namespace storage +} // namespace mozilla diff --git a/storage/mozStorageConnection.h b/storage/mozStorageConnection.h new file mode 100644 index 000000000..979ac6436 --- /dev/null +++ b/storage/mozStorageConnection.h @@ -0,0 +1,439 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_storage_Connection_h +#define mozilla_storage_Connection_h + +#include "nsAutoPtr.h" +#include "nsCOMPtr.h" +#include "mozilla/Mutex.h" +#include "nsProxyRelease.h" +#include "nsThreadUtils.h" +#include "nsIInterfaceRequestor.h" + +#include "nsDataHashtable.h" +#include "mozIStorageProgressHandler.h" +#include "SQLiteMutex.h" +#include "mozIStorageConnection.h" +#include "mozStorageService.h" +#include "mozIStorageAsyncConnection.h" +#include "mozIStorageCompletionCallback.h" + +#include "nsIMutableArray.h" +#include "mozilla/Attributes.h" + +#include "sqlite3.h" + +class nsIFile; +class nsIFileURL; +class nsIEventTarget; +class nsIThread; + +namespace mozilla { +namespace storage { + +class Connection final : public mozIStorageConnection + , public nsIInterfaceRequestor +{ +public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_MOZISTORAGEASYNCCONNECTION + NS_DECL_MOZISTORAGECONNECTION + NS_DECL_NSIINTERFACEREQUESTOR + + /** + * Structure used to describe user functions on the database connection. + */ + struct FunctionInfo { + enum FunctionType { + SIMPLE, + AGGREGATE + }; + + nsCOMPtr<nsISupports> function; + FunctionType type; + int32_t numArgs; + }; + + /** + * @param aService + * Pointer to the storage service. Held onto for the lifetime of the + * connection. + * @param aFlags + * The flags to pass to sqlite3_open_v2. + * @param aAsyncOnly + * If |true|, the Connection only implements asynchronous interface: + * - |mozIStorageAsyncConnection|; + * If |false|, the result also implements synchronous interface: + * - |mozIStorageConnection|. + * @param aIgnoreLockingMode + * If |true|, ignore locks in force on the file. Only usable with + * read-only connections. Defaults to false. + * Use with extreme caution. If sqlite ignores locks, reads may fail + * indicating database corruption (the database won't actually be + * corrupt) or produce wrong results without any indication that has + * happened. + */ + Connection(Service *aService, int aFlags, bool aAsyncOnly, + bool aIgnoreLockingMode = false); + + /** + * Creates the connection to an in-memory database. + */ + nsresult initialize(); + + /** + * Creates the connection to the database. + * + * @param aDatabaseFile + * The nsIFile of the location of the database to open, or create if it + * does not exist. + */ + nsresult initialize(nsIFile *aDatabaseFile); + + /** + * Creates the connection to the database. + * + * @param aFileURL + * The nsIFileURL of the location of the database to open, or create if it + * does not exist. + */ + nsresult initialize(nsIFileURL *aFileURL); + + /** + * Fetches runtime status information for this connection. + * + * @param aStatusOption One of the SQLITE_DBSTATUS options defined at + * http://www.sqlite.org/c3ref/c_dbstatus_options.html + * @param [optional] aMaxValue if provided, will be set to the highest + * istantaneous value. + * @return the current value for the specified option. + */ + int32_t getSqliteRuntimeStatus(int32_t aStatusOption, + int32_t* aMaxValue=nullptr); + /** + * Registers/unregisters a commit hook callback. + * + * @param aCallbackFn a callback function to be invoked on transactions + * commit. Pass nullptr to unregister the current callback. + * @param [optional] aData if provided, will be passed to the callback. + * @see http://sqlite.org/c3ref/commit_hook.html + */ + void setCommitHook(int (*aCallbackFn)(void *) , void *aData=nullptr) { + MOZ_ASSERT(mDBConn, "A connection must exist at this point"); + ::sqlite3_commit_hook(mDBConn, aCallbackFn, aData); + }; + + /** + * Gets autocommit status. + */ + bool getAutocommit() { + return mDBConn && static_cast<bool>(::sqlite3_get_autocommit(mDBConn)); + }; + + /** + * Lazily creates and returns a background execution thread. In the future, + * the thread may be re-claimed if left idle, so you should call this + * method just before you dispatch and not save the reference. + * + * @returns an event target suitable for asynchronous statement execution. + */ + nsIEventTarget *getAsyncExecutionTarget(); + + /** + * Mutex used by asynchronous statements to protect state. The mutex is + * declared on the connection object because there is no contention between + * asynchronous statements (they are serialized on mAsyncExecutionThread). + * Currently protects: + * - Connection.mAsyncExecutionThreadShuttingDown + * - Connection.mAsyncExecutionThread + * - Connection.mConnectionClosed + * - AsyncExecuteStatements.mCancelRequested + */ + Mutex sharedAsyncExecutionMutex; + + /** + * Wraps the mutex that SQLite gives us from sqlite3_db_mutex. This is public + * because we already expose the sqlite3* native connection and proper + * operation of the deadlock detector requires everyone to use the same single + * SQLiteMutex instance for correctness. + */ + SQLiteMutex sharedDBMutex; + + /** + * References the thread this database was opened on. This MUST be thread it is + * closed on. + */ + const nsCOMPtr<nsIThread> threadOpenedOn; + + /** + * Closes the SQLite database, and warns about any non-finalized statements. + */ + nsresult internalClose(sqlite3 *aDBConn); + + /** + * Shuts down the passed-in async thread. + */ + void shutdownAsyncThread(nsIThread *aAsyncThread); + + /** + * Obtains the filename of the connection. Useful for logging. + */ + nsCString getFilename(); + + /** + * Creates an sqlite3 prepared statement object from an SQL string. + * + * @param aNativeConnection + * The underlying Sqlite connection to prepare the statement with. + * @param aSQL + * The SQL statement string to compile. + * @param _stmt + * New sqlite3_stmt object. + * @return the result from sqlite3_prepare_v2. + */ + int prepareStatement(sqlite3* aNativeConnection, + const nsCString &aSQL, sqlite3_stmt **_stmt); + + /** + * Performs a sqlite3_step on aStatement, while properly handling SQLITE_LOCKED + * when not on the main thread by waiting until we are notified. + * + * @param aNativeConnection + * The underlying Sqlite connection to step the statement with. + * @param aStatement + * A pointer to a sqlite3_stmt object. + * @return the result from sqlite3_step. + */ + int stepStatement(sqlite3* aNativeConnection, sqlite3_stmt* aStatement); + + /** + * Raw connection transaction management. + * + * @see BeginTransactionAs, CommitTransaction, RollbackTransaction. + */ + nsresult beginTransactionInternal(sqlite3 *aNativeConnection, + int32_t aTransactionType=TRANSACTION_DEFERRED); + nsresult commitTransactionInternal(sqlite3 *aNativeConnection); + nsresult rollbackTransactionInternal(sqlite3 *aNativeConnection); + + bool connectionReady(); + + /** + * True if this connection is shutting down but not yet closed. + */ + bool isClosing(); + + /** + * True if the underlying connection is closed. + * Any sqlite resources may be lost when this returns true, so nothing should + * try to use them. + */ + bool isClosed(); + + nsresult initializeClone(Connection *aClone, bool aReadOnly); + +private: + ~Connection(); + nsresult initializeInternal(); + + /** + * Sets the database into a closed state so no further actions can be + * performed. + * + * @note mDBConn is set to nullptr in this method. + */ + nsresult setClosedState(); + + /** + * Helper for calls to sqlite3_exec. Reports long delays to Telemetry. + * + * @param aNativeConnection + * The underlying Sqlite connection to execute the query with. + * @param aSqlString + * SQL string to execute + * @return the result from sqlite3_exec. + */ + int executeSql(sqlite3 *aNativeConnection, const char *aSqlString); + + /** + * Describes a certain primitive type in the database. + * + * Possible Values Are: + * INDEX - To check for the existence of an index + * TABLE - To check for the existence of a table + */ + enum DatabaseElementType { + INDEX, + TABLE + }; + + /** + * Determines if the specified primitive exists. + * + * @param aElementType + * The type of element to check the existence of + * @param aElementName + * The name of the element to check for + * @returns true if element exists, false otherwise + */ + nsresult databaseElementExists(enum DatabaseElementType aElementType, + const nsACString& aElementName, + bool *_exists); + + bool findFunctionByInstance(nsISupports *aInstance); + + static int sProgressHelper(void *aArg); + // Generic progress handler + // Dispatch call to registered progress handler, + // if there is one. Do nothing in other cases. + int progressHandler(); + + sqlite3 *mDBConn; + nsCOMPtr<nsIFileURL> mFileURL; + nsCOMPtr<nsIFile> mDatabaseFile; + + /** + * The filename that will be reported to telemetry for this connection. By + * default this will be the leaf of the path to the database file. + */ + nsCString mTelemetryFilename; + + /** + * Lazily created thread for asynchronous statement execution. Consumers + * should use getAsyncExecutionTarget rather than directly accessing this + * field. + */ + nsCOMPtr<nsIThread> mAsyncExecutionThread; + + /** + * Set to true by Close() or AsyncClose() prior to shutdown. + * + * If false, we guarantee both that the underlying sqlite3 database + * connection is still open and that getAsyncExecutionTarget() can + * return a thread. Once true, either the sqlite3 database + * connection is being shutdown or it has been + * shutdown. Additionally, once true, getAsyncExecutionTarget() + * returns null. + * + * This variable should be accessed while holding the + * sharedAsyncExecutionMutex. + */ + bool mAsyncExecutionThreadShuttingDown; + + /** + * Tracks whether the async thread has been initialized and Shutdown() has + * not yet been invoked on it. + */ +#ifdef DEBUG + bool mAsyncExecutionThreadIsAlive; +#endif + + /** + * Set to true just prior to calling sqlite3_close on the + * connection. + * + * This variable should be accessed while holding the + * sharedAsyncExecutionMutex. + */ + bool mConnectionClosed; + + /** + * Tracks if we have a transaction in progress or not. Access protected by + * sharedDBMutex. + */ + bool mTransactionInProgress; + + /** + * Stores the mapping of a given function by name to its instance. Access is + * protected by sharedDBMutex. + */ + nsDataHashtable<nsCStringHashKey, FunctionInfo> mFunctions; + + /** + * Stores the registered progress handler for the database connection. Access + * is protected by sharedDBMutex. + */ + nsCOMPtr<mozIStorageProgressHandler> mProgressHandler; + + /** + * Stores the flags we passed to sqlite3_open_v2. + */ + const int mFlags; + + /** + * Stores whether we should ask sqlite3_open_v2 to ignore locking. + */ + const bool mIgnoreLockingMode; + + // This is here for two reasons: 1) It's used to make sure that the + // connections do not outlive the service. 2) Our custom collating functions + // call its localeCompareStrings() method. + RefPtr<Service> mStorageService; + + /** + * If |false|, this instance supports synchronous operations + * and it can be cast to |mozIStorageConnection|. + */ + const bool mAsyncOnly; +}; + + +/** + * A Runnable designed to call a mozIStorageCompletionCallback on + * the appropriate thread. + */ +class CallbackComplete final : public Runnable +{ +public: + /** + * @param aValue The result to pass to the callback. It must + * already be owned by the main thread. + * @param aCallback The callback. It must already be owned by the + * main thread. + */ + CallbackComplete(nsresult aStatus, + nsISupports* aValue, + already_AddRefed<mozIStorageCompletionCallback> aCallback) + : mStatus(aStatus) + , mValue(aValue) + , mCallback(aCallback) + { + } + + NS_IMETHOD Run() override { + MOZ_ASSERT(NS_IsMainThread()); + nsresult rv = mCallback->Complete(mStatus, mValue); + + // Ensure that we release on the main thread + mValue = nullptr; + mCallback = nullptr; + return rv; + } + +private: + nsresult mStatus; + nsCOMPtr<nsISupports> mValue; + // This is a RefPtr<T> and not a nsCOMPtr<T> because + // nsCOMP<T> would cause an off-main thread QI, which + // is not a good idea (and crashes XPConnect). + RefPtr<mozIStorageCompletionCallback> mCallback; +}; + +} // namespace storage +} // namespace mozilla + +/** + * Casting Connection to nsISupports is ambiguous. + * This method handles that. + */ +inline nsISupports* +ToSupports(mozilla::storage::Connection* p) +{ + return NS_ISUPPORTS_CAST(mozIStorageAsyncConnection*, p); +} + +#endif // mozilla_storage_Connection_h diff --git a/storage/mozStorageError.cpp b/storage/mozStorageError.cpp new file mode 100644 index 000000000..1ddf25314 --- /dev/null +++ b/storage/mozStorageError.cpp @@ -0,0 +1,49 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozStorageError.h" + +namespace mozilla { +namespace storage { + +//////////////////////////////////////////////////////////////////////////////// +//// Error + +Error::Error(int aResult, + const char *aMessage) +: mResult(aResult) +, mMessage(aMessage) +{ +} + +/** + * Note: This object is only ever accessed on one thread at a time. It it not + * threadsafe, but it does need threadsafe AddRef and Release. + */ +NS_IMPL_ISUPPORTS( + Error, + mozIStorageError +) + +//////////////////////////////////////////////////////////////////////////////// +//// mozIStorageError + +NS_IMETHODIMP +Error::GetResult(int32_t *_result) +{ + *_result = mResult; + return NS_OK; +} + +NS_IMETHODIMP +Error::GetMessage(nsACString &_message) +{ + _message = mMessage; + return NS_OK; +} + +} // namespace storage +} // namespace mozilla diff --git a/storage/mozStorageError.h b/storage/mozStorageError.h new file mode 100644 index 000000000..07963cf13 --- /dev/null +++ b/storage/mozStorageError.h @@ -0,0 +1,35 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozStorageError_h +#define mozStorageError_h + +#include "mozIStorageError.h" +#include "nsString.h" +#include "mozilla/Attributes.h" + +namespace mozilla { +namespace storage { + +class Error final : public mozIStorageError +{ +public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_MOZISTORAGEERROR + + Error(int aResult, const char *aMessage); + +private: + ~Error() {} + + int mResult; + nsCString mMessage; +}; + +} // namespace storage +} // namespace mozilla + +#endif // mozStorageError_h diff --git a/storage/mozStorageHelper.h b/storage/mozStorageHelper.h new file mode 100644 index 000000000..1b4fde799 --- /dev/null +++ b/storage/mozStorageHelper.h @@ -0,0 +1,209 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef MOZSTORAGEHELPER_H +#define MOZSTORAGEHELPER_H + +#include "nsAutoPtr.h" +#include "nsStringGlue.h" +#include "mozilla/DebugOnly.h" + +#include "mozIStorageAsyncConnection.h" +#include "mozIStorageConnection.h" +#include "mozIStorageStatement.h" +#include "mozIStoragePendingStatement.h" +#include "nsError.h" + +/** + * This class wraps a transaction inside a given C++ scope, guaranteeing that + * the transaction will be completed even if you have an exception or + * return early. + * + * A common use is to create an instance with aCommitOnComplete = false (rollback), + * then call Commit() on this object manually when your function completes + * successfully. + * + * @note nested transactions are not supported by Sqlite, so if a transaction + * is already in progress, this object does nothing. Note that in this case, + * you may not get the transaction type you asked for, and you won't be able + * to rollback. + * + * @param aConnection + * The connection to create the transaction on. + * @param aCommitOnComplete + * Controls whether the transaction is committed or rolled back when + * this object goes out of scope. + * @param aType [optional] + * The transaction type, as defined in mozIStorageConnection. Defaults + * to TRANSACTION_DEFERRED. + * @param aAsyncCommit [optional] + * Whether commit should be executed asynchronously on the helper thread. + * This is a special option introduced as an interim solution to reduce + * main-thread fsyncs in Places. Can only be used on main-thread. + * + * WARNING: YOU SHOULD _NOT_ WRITE NEW MAIN-THREAD CODE USING THIS! + * + * Notice that async commit might cause synchronous statements to fail + * with SQLITE_BUSY. A possible mitigation strategy is to use + * PRAGMA busy_timeout, but notice that might cause main-thread jank. + * Finally, if the database is using WAL journaling mode, other + * connections won't see the changes done in async committed transactions + * until commit is complete. + * + * For all of the above reasons, this should only be used as an interim + * solution and avoided completely if possible. + */ +class mozStorageTransaction +{ +public: + mozStorageTransaction(mozIStorageConnection* aConnection, + bool aCommitOnComplete, + int32_t aType = mozIStorageConnection::TRANSACTION_DEFERRED, + bool aAsyncCommit = false) + : mConnection(aConnection), + mHasTransaction(false), + mCommitOnComplete(aCommitOnComplete), + mCompleted(false), + mAsyncCommit(aAsyncCommit) + { + if (mConnection) { + nsAutoCString query("BEGIN"); + switch(aType) { + case mozIStorageConnection::TRANSACTION_IMMEDIATE: + query.AppendLiteral(" IMMEDIATE"); + break; + case mozIStorageConnection::TRANSACTION_EXCLUSIVE: + query.AppendLiteral(" EXCLUSIVE"); + break; + case mozIStorageConnection::TRANSACTION_DEFERRED: + query.AppendLiteral(" DEFERRED"); + break; + default: + MOZ_ASSERT(false, "Unknown transaction type"); + } + // If a transaction is already in progress, this will fail, since Sqlite + // doesn't support nested transactions. + mHasTransaction = NS_SUCCEEDED(mConnection->ExecuteSimpleSQL(query)); + } + } + + ~mozStorageTransaction() + { + if (mConnection && mHasTransaction && !mCompleted) { + if (mCommitOnComplete) { + mozilla::DebugOnly<nsresult> rv = Commit(); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "A transaction didn't commit correctly"); + } + else { + mozilla::DebugOnly<nsresult> rv = Rollback(); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "A transaction didn't rollback correctly"); + } + } + } + + /** + * Commits the transaction if one is in progress. If one is not in progress, + * this is a NOP since the actual owner of the transaction outside of our + * scope is in charge of finally committing or rolling back the transaction. + */ + nsresult Commit() + { + if (!mConnection || mCompleted || !mHasTransaction) + return NS_OK; + mCompleted = true; + + // TODO (bug 559659): this might fail with SQLITE_BUSY, but we don't handle + // it, thus the transaction might stay open until the next COMMIT. + nsresult rv; + if (mAsyncCommit) { + nsCOMPtr<mozIStoragePendingStatement> ps; + rv = mConnection->ExecuteSimpleSQLAsync(NS_LITERAL_CSTRING("COMMIT"), + nullptr, getter_AddRefs(ps)); + } + else { + rv = mConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING("COMMIT")); + } + + if (NS_SUCCEEDED(rv)) + mHasTransaction = false; + + return rv; + } + + /** + * Rolls back the transaction if one is in progress. If one is not in progress, + * this is a NOP since the actual owner of the transaction outside of our + * scope is in charge of finally rolling back the transaction. + */ + nsresult Rollback() + { + if (!mConnection || mCompleted || !mHasTransaction) + return NS_OK; + mCompleted = true; + + // TODO (bug 1062823): from Sqlite 3.7.11 on, rollback won't ever return + // a busy error, so this handling can be removed. + nsresult rv = NS_OK; + do { + rv = mConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING("ROLLBACK")); + if (rv == NS_ERROR_STORAGE_BUSY) + (void)PR_Sleep(PR_INTERVAL_NO_WAIT); + } while (rv == NS_ERROR_STORAGE_BUSY); + + if (NS_SUCCEEDED(rv)) + mHasTransaction = false; + + return rv; + } + +protected: + nsCOMPtr<mozIStorageConnection> mConnection; + bool mHasTransaction; + bool mCommitOnComplete; + bool mCompleted; + bool mAsyncCommit; +}; + +/** + * This class wraps a statement so that it is guaraneed to be reset when + * this object goes out of scope. + * + * Note that this always just resets the statement. If the statement doesn't + * need resetting, the reset operation is inexpensive. + */ +class MOZ_STACK_CLASS mozStorageStatementScoper +{ +public: + explicit mozStorageStatementScoper(mozIStorageStatement* aStatement) + : mStatement(aStatement) + { + } + ~mozStorageStatementScoper() + { + if (mStatement) + mStatement->Reset(); + } + + /** + * Call this to make the statement not reset. You might do this if you know + * that the statement has been reset. + */ + void Abandon() + { + mStatement = nullptr; + } + +protected: + nsCOMPtr<mozIStorageStatement> mStatement; +}; + +// Use this to make queries uniquely identifiable in telemetry +// statistics, especially PRAGMAs. We don't include __LINE__ so that +// queries are stable in the face of source code changes. +#define MOZ_STORAGE_UNIQUIFY_QUERY_STR "/* " __FILE__ " */ " + +#endif /* MOZSTORAGEHELPER_H */ diff --git a/storage/mozStoragePrivateHelpers.cpp b/storage/mozStoragePrivateHelpers.cpp new file mode 100644 index 000000000..91924204f --- /dev/null +++ b/storage/mozStoragePrivateHelpers.cpp @@ -0,0 +1,277 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "sqlite3.h" + +#include "jsfriendapi.h" + +#include "nsPrintfCString.h" +#include "nsString.h" +#include "nsError.h" +#include "mozilla/Mutex.h" +#include "mozilla/CondVar.h" +#include "nsQueryObject.h" +#include "nsThreadUtils.h" +#include "nsJSUtils.h" + +#include "Variant.h" +#include "mozStoragePrivateHelpers.h" +#include "mozIStorageStatement.h" +#include "mozIStorageCompletionCallback.h" +#include "mozIStorageBindingParams.h" + +#include "mozilla/Logging.h" +extern mozilla::LazyLogModule gStorageLog; + +namespace mozilla { +namespace storage { + +nsresult +convertResultCode(int aSQLiteResultCode) +{ + // Drop off the extended result bits of the result code. + int rc = aSQLiteResultCode & 0xFF; + + switch (rc) { + case SQLITE_OK: + case SQLITE_ROW: + case SQLITE_DONE: + return NS_OK; + case SQLITE_CORRUPT: + case SQLITE_NOTADB: + return NS_ERROR_FILE_CORRUPTED; + case SQLITE_PERM: + case SQLITE_CANTOPEN: + return NS_ERROR_FILE_ACCESS_DENIED; + case SQLITE_BUSY: + return NS_ERROR_STORAGE_BUSY; + case SQLITE_LOCKED: + return NS_ERROR_FILE_IS_LOCKED; + case SQLITE_READONLY: + return NS_ERROR_FILE_READ_ONLY; + case SQLITE_IOERR: + return NS_ERROR_STORAGE_IOERR; + case SQLITE_FULL: + case SQLITE_TOOBIG: + return NS_ERROR_FILE_NO_DEVICE_SPACE; + case SQLITE_NOMEM: + return NS_ERROR_OUT_OF_MEMORY; + case SQLITE_MISUSE: + return NS_ERROR_UNEXPECTED; + case SQLITE_ABORT: + case SQLITE_INTERRUPT: + return NS_ERROR_ABORT; + case SQLITE_CONSTRAINT: + return NS_ERROR_STORAGE_CONSTRAINT; + } + + // generic error +#ifdef DEBUG + nsAutoCString message; + message.AppendLiteral("SQLite returned error code "); + message.AppendInt(rc); + message.AppendLiteral(" , Storage will convert it to NS_ERROR_FAILURE"); + NS_WARNING_ASSERTION(rc == SQLITE_ERROR, message.get()); +#endif + return NS_ERROR_FAILURE; +} + +void +checkAndLogStatementPerformance(sqlite3_stmt *aStatement) +{ + // Check to see if the query performed sorting operations or not. If it + // did, it may need to be optimized! + int count = ::sqlite3_stmt_status(aStatement, SQLITE_STMTSTATUS_SORT, 1); + if (count <= 0) + return; + + const char *sql = ::sqlite3_sql(aStatement); + + // Check to see if this is marked to not warn + if (::strstr(sql, "/* do not warn (bug ")) + return; + + // CREATE INDEX always sorts (sorting is a necessary step in creating + // an index). So ignore the warning there. + if (::strstr(sql, "CREATE INDEX") || ::strstr(sql, "CREATE UNIQUE INDEX")) + return; + + nsAutoCString message("Suboptimal indexes for the SQL statement "); +#ifdef MOZ_STORAGE_SORTWARNING_SQL_DUMP + message.Append('`'); + message.Append(sql); + message.AppendLiteral("` ["); + message.AppendInt(count); + message.AppendLiteral(" sort operation(s)]"); +#else + nsPrintfCString address("0x%p", aStatement); + message.Append(address); +#endif + message.AppendLiteral(" (http://mzl.la/1FuID0j)."); + NS_WARNING(message.get()); +} + +nsIVariant * +convertJSValToVariant( + JSContext *aCtx, + const JS::Value& aValue) +{ + if (aValue.isInt32()) + return new IntegerVariant(aValue.toInt32()); + + if (aValue.isDouble()) + return new FloatVariant(aValue.toDouble()); + + if (aValue.isString()) { + nsAutoJSString value; + if (!value.init(aCtx, aValue.toString())) + return nullptr; + return new TextVariant(value); + } + + if (aValue.isBoolean()) + return new IntegerVariant(aValue.isTrue() ? 1 : 0); + + if (aValue.isNull()) + return new NullVariant(); + + if (aValue.isObject()) { + JS::Rooted<JSObject*> obj(aCtx, &aValue.toObject()); + // We only support Date instances, all others fail. + bool valid; + if (!js::DateIsValid(aCtx, obj, &valid) || !valid) + return nullptr; + + double msecd; + if (!js::DateGetMsecSinceEpoch(aCtx, obj, &msecd)) + return nullptr; + + msecd *= 1000.0; + int64_t msec = msecd; + + return new IntegerVariant(msec); + } + + return nullptr; +} + +Variant_base * +convertVariantToStorageVariant(nsIVariant* aVariant) +{ + RefPtr<Variant_base> variant = do_QueryObject(aVariant); + if (variant) { + // JS helpers already convert the JS representation to a Storage Variant, + // in such a case there's nothing left to do here, so just pass-through. + return variant; + } + + if (!aVariant) + return new NullVariant(); + + uint16_t dataType; + nsresult rv = aVariant->GetDataType(&dataType); + NS_ENSURE_SUCCESS(rv, nullptr); + + switch (dataType) { + case nsIDataType::VTYPE_BOOL: + case nsIDataType::VTYPE_INT8: + case nsIDataType::VTYPE_INT16: + case nsIDataType::VTYPE_INT32: + case nsIDataType::VTYPE_UINT8: + case nsIDataType::VTYPE_UINT16: + case nsIDataType::VTYPE_UINT32: + case nsIDataType::VTYPE_INT64: + case nsIDataType::VTYPE_UINT64: { + int64_t v; + rv = aVariant->GetAsInt64(&v); + NS_ENSURE_SUCCESS(rv, nullptr); + return new IntegerVariant(v); + } + case nsIDataType::VTYPE_FLOAT: + case nsIDataType::VTYPE_DOUBLE: { + double v; + rv = aVariant->GetAsDouble(&v); + NS_ENSURE_SUCCESS(rv, nullptr); + return new FloatVariant(v); + } + case nsIDataType::VTYPE_CHAR: + case nsIDataType::VTYPE_CHAR_STR: + case nsIDataType::VTYPE_STRING_SIZE_IS: + case nsIDataType::VTYPE_UTF8STRING: + case nsIDataType::VTYPE_CSTRING: { + nsCString v; + rv = aVariant->GetAsAUTF8String(v); + NS_ENSURE_SUCCESS(rv, nullptr); + return new UTF8TextVariant(v); + } + case nsIDataType::VTYPE_WCHAR: + case nsIDataType::VTYPE_DOMSTRING: + case nsIDataType::VTYPE_WCHAR_STR: + case nsIDataType::VTYPE_WSTRING_SIZE_IS: + case nsIDataType::VTYPE_ASTRING: { + nsString v; + rv = aVariant->GetAsAString(v); + NS_ENSURE_SUCCESS(rv, nullptr); + return new TextVariant(v); + } + case nsIDataType::VTYPE_ARRAY: { + uint16_t type; + nsIID iid; + uint32_t len; + void *rawArray; + // Note this copies the array data. + rv = aVariant->GetAsArray(&type, &iid, &len, &rawArray); + NS_ENSURE_SUCCESS(rv, nullptr); + if (type == nsIDataType::VTYPE_UINT8) { + std::pair<uint8_t *, int> v(static_cast<uint8_t *>(rawArray), len); + // Take ownership of the data avoiding a further copy. + return new AdoptedBlobVariant(v); + } + MOZ_FALLTHROUGH; + } + case nsIDataType::VTYPE_EMPTY: + case nsIDataType::VTYPE_EMPTY_ARRAY: + case nsIDataType::VTYPE_VOID: + return new NullVariant(); + case nsIDataType::VTYPE_ID: + case nsIDataType::VTYPE_INTERFACE: + case nsIDataType::VTYPE_INTERFACE_IS: + default: + NS_WARNING("Unsupported variant type"); + return nullptr; + } + + return nullptr; +} + +namespace { +class CallbackEvent : public Runnable +{ +public: + explicit CallbackEvent(mozIStorageCompletionCallback *aCallback) + : mCallback(aCallback) + { + } + + NS_IMETHOD Run() override + { + (void)mCallback->Complete(NS_OK, nullptr); + return NS_OK; + } +private: + nsCOMPtr<mozIStorageCompletionCallback> mCallback; +}; +} // namespace +already_AddRefed<nsIRunnable> +newCompletionEvent(mozIStorageCompletionCallback *aCallback) +{ + NS_ASSERTION(aCallback, "Passing a null callback is a no-no!"); + nsCOMPtr<nsIRunnable> event = new CallbackEvent(aCallback); + return event.forget(); +} + +} // namespace storage +} // namespace mozilla diff --git a/storage/mozStoragePrivateHelpers.h b/storage/mozStoragePrivateHelpers.h new file mode 100644 index 000000000..cfec6ff7f --- /dev/null +++ b/storage/mozStoragePrivateHelpers.h @@ -0,0 +1,143 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozStoragePrivateHelpers_h +#define mozStoragePrivateHelpers_h + +/** + * This file contains convenience methods for mozStorage. + */ + +#include "sqlite3.h" +#include "nsIVariant.h" +#include "nsError.h" +#include "nsAutoPtr.h" +#include "js/TypeDecls.h" +#include "Variant.h" + +class mozIStorageCompletionCallback; +class nsIRunnable; + +namespace mozilla { +namespace storage { + +//////////////////////////////////////////////////////////////////////////////// +//// Macros + +#define ENSURE_INDEX_VALUE(aIndex, aCount) \ + NS_ENSURE_TRUE(aIndex < aCount, NS_ERROR_INVALID_ARG) + +//////////////////////////////////////////////////////////////////////////////// +//// Functions + +/** + * Converts a SQLite return code to an nsresult return code. + * + * @param aSQLiteResultCode + * The SQLite return code to convert. + * @returns the corresponding nsresult code for aSQLiteResultCode. + */ +nsresult convertResultCode(int aSQLiteResultCode); + +/** + * Checks the performance of a SQLite statement and logs a warning with + * NS_WARNING. Currently this only checks the number of sort operations done + * on a statement, and if more than zero have been done, the statement can be + * made faster with the careful use of an index. + * + * @param aStatement + * The sqlite3_stmt object to check. + */ +void checkAndLogStatementPerformance(sqlite3_stmt *aStatement); + +/** + * Convert the provided JS::Value into a variant representation if possible. + * + * @param aCtx + * The JSContext the value is from. + * @param aValue + * The JavaScript value to convert. All primitive types are supported, + * but only Date objects are supported from the Date family. Date + * objects are coerced to PRTime (nanoseconds since epoch) values. + * @return the variant if conversion was successful, nullptr if conversion + * failed. The caller is responsible for addref'ing if non-null. + */ +nsIVariant *convertJSValToVariant(JSContext *aCtx, const JS::Value& aValue); + +/** + * Convert a provided nsIVariant implementation to our own thread-safe + * refcounting implementation, if needed. + * + * @param aValue + * The original nsIVariant to be converted. + * @return a thread-safe refcounting nsIVariant implementation. + */ +Variant_base *convertVariantToStorageVariant(nsIVariant *aVariant); + +/** + * Obtains an event that will notify a completion callback about completion. + * + * @param aCallback + * The callback to be notified. + * @return an nsIRunnable that can be dispatched to the calling thread. + */ +already_AddRefed<nsIRunnable> newCompletionEvent( + mozIStorageCompletionCallback *aCallback +); + +/** + * Utility method to get a Blob as a string value. The string expects + * the interface exposed by nsAString/nsACString/etc. + */ +template<class T, class V> +nsresult +DoGetBlobAsString(T* aThis, uint32_t aIndex, V& aValue) +{ + typedef typename V::char_type char_type; + + uint32_t size; + char_type* blob; + nsresult rv = + aThis->GetBlob(aIndex, &size, reinterpret_cast<uint8_t**>(&blob)); + NS_ENSURE_SUCCESS(rv, rv); + + aValue.Assign(blob, size / sizeof(char_type)); + delete[] blob; + return NS_OK; +} + +/** + * Utility method to bind a string value as a Blob. The string expects + * the interface exposed by nsAString/nsACString/etc. + */ +template<class T, class V> +nsresult +DoBindStringAsBlobByName(T* aThis, const nsACString& aName, const V& aValue) +{ + typedef typename V::char_type char_type; + return aThis->BindBlobByName(aName, + reinterpret_cast<const uint8_t*>(aValue.BeginReading()), + aValue.Length() * sizeof(char_type)); +} + +/** + * Utility method to bind a string value as a Blob. The string expects + * the interface exposed by nsAString/nsACString/etc. + */ +template<class T, class V> +nsresult +DoBindStringAsBlobByIndex(T* aThis, uint32_t aIndex, const V& aValue) +{ + typedef typename V::char_type char_type; + return aThis->BindBlobByIndex(aIndex, + reinterpret_cast<const uint8_t*>(aValue.BeginReading()), + aValue.Length() * sizeof(char_type)); +} + +} // namespace storage +} // namespace mozilla + +#endif // mozStoragePrivateHelpers_h diff --git a/storage/mozStorageResultSet.cpp b/storage/mozStorageResultSet.cpp new file mode 100644 index 000000000..e4f3d1f35 --- /dev/null +++ b/storage/mozStorageResultSet.cpp @@ -0,0 +1,59 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozStorageRow.h" +#include "mozStorageResultSet.h" + +namespace mozilla { +namespace storage { + +//////////////////////////////////////////////////////////////////////////////// +//// ResultSet + +ResultSet::ResultSet() +: mCurrentIndex(0) +{ +} + +ResultSet::~ResultSet() +{ + mData.Clear(); +} + +nsresult +ResultSet::add(mozIStorageRow *aRow) +{ + return mData.AppendObject(aRow) ? NS_OK : NS_ERROR_OUT_OF_MEMORY; +} + +/** + * Note: This object is only ever accessed on one thread at a time. It it not + * threadsafe, but it does need threadsafe AddRef and Release. + */ +NS_IMPL_ISUPPORTS( + ResultSet, + mozIStorageResultSet +) + +//////////////////////////////////////////////////////////////////////////////// +//// mozIStorageResultSet + +NS_IMETHODIMP +ResultSet::GetNextRow(mozIStorageRow **_row) +{ + NS_ENSURE_ARG_POINTER(_row); + + if (mCurrentIndex >= mData.Count()) { + // Just return null here + return NS_OK; + } + + NS_ADDREF(*_row = mData.ObjectAt(mCurrentIndex++)); + return NS_OK; +} + +} // namespace storage +} // namespace mozilla diff --git a/storage/mozStorageResultSet.h b/storage/mozStorageResultSet.h new file mode 100644 index 000000000..07a861c52 --- /dev/null +++ b/storage/mozStorageResultSet.h @@ -0,0 +1,53 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozStorageResultSet_h +#define mozStorageResultSet_h + +#include "mozIStorageResultSet.h" +#include "nsCOMArray.h" +#include "mozilla/Attributes.h" +class mozIStorageRow; + +namespace mozilla { +namespace storage { + +class ResultSet final : public mozIStorageResultSet +{ +public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_MOZISTORAGERESULTSET + + ResultSet(); + + /** + * Adds a tuple to this result set. + */ + nsresult add(mozIStorageRow *aTuple); + + /** + * @returns the number of rows this result set holds. + */ + int32_t rows() const { return mData.Count(); } + +private: + ~ResultSet(); + + /** + * Stores the current index of the active result set. + */ + int32_t mCurrentIndex; + + /** + * Stores the tuples. + */ + nsCOMArray<mozIStorageRow> mData; +}; + +} // namespace storage +} // namespace mozilla + +#endif // mozStorageResultSet_h diff --git a/storage/mozStorageRow.cpp b/storage/mozStorageRow.cpp new file mode 100644 index 000000000..7bcac4c30 --- /dev/null +++ b/storage/mozStorageRow.cpp @@ -0,0 +1,247 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsString.h" + +#include "sqlite3.h" +#include "mozStoragePrivateHelpers.h" +#include "Variant.h" +#include "mozStorageRow.h" + +namespace mozilla { +namespace storage { + +//////////////////////////////////////////////////////////////////////////////// +//// Row + +nsresult +Row::initialize(sqlite3_stmt *aStatement) +{ + // Get the number of results + mNumCols = ::sqlite3_column_count(aStatement); + + // Start copying over values + for (uint32_t i = 0; i < mNumCols; i++) { + // Store the value + nsIVariant *variant = nullptr; + int type = ::sqlite3_column_type(aStatement, i); + switch (type) { + case SQLITE_INTEGER: + variant = new IntegerVariant(::sqlite3_column_int64(aStatement, i)); + break; + case SQLITE_FLOAT: + variant = new FloatVariant(::sqlite3_column_double(aStatement, i)); + break; + case SQLITE_TEXT: + { + nsDependentString str( + static_cast<const char16_t *>(::sqlite3_column_text16(aStatement, i)) + ); + variant = new TextVariant(str); + break; + } + case SQLITE_NULL: + variant = new NullVariant(); + break; + case SQLITE_BLOB: + { + int size = ::sqlite3_column_bytes(aStatement, i); + const void *data = ::sqlite3_column_blob(aStatement, i); + variant = new BlobVariant(std::pair<const void *, int>(data, size)); + break; + } + default: + return NS_ERROR_UNEXPECTED; + } + NS_ENSURE_TRUE(variant, NS_ERROR_OUT_OF_MEMORY); + + // Insert into our storage array + NS_ENSURE_TRUE(mData.InsertObjectAt(variant, i), NS_ERROR_OUT_OF_MEMORY); + + // Associate the name (if any) with the index + const char *name = ::sqlite3_column_name(aStatement, i); + if (!name) break; + nsAutoCString colName(name); + mNameHashtable.Put(colName, i); + } + + return NS_OK; +} + +/** + * Note: This object is only ever accessed on one thread at a time. It it not + * threadsafe, but it does need threadsafe AddRef and Release. + */ +NS_IMPL_ISUPPORTS( + Row, + mozIStorageRow, + mozIStorageValueArray +) + +//////////////////////////////////////////////////////////////////////////////// +//// mozIStorageRow + +NS_IMETHODIMP +Row::GetResultByIndex(uint32_t aIndex, + nsIVariant **_result) +{ + ENSURE_INDEX_VALUE(aIndex, mNumCols); + NS_ADDREF(*_result = mData.ObjectAt(aIndex)); + return NS_OK; +} + +NS_IMETHODIMP +Row::GetResultByName(const nsACString &aName, + nsIVariant **_result) +{ + uint32_t index; + NS_ENSURE_TRUE(mNameHashtable.Get(aName, &index), NS_ERROR_NOT_AVAILABLE); + return GetResultByIndex(index, _result); +} + +//////////////////////////////////////////////////////////////////////////////// +//// mozIStorageValueArray + +NS_IMETHODIMP +Row::GetNumEntries(uint32_t *_entries) +{ + *_entries = mNumCols; + return NS_OK; +} + +NS_IMETHODIMP +Row::GetTypeOfIndex(uint32_t aIndex, + int32_t *_type) +{ + ENSURE_INDEX_VALUE(aIndex, mNumCols); + + uint16_t type; + (void)mData.ObjectAt(aIndex)->GetDataType(&type); + switch (type) { + case nsIDataType::VTYPE_INT32: + case nsIDataType::VTYPE_INT64: + *_type = mozIStorageValueArray::VALUE_TYPE_INTEGER; + break; + case nsIDataType::VTYPE_DOUBLE: + *_type = mozIStorageValueArray::VALUE_TYPE_FLOAT; + break; + case nsIDataType::VTYPE_ASTRING: + *_type = mozIStorageValueArray::VALUE_TYPE_TEXT; + break; + case nsIDataType::VTYPE_ARRAY: + *_type = mozIStorageValueArray::VALUE_TYPE_BLOB; + break; + default: + *_type = mozIStorageValueArray::VALUE_TYPE_NULL; + break; + } + return NS_OK; +} + +NS_IMETHODIMP +Row::GetInt32(uint32_t aIndex, + int32_t *_value) +{ + ENSURE_INDEX_VALUE(aIndex, mNumCols); + return mData.ObjectAt(aIndex)->GetAsInt32(_value); +} + +NS_IMETHODIMP +Row::GetInt64(uint32_t aIndex, + int64_t *_value) +{ + ENSURE_INDEX_VALUE(aIndex, mNumCols); + return mData.ObjectAt(aIndex)->GetAsInt64(_value); +} + +NS_IMETHODIMP +Row::GetDouble(uint32_t aIndex, + double *_value) +{ + ENSURE_INDEX_VALUE(aIndex, mNumCols); + return mData.ObjectAt(aIndex)->GetAsDouble(_value); +} + +NS_IMETHODIMP +Row::GetUTF8String(uint32_t aIndex, + nsACString &_value) +{ + ENSURE_INDEX_VALUE(aIndex, mNumCols); + return mData.ObjectAt(aIndex)->GetAsAUTF8String(_value); +} + +NS_IMETHODIMP +Row::GetString(uint32_t aIndex, + nsAString &_value) +{ + ENSURE_INDEX_VALUE(aIndex, mNumCols); + return mData.ObjectAt(aIndex)->GetAsAString(_value); +} + +NS_IMETHODIMP +Row::GetBlob(uint32_t aIndex, + uint32_t *_size, + uint8_t **_blob) +{ + ENSURE_INDEX_VALUE(aIndex, mNumCols); + + uint16_t type; + nsIID interfaceIID; + return mData.ObjectAt(aIndex)->GetAsArray(&type, &interfaceIID, _size, + reinterpret_cast<void **>(_blob)); +} + +NS_IMETHODIMP +Row::GetBlobAsString(uint32_t aIndex, nsAString& aValue) +{ + return DoGetBlobAsString(this, aIndex, aValue); +} + +NS_IMETHODIMP +Row::GetBlobAsUTF8String(uint32_t aIndex, nsACString& aValue) +{ + return DoGetBlobAsString(this, aIndex, aValue); +} + +NS_IMETHODIMP +Row::GetIsNull(uint32_t aIndex, + bool *_isNull) +{ + ENSURE_INDEX_VALUE(aIndex, mNumCols); + NS_ENSURE_ARG_POINTER(_isNull); + + uint16_t type; + (void)mData.ObjectAt(aIndex)->GetDataType(&type); + *_isNull = type == nsIDataType::VTYPE_EMPTY; + return NS_OK; +} + +NS_IMETHODIMP +Row::GetSharedUTF8String(uint32_t, + uint32_t *, + char const **) +{ + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +Row::GetSharedString(uint32_t, + uint32_t *, + const char16_t **) +{ + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +Row::GetSharedBlob(uint32_t, + uint32_t *, + const uint8_t **) +{ + return NS_ERROR_NOT_IMPLEMENTED; +} + +} // namespace storage +} // namespace mozilla diff --git a/storage/mozStorageRow.h b/storage/mozStorageRow.h new file mode 100644 index 000000000..9145c40a5 --- /dev/null +++ b/storage/mozStorageRow.h @@ -0,0 +1,60 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozStorageRow_h +#define mozStorageRow_h + +#include "mozIStorageRow.h" +#include "nsCOMArray.h" +#include "nsDataHashtable.h" +#include "mozilla/Attributes.h" +class nsIVariant; +struct sqlite3_stmt; + +namespace mozilla { +namespace storage { + +class Row final : public mozIStorageRow +{ +public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_MOZISTORAGEROW + NS_DECL_MOZISTORAGEVALUEARRAY + + Row() : mNumCols(0) {} + + /** + * Initializes the object with the given statement. Copies the values from + * the statement. + * + * @param aStatement + * The sqlite statement to pull results from. + */ + nsresult initialize(sqlite3_stmt *aStatement); + +private: + ~Row() {} + + /** + * The number of columns in this tuple. + */ + uint32_t mNumCols; + + /** + * Stores the data in the tuple. + */ + nsCOMArray<nsIVariant> mData; + + /** + * Maps a given name to a column index. + */ + nsDataHashtable<nsCStringHashKey, uint32_t> mNameHashtable; +}; + +} // namespace storage +} // namespace mozilla + +#endif // mozStorageRow_h diff --git a/storage/mozStorageSQLFunctions.cpp b/storage/mozStorageSQLFunctions.cpp new file mode 100644 index 000000000..995e8987f --- /dev/null +++ b/storage/mozStorageSQLFunctions.cpp @@ -0,0 +1,406 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/ArrayUtils.h" + +#include "mozStorageSQLFunctions.h" +#include "nsUnicharUtils.h" +#include <algorithm> + +namespace mozilla { +namespace storage { + +//////////////////////////////////////////////////////////////////////////////// +//// Local Helper Functions + +namespace { + +/** + * Performs the LIKE comparison of a string against a pattern. For more detail + * see http://www.sqlite.org/lang_expr.html#like. + * + * @param aPatternItr + * An iterator at the start of the pattern to check for. + * @param aPatternEnd + * An iterator at the end of the pattern to check for. + * @param aStringItr + * An iterator at the start of the string to check for the pattern. + * @param aStringEnd + * An iterator at the end of the string to check for the pattern. + * @param aEscapeChar + * The character to use for escaping symbols in the pattern. + * @return 1 if the pattern is found, 0 otherwise. + */ +int +likeCompare(nsAString::const_iterator aPatternItr, + nsAString::const_iterator aPatternEnd, + nsAString::const_iterator aStringItr, + nsAString::const_iterator aStringEnd, + char16_t aEscapeChar) +{ + const char16_t MATCH_ALL('%'); + const char16_t MATCH_ONE('_'); + + bool lastWasEscape = false; + while (aPatternItr != aPatternEnd) { + /** + * What we do in here is take a look at each character from the input + * pattern, and do something with it. There are 4 possibilities: + * 1) character is an un-escaped match-all character + * 2) character is an un-escaped match-one character + * 3) character is an un-escaped escape character + * 4) character is not any of the above + */ + if (!lastWasEscape && *aPatternItr == MATCH_ALL) { + // CASE 1 + /** + * Now we need to skip any MATCH_ALL or MATCH_ONE characters that follow a + * MATCH_ALL character. For each MATCH_ONE character, skip one character + * in the pattern string. + */ + while (*aPatternItr == MATCH_ALL || *aPatternItr == MATCH_ONE) { + if (*aPatternItr == MATCH_ONE) { + // If we've hit the end of the string we are testing, no match + if (aStringItr == aStringEnd) + return 0; + aStringItr++; + } + aPatternItr++; + } + + // If we've hit the end of the pattern string, match + if (aPatternItr == aPatternEnd) + return 1; + + while (aStringItr != aStringEnd) { + if (likeCompare(aPatternItr, aPatternEnd, aStringItr, aStringEnd, + aEscapeChar)) { + // we've hit a match, so indicate this + return 1; + } + aStringItr++; + } + + // No match + return 0; + } + else if (!lastWasEscape && *aPatternItr == MATCH_ONE) { + // CASE 2 + if (aStringItr == aStringEnd) { + // If we've hit the end of the string we are testing, no match + return 0; + } + aStringItr++; + lastWasEscape = false; + } + else if (!lastWasEscape && *aPatternItr == aEscapeChar) { + // CASE 3 + lastWasEscape = true; + } + else { + // CASE 4 + if (::ToUpperCase(*aStringItr) != ::ToUpperCase(*aPatternItr)) { + // If we've hit a point where the strings don't match, there is no match + return 0; + } + aStringItr++; + lastWasEscape = false; + } + + aPatternItr++; + } + + return aStringItr == aStringEnd; +} + +/** + * Compute the Levenshtein Edit Distance between two strings. + * + * @param aStringS + * a string + * @param aStringT + * another string + * @param _result + * an outparam that will receive the edit distance between the arguments + * @return a Sqlite result code, e.g. SQLITE_OK, SQLITE_NOMEM, etc. + */ +int +levenshteinDistance(const nsAString &aStringS, + const nsAString &aStringT, + int *_result) +{ + // Set the result to a non-sensical value in case we encounter an error. + *_result = -1; + + const uint32_t sLen = aStringS.Length(); + const uint32_t tLen = aStringT.Length(); + + if (sLen == 0) { + *_result = tLen; + return SQLITE_OK; + } + if (tLen == 0) { + *_result = sLen; + return SQLITE_OK; + } + + // Notionally, Levenshtein Distance is computed in a matrix. If we + // assume s = "span" and t = "spam", the matrix would look like this: + // s --> + // t s p a n + // | 0 1 2 3 4 + // V s 1 * * * * + // p 2 * * * * + // a 3 * * * * + // m 4 * * * * + // + // Note that the row width is sLen + 1 and the column height is tLen + 1, + // where sLen is the length of the string "s" and tLen is the length of "t". + // The first row and the first column are initialized as shown, and + // the algorithm computes the remaining cells row-by-row, and + // left-to-right within each row. The computation only requires that + // we be able to see the current row and the previous one. + + // Allocate memory for two rows. + AutoTArray<int, nsAutoString::kDefaultStorageSize> row1; + AutoTArray<int, nsAutoString::kDefaultStorageSize> row2; + + // Declare the raw pointers that will actually be used to access the memory. + int *prevRow = row1.AppendElements(sLen + 1); + int *currRow = row2.AppendElements(sLen + 1); + + // Initialize the first row. + for (uint32_t i = 0; i <= sLen; i++) + prevRow[i] = i; + + const char16_t *s = aStringS.BeginReading(); + const char16_t *t = aStringT.BeginReading(); + + // Compute the empty cells in the "matrix" row-by-row, starting with + // the second row. + for (uint32_t ti = 1; ti <= tLen; ti++) { + + // Initialize the first cell in this row. + currRow[0] = ti; + + // Get the character from "t" that corresponds to this row. + const char16_t tch = t[ti - 1]; + + // Compute the remaining cells in this row, left-to-right, + // starting at the second column (and first character of "s"). + for (uint32_t si = 1; si <= sLen; si++) { + + // Get the character from "s" that corresponds to this column, + // compare it to the t-character, and compute the "cost". + const char16_t sch = s[si - 1]; + int cost = (sch == tch) ? 0 : 1; + + // ............ We want to calculate the value of cell "d" from + // ...ab....... the previously calculated (or initialized) cells + // ...cd....... "a", "b", and "c", where d = min(a', b', c'). + // ............ + int aPrime = prevRow[si - 1] + cost; + int bPrime = prevRow[si] + 1; + int cPrime = currRow[si - 1] + 1; + currRow[si] = std::min(aPrime, std::min(bPrime, cPrime)); + } + + // Advance to the next row. The current row becomes the previous + // row and we recycle the old previous row as the new current row. + // We don't need to re-initialize the new current row since we will + // rewrite all of its cells anyway. + int *oldPrevRow = prevRow; + prevRow = currRow; + currRow = oldPrevRow; + } + + // The final result is the value of the last cell in the last row. + // Note that that's now in the "previous" row, since we just swapped them. + *_result = prevRow[sLen]; + return SQLITE_OK; +} + +// This struct is used only by registerFunctions below, but ISO C++98 forbids +// instantiating a template dependent on a locally-defined type. Boo-urns! +struct Functions { + const char *zName; + int nArg; + int enc; + void *pContext; + void (*xFunc)(::sqlite3_context*, int, sqlite3_value**); +}; + +} // namespace + +//////////////////////////////////////////////////////////////////////////////// +//// Exposed Functions + +int +registerFunctions(sqlite3 *aDB) +{ + Functions functions[] = { + {"lower", + 1, + SQLITE_UTF16, + 0, + caseFunction}, + {"lower", + 1, + SQLITE_UTF8, + 0, + caseFunction}, + {"upper", + 1, + SQLITE_UTF16, + (void*)1, + caseFunction}, + {"upper", + 1, + SQLITE_UTF8, + (void*)1, + caseFunction}, + + {"like", + 2, + SQLITE_UTF16, + 0, + likeFunction}, + {"like", + 2, + SQLITE_UTF8, + 0, + likeFunction}, + {"like", + 3, + SQLITE_UTF16, + 0, + likeFunction}, + {"like", + 3, + SQLITE_UTF8, + 0, + likeFunction}, + + {"levenshteinDistance", + 2, + SQLITE_UTF16, + 0, + levenshteinDistanceFunction}, + {"levenshteinDistance", + 2, + SQLITE_UTF8, + 0, + levenshteinDistanceFunction}, + }; + + int rv = SQLITE_OK; + for (size_t i = 0; SQLITE_OK == rv && i < ArrayLength(functions); ++i) { + struct Functions *p = &functions[i]; + rv = ::sqlite3_create_function(aDB, p->zName, p->nArg, p->enc, p->pContext, + p->xFunc, nullptr, nullptr); + } + + return rv; +} + +//////////////////////////////////////////////////////////////////////////////// +//// SQL Functions + +void +caseFunction(sqlite3_context *aCtx, + int aArgc, + sqlite3_value **aArgv) +{ + NS_ASSERTION(1 == aArgc, "Invalid number of arguments!"); + + nsAutoString data(static_cast<const char16_t *>(::sqlite3_value_text16(aArgv[0]))); + bool toUpper = ::sqlite3_user_data(aCtx) ? true : false; + + if (toUpper) + ::ToUpperCase(data); + else + ::ToLowerCase(data); + + // Set the result. + ::sqlite3_result_text16(aCtx, data.get(), -1, SQLITE_TRANSIENT); +} + +/** + * This implements the like() SQL function. This is used by the LIKE operator. + * The SQL statement 'A LIKE B' is implemented as 'like(B, A)', and if there is + * an escape character, say E, it is implemented as 'like(B, A, E)'. + */ +void +likeFunction(sqlite3_context *aCtx, + int aArgc, + sqlite3_value **aArgv) +{ + NS_ASSERTION(2 == aArgc || 3 == aArgc, "Invalid number of arguments!"); + + if (::sqlite3_value_bytes(aArgv[0]) > SQLITE_MAX_LIKE_PATTERN_LENGTH) { + ::sqlite3_result_error(aCtx, "LIKE or GLOB pattern too complex", + SQLITE_TOOBIG); + return; + } + + if (!::sqlite3_value_text16(aArgv[0]) || !::sqlite3_value_text16(aArgv[1])) + return; + + nsDependentString A(static_cast<const char16_t *>(::sqlite3_value_text16(aArgv[1]))); + nsDependentString B(static_cast<const char16_t *>(::sqlite3_value_text16(aArgv[0]))); + NS_ASSERTION(!B.IsEmpty(), "LIKE string must not be null!"); + + char16_t E = 0; + if (3 == aArgc) + E = static_cast<const char16_t *>(::sqlite3_value_text16(aArgv[2]))[0]; + + nsAString::const_iterator itrString, endString; + A.BeginReading(itrString); + A.EndReading(endString); + nsAString::const_iterator itrPattern, endPattern; + B.BeginReading(itrPattern); + B.EndReading(endPattern); + ::sqlite3_result_int(aCtx, likeCompare(itrPattern, endPattern, itrString, + endString, E)); +} + +void levenshteinDistanceFunction(sqlite3_context *aCtx, + int aArgc, + sqlite3_value **aArgv) +{ + NS_ASSERTION(2 == aArgc, "Invalid number of arguments!"); + + // If either argument is a SQL NULL, then return SQL NULL. + if (::sqlite3_value_type(aArgv[0]) == SQLITE_NULL || + ::sqlite3_value_type(aArgv[1]) == SQLITE_NULL) { + ::sqlite3_result_null(aCtx); + return; + } + + int aLen = ::sqlite3_value_bytes16(aArgv[0]) / sizeof(char16_t); + const char16_t *a = static_cast<const char16_t *>(::sqlite3_value_text16(aArgv[0])); + + int bLen = ::sqlite3_value_bytes16(aArgv[1]) / sizeof(char16_t); + const char16_t *b = static_cast<const char16_t *>(::sqlite3_value_text16(aArgv[1])); + + // Compute the Levenshtein Distance, and return the result (or error). + int distance = -1; + const nsDependentString A(a, aLen); + const nsDependentString B(b, bLen); + int status = levenshteinDistance(A, B, &distance); + if (status == SQLITE_OK) { + ::sqlite3_result_int(aCtx, distance); + } + else if (status == SQLITE_NOMEM) { + ::sqlite3_result_error_nomem(aCtx); + } + else { + ::sqlite3_result_error(aCtx, "User function returned error code", -1); + } +} + +} // namespace storage +} // namespace mozilla diff --git a/storage/mozStorageSQLFunctions.h b/storage/mozStorageSQLFunctions.h new file mode 100644 index 000000000..556a4a7c1 --- /dev/null +++ b/storage/mozStorageSQLFunctions.h @@ -0,0 +1,76 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozStorageSQLFunctions_h +#define mozStorageSQLFunctions_h + +#include "sqlite3.h" +#include "nscore.h" + +namespace mozilla { +namespace storage { + +/** + * Registers the functions declared here with the specified database. + * + * @param aDB + * The database we'll be registering the functions with. + * @return the SQLite status code indicating success or failure. + */ +int registerFunctions(sqlite3 *aDB); + +//////////////////////////////////////////////////////////////////////////////// +//// Predefined Functions + +/** + * Overridden function to perform the SQL functions UPPER and LOWER. These + * support unicode, which the default implementations do not do. + * + * @param aCtx + * The sqlite_context that this function is being called on. + * @param aArgc + * The number of arguments the function is being called with. + * @param aArgv + * An array of the arguments the functions is being called with. + */ +void caseFunction(sqlite3_context *aCtx, + int aArgc, + sqlite3_value **aArgv); + +/** + * Overridden function to perform the SQL function LIKE. This supports unicode, + * which the default implementation does not do. + * + * @param aCtx + * The sqlite_context that this function is being called on. + * @param aArgc + * The number of arguments the function is being called with. + * @param aArgv + * An array of the arguments the functions is being called with. + */ +void likeFunction(sqlite3_context *aCtx, + int aArgc, + sqlite3_value **aArgv); + +/** + * An implementation of the Levenshtein Edit Distance algorithm for use in + * Sqlite queries. + * + * @param aCtx + * The sqlite_context that this function is being called on. + * @param aArgc + * The number of arguments the function is being called with. + * @param aArgv + * An array of the arguments the functions is being called with. + */ +void levenshteinDistanceFunction(sqlite3_context *aCtx, + int aArgc, + sqlite3_value **aArgv); + +} // namespace storage +} // namespace mozilla + +#endif // mozStorageSQLFunctions_h diff --git a/storage/mozStorageService.cpp b/storage/mozStorageService.cpp new file mode 100644 index 000000000..4f288ad42 --- /dev/null +++ b/storage/mozStorageService.cpp @@ -0,0 +1,982 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/Attributes.h" +#include "mozilla/DebugOnly.h" + +#include "mozStorageService.h" +#include "mozStorageConnection.h" +#include "nsAutoPtr.h" +#include "nsCollationCID.h" +#include "nsEmbedCID.h" +#include "nsThreadUtils.h" +#include "mozStoragePrivateHelpers.h" +#include "nsILocale.h" +#include "nsILocaleService.h" +#include "nsIXPConnect.h" +#include "nsIObserverService.h" +#include "nsIPropertyBag2.h" +#include "mozilla/Services.h" +#include "mozilla/Preferences.h" +#include "mozilla/LateWriteChecks.h" +#include "mozIStorageCompletionCallback.h" +#include "mozIStoragePendingStatement.h" + +#include "sqlite3.h" + +#ifdef SQLITE_OS_WIN +// "windows.h" was included and it can #define lots of things we care about... +#undef CompareString +#endif + +#include "nsIPromptService.h" + +#ifdef MOZ_STORAGE_MEMORY +# include "mozmemory.h" +# ifdef MOZ_DMD +# include "DMD.h" +# endif +#endif + +//////////////////////////////////////////////////////////////////////////////// +//// Defines + +#define PREF_TS_SYNCHRONOUS "toolkit.storage.synchronous" +#define PREF_TS_SYNCHRONOUS_DEFAULT 1 + +#define PREF_TS_PAGESIZE "toolkit.storage.pageSize" + +// This value must be kept in sync with the value of SQLITE_DEFAULT_PAGE_SIZE in +// db/sqlite3/src/Makefile.in. +#define PREF_TS_PAGESIZE_DEFAULT 32768 + +namespace mozilla { +namespace storage { + +//////////////////////////////////////////////////////////////////////////////// +//// Memory Reporting + +#ifdef MOZ_DMD +static mozilla::Atomic<size_t> gSqliteMemoryUsed; +#endif + +static int64_t +StorageSQLiteDistinguishedAmount() +{ + return ::sqlite3_memory_used(); +} + +/** + * Passes a single SQLite memory statistic to a memory reporter callback. + * + * @param aHandleReport + * The callback. + * @param aData + * The data for the callback. + * @param aConn + * The SQLite connection. + * @param aPathHead + * Head of the path for the memory report. + * @param aKind + * The memory report statistic kind, one of "stmt", "cache" or + * "schema". + * @param aDesc + * The memory report description. + * @param aOption + * The SQLite constant for getting the measurement. + * @param aTotal + * The accumulator for the measurement. + */ +static void +ReportConn(nsIHandleReportCallback *aHandleReport, + nsISupports *aData, + Connection *aConn, + const nsACString &aPathHead, + const nsACString &aKind, + const nsACString &aDesc, + int32_t aOption, + size_t *aTotal) +{ + nsCString path(aPathHead); + path.Append(aKind); + path.AppendLiteral("-used"); + + int32_t val = aConn->getSqliteRuntimeStatus(aOption); + aHandleReport->Callback(EmptyCString(), path, + nsIMemoryReporter::KIND_HEAP, + nsIMemoryReporter::UNITS_BYTES, + int64_t(val), aDesc, aData); + *aTotal += val; +} + +// Warning: To get a Connection's measurements requires holding its lock. +// There may be a delay getting the lock if another thread is accessing the +// Connection. This isn't very nice if CollectReports is called from the main +// thread! But at the time of writing this function is only called when +// about:memory is loaded (not, for example, when telemetry pings occur) and +// any delays in that case aren't so bad. +NS_IMETHODIMP +Service::CollectReports(nsIHandleReportCallback *aHandleReport, + nsISupports *aData, bool aAnonymize) +{ + size_t totalConnSize = 0; + { + nsTArray<RefPtr<Connection> > connections; + getConnections(connections); + + for (uint32_t i = 0; i < connections.Length(); i++) { + RefPtr<Connection> &conn = connections[i]; + + // Someone may have closed the Connection, in which case we skip it. + bool isReady; + (void)conn->GetConnectionReady(&isReady); + if (!isReady) { + continue; + } + + nsCString pathHead("explicit/storage/sqlite/"); + // This filename isn't privacy-sensitive, and so is never anonymized. + pathHead.Append(conn->getFilename()); + pathHead.Append('/'); + + SQLiteMutexAutoLock lockedScope(conn->sharedDBMutex); + + NS_NAMED_LITERAL_CSTRING(stmtDesc, + "Memory (approximate) used by all prepared statements used by " + "connections to this database."); + ReportConn(aHandleReport, aData, conn, pathHead, + NS_LITERAL_CSTRING("stmt"), stmtDesc, + SQLITE_DBSTATUS_STMT_USED, &totalConnSize); + + NS_NAMED_LITERAL_CSTRING(cacheDesc, + "Memory (approximate) used by all pager caches used by connections " + "to this database."); + ReportConn(aHandleReport, aData, conn, pathHead, + NS_LITERAL_CSTRING("cache"), cacheDesc, + SQLITE_DBSTATUS_CACHE_USED_SHARED, &totalConnSize); + + NS_NAMED_LITERAL_CSTRING(schemaDesc, + "Memory (approximate) used to store the schema for all databases " + "associated with connections to this database."); + ReportConn(aHandleReport, aData, conn, pathHead, + NS_LITERAL_CSTRING("schema"), schemaDesc, + SQLITE_DBSTATUS_SCHEMA_USED, &totalConnSize); + } + +#ifdef MOZ_DMD + if (::sqlite3_memory_used() != int64_t(gSqliteMemoryUsed)) { + NS_WARNING("memory consumption reported by SQLite doesn't match " + "our measurements"); + } +#endif + } + + int64_t other = ::sqlite3_memory_used() - totalConnSize; + + MOZ_COLLECT_REPORT( + "explicit/storage/sqlite/other", KIND_HEAP, UNITS_BYTES, other, + "All unclassified sqlite memory."); + + return NS_OK; +} + +//////////////////////////////////////////////////////////////////////////////// +//// Service + +NS_IMPL_ISUPPORTS( + Service, + mozIStorageService, + nsIObserver, + nsIMemoryReporter +) + +Service *Service::gService = nullptr; + +Service * +Service::getSingleton() +{ + if (gService) { + NS_ADDREF(gService); + return gService; + } + + // Ensure that we are using the same version of SQLite that we compiled with + // or newer. Our configure check ensures we are using a new enough version + // at compile time. + if (SQLITE_VERSION_NUMBER > ::sqlite3_libversion_number()) { + nsCOMPtr<nsIPromptService> ps(do_GetService(NS_PROMPTSERVICE_CONTRACTID)); + if (ps) { + nsAutoString title, message; + title.AppendLiteral("SQLite Version Error"); + message.AppendLiteral("The application has been updated, but the SQLite " + "library wasn't updated properly and the application " + "cannot run. Please try to launch the application again. " + "If that should still fail, please try reinstalling " + "it, or visit https://support.mozilla.org/."); + (void)ps->Alert(nullptr, title.get(), message.get()); + } + MOZ_CRASH("SQLite Version Error"); + } + + // The first reference to the storage service must be obtained on the + // main thread. + NS_ENSURE_TRUE(NS_IsMainThread(), nullptr); + gService = new Service(); + if (gService) { + NS_ADDREF(gService); + if (NS_FAILED(gService->initialize())) + NS_RELEASE(gService); + } + + return gService; +} + +nsIXPConnect *Service::sXPConnect = nullptr; + +// static +already_AddRefed<nsIXPConnect> +Service::getXPConnect() +{ + NS_PRECONDITION(NS_IsMainThread(), + "Must only get XPConnect on the main thread!"); + NS_PRECONDITION(gService, + "Can not get XPConnect without an instance of our service!"); + + // If we've been shutdown, sXPConnect will be null. To prevent leaks, we do + // not cache the service after this point. + nsCOMPtr<nsIXPConnect> xpc(sXPConnect); + if (!xpc) + xpc = do_GetService(nsIXPConnect::GetCID()); + NS_ASSERTION(xpc, "Could not get XPConnect!"); + return xpc.forget(); +} + +int32_t Service::sSynchronousPref; + +// static +int32_t +Service::getSynchronousPref() +{ + return sSynchronousPref; +} + +int32_t Service::sDefaultPageSize = PREF_TS_PAGESIZE_DEFAULT; + +Service::Service() +: mMutex("Service::mMutex") +, mSqliteVFS(nullptr) +, mRegistrationMutex("Service::mRegistrationMutex") +, mConnections() +{ +} + +Service::~Service() +{ + mozilla::UnregisterWeakMemoryReporter(this); + mozilla::UnregisterStorageSQLiteDistinguishedAmount(); + + int rc = sqlite3_vfs_unregister(mSqliteVFS); + if (rc != SQLITE_OK) + NS_WARNING("Failed to unregister sqlite vfs wrapper."); + + // Shutdown the sqlite3 API. Warn if shutdown did not turn out okay, but + // there is nothing actionable we can do in that case. + rc = ::sqlite3_shutdown(); + if (rc != SQLITE_OK) + NS_WARNING("sqlite3 did not shutdown cleanly."); + + DebugOnly<bool> shutdownObserved = !sXPConnect; + NS_ASSERTION(shutdownObserved, "Shutdown was not observed!"); + + gService = nullptr; + delete mSqliteVFS; + mSqliteVFS = nullptr; +} + +void +Service::registerConnection(Connection *aConnection) +{ + mRegistrationMutex.AssertNotCurrentThreadOwns(); + MutexAutoLock mutex(mRegistrationMutex); + (void)mConnections.AppendElement(aConnection); +} + +void +Service::unregisterConnection(Connection *aConnection) +{ + // If this is the last Connection it might be the only thing keeping Service + // alive. So ensure that Service is destroyed only after the Connection is + // cleanly unregistered and destroyed. + RefPtr<Service> kungFuDeathGrip(this); + { + mRegistrationMutex.AssertNotCurrentThreadOwns(); + MutexAutoLock mutex(mRegistrationMutex); + + for (uint32_t i = 0 ; i < mConnections.Length(); ++i) { + if (mConnections[i] == aConnection) { + nsCOMPtr<nsIThread> thread = mConnections[i]->threadOpenedOn; + + // Ensure the connection is released on its opening thread. Note, we + // must use .forget().take() so that we can manually cast to an + // unambiguous nsISupports type. + NS_ProxyRelease(thread, mConnections[i].forget()); + + mConnections.RemoveElementAt(i); + return; + } + } + + MOZ_ASSERT_UNREACHABLE("Attempt to unregister unknown storage connection!"); + } +} + +void +Service::getConnections(/* inout */ nsTArray<RefPtr<Connection> >& aConnections) +{ + mRegistrationMutex.AssertNotCurrentThreadOwns(); + MutexAutoLock mutex(mRegistrationMutex); + aConnections.Clear(); + aConnections.AppendElements(mConnections); +} + +void +Service::minimizeMemory() +{ + nsTArray<RefPtr<Connection> > connections; + getConnections(connections); + + for (uint32_t i = 0; i < connections.Length(); i++) { + RefPtr<Connection> conn = connections[i]; + if (!conn->connectionReady()) + continue; + + NS_NAMED_LITERAL_CSTRING(shrinkPragma, "PRAGMA shrink_memory"); + nsCOMPtr<mozIStorageConnection> syncConn = do_QueryInterface( + NS_ISUPPORTS_CAST(mozIStorageAsyncConnection*, conn)); + bool onOpenedThread = false; + + if (!syncConn) { + // This is a mozIStorageAsyncConnection, it can only be used on the main + // thread, so we can do a straight API call. + nsCOMPtr<mozIStoragePendingStatement> ps; + DebugOnly<nsresult> rv = + conn->ExecuteSimpleSQLAsync(shrinkPragma, nullptr, getter_AddRefs(ps)); + MOZ_ASSERT(NS_SUCCEEDED(rv), "Should have purged sqlite caches"); + } else if (NS_SUCCEEDED(conn->threadOpenedOn->IsOnCurrentThread(&onOpenedThread)) && + onOpenedThread) { + // We are on the opener thread, so we can just proceed. + conn->ExecuteSimpleSQL(shrinkPragma); + } else { + // We are on the wrong thread, the query should be executed on the + // opener thread, so we must dispatch to it. + nsCOMPtr<nsIRunnable> event = + NewRunnableMethod<const nsCString>( + conn, &Connection::ExecuteSimpleSQL, shrinkPragma); + conn->threadOpenedOn->Dispatch(event, NS_DISPATCH_NORMAL); + } + } +} + +void +Service::shutdown() +{ + NS_IF_RELEASE(sXPConnect); +} + +sqlite3_vfs *ConstructTelemetryVFS(); + +#ifdef MOZ_STORAGE_MEMORY + +namespace { + +// By default, SQLite tracks the size of all its heap blocks by adding an extra +// 8 bytes at the start of the block to hold the size. Unfortunately, this +// causes a lot of 2^N-sized allocations to be rounded up by jemalloc +// allocator, wasting memory. For example, a request for 1024 bytes has 8 +// bytes added, becoming a request for 1032 bytes, and jemalloc rounds this up +// to 2048 bytes, wasting 1012 bytes. (See bug 676189 for more details.) +// +// So we register jemalloc as the malloc implementation, which avoids this +// 8-byte overhead, and thus a lot of waste. This requires us to provide a +// function, sqliteMemRoundup(), which computes the actual size that will be +// allocated for a given request. SQLite uses this function before all +// allocations, and may be able to use any excess bytes caused by the rounding. +// +// Note: the wrappers for malloc, realloc and moz_malloc_usable_size are +// necessary because the sqlite_mem_methods type signatures differ slightly +// from the standard ones -- they use int instead of size_t. But we don't need +// a wrapper for free. + +#ifdef MOZ_DMD + +// sqlite does its own memory accounting, and we use its numbers in our memory +// reporters. But we don't want sqlite's heap blocks to show up in DMD's +// output as unreported, so we mark them as reported when they're allocated and +// mark them as unreported when they are freed. +// +// In other words, we are marking all sqlite heap blocks as reported even +// though we're not reporting them ourselves. Instead we're trusting that +// sqlite is fully and correctly accounting for all of its heap blocks via its +// own memory accounting. Well, we don't have to trust it entirely, because +// it's easy to keep track (while doing this DMD-specific marking) of exactly +// how much memory SQLite is using. And we can compare that against what +// SQLite reports it is using. + +MOZ_DEFINE_MALLOC_SIZE_OF_ON_ALLOC(SqliteMallocSizeOfOnAlloc) +MOZ_DEFINE_MALLOC_SIZE_OF_ON_FREE(SqliteMallocSizeOfOnFree) + +#endif + +static void *sqliteMemMalloc(int n) +{ + void* p = ::malloc(n); +#ifdef MOZ_DMD + gSqliteMemoryUsed += SqliteMallocSizeOfOnAlloc(p); +#endif + return p; +} + +static void sqliteMemFree(void *p) +{ +#ifdef MOZ_DMD + gSqliteMemoryUsed -= SqliteMallocSizeOfOnFree(p); +#endif + ::free(p); +} + +static void *sqliteMemRealloc(void *p, int n) +{ +#ifdef MOZ_DMD + gSqliteMemoryUsed -= SqliteMallocSizeOfOnFree(p); + void *pnew = ::realloc(p, n); + if (pnew) { + gSqliteMemoryUsed += SqliteMallocSizeOfOnAlloc(pnew); + } else { + // realloc failed; undo the SqliteMallocSizeOfOnFree from above + gSqliteMemoryUsed += SqliteMallocSizeOfOnAlloc(p); + } + return pnew; +#else + return ::realloc(p, n); +#endif +} + +static int sqliteMemSize(void *p) +{ + return ::moz_malloc_usable_size(p); +} + +static int sqliteMemRoundup(int n) +{ + n = malloc_good_size(n); + + // jemalloc can return blocks of size 2 and 4, but SQLite requires that all + // allocations be 8-aligned. So we round up sub-8 requests to 8. This + // wastes a small amount of memory but is obviously safe. + return n <= 8 ? 8 : n; +} + +static int sqliteMemInit(void *p) +{ + return 0; +} + +static void sqliteMemShutdown(void *p) +{ +} + +const sqlite3_mem_methods memMethods = { + &sqliteMemMalloc, + &sqliteMemFree, + &sqliteMemRealloc, + &sqliteMemSize, + &sqliteMemRoundup, + &sqliteMemInit, + &sqliteMemShutdown, + nullptr +}; + +} // namespace + +#endif // MOZ_STORAGE_MEMORY + +static const char* sObserverTopics[] = { + "memory-pressure", + "xpcom-shutdown", + "xpcom-shutdown-threads" +}; + +nsresult +Service::initialize() +{ + MOZ_ASSERT(NS_IsMainThread(), "Must be initialized on the main thread"); + + int rc; + +#ifdef MOZ_STORAGE_MEMORY + rc = ::sqlite3_config(SQLITE_CONFIG_MALLOC, &memMethods); + if (rc != SQLITE_OK) + return convertResultCode(rc); +#endif + + // TODO (bug 1191405): do not preallocate the connections caches until we + // have figured the impact on our consumers and memory. + sqlite3_config(SQLITE_CONFIG_PAGECACHE, NULL, 0, 0); + + // Explicitly initialize sqlite3. Although this is implicitly called by + // various sqlite3 functions (and the sqlite3_open calls in our case), + // the documentation suggests calling this directly. So we do. + rc = ::sqlite3_initialize(); + if (rc != SQLITE_OK) + return convertResultCode(rc); + + mSqliteVFS = ConstructTelemetryVFS(); + if (mSqliteVFS) { + rc = sqlite3_vfs_register(mSqliteVFS, 1); + if (rc != SQLITE_OK) + return convertResultCode(rc); + } else { + NS_WARNING("Failed to register telemetry VFS"); + } + + // Register for xpcom-shutdown so we can cleanup after ourselves. The + // observer service can only be used on the main thread. + nsCOMPtr<nsIObserverService> os = mozilla::services::GetObserverService(); + NS_ENSURE_TRUE(os, NS_ERROR_FAILURE); + + for (size_t i = 0; i < ArrayLength(sObserverTopics); ++i) { + nsresult rv = os->AddObserver(this, sObserverTopics[i], false); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + // We cache XPConnect for our language helpers. XPConnect can only be + // used on the main thread. + (void)CallGetService(nsIXPConnect::GetCID(), &sXPConnect); + + // We need to obtain the toolkit.storage.synchronous preferences on the main + // thread because the preference service can only be accessed there. This + // is cached in the service for all future Open[Unshared]Database calls. + sSynchronousPref = + Preferences::GetInt(PREF_TS_SYNCHRONOUS, PREF_TS_SYNCHRONOUS_DEFAULT); + + // We need to obtain the toolkit.storage.pageSize preferences on the main + // thread because the preference service can only be accessed there. This + // is cached in the service for all future Open[Unshared]Database calls. + sDefaultPageSize = + Preferences::GetInt(PREF_TS_PAGESIZE, PREF_TS_PAGESIZE_DEFAULT); + + mozilla::RegisterWeakMemoryReporter(this); + mozilla::RegisterStorageSQLiteDistinguishedAmount(StorageSQLiteDistinguishedAmount); + + return NS_OK; +} + +int +Service::localeCompareStrings(const nsAString &aStr1, + const nsAString &aStr2, + int32_t aComparisonStrength) +{ + // The implementation of nsICollation.CompareString() is platform-dependent. + // On Linux it's not thread-safe. It may not be on Windows and OS X either, + // but it's more difficult to tell. We therefore synchronize this method. + MutexAutoLock mutex(mMutex); + + nsICollation *coll = getLocaleCollation(); + if (!coll) { + NS_ERROR("Storage service has no collation"); + return 0; + } + + int32_t res; + nsresult rv = coll->CompareString(aComparisonStrength, aStr1, aStr2, &res); + if (NS_FAILED(rv)) { + NS_ERROR("Collation compare string failed"); + return 0; + } + + return res; +} + +nsICollation * +Service::getLocaleCollation() +{ + mMutex.AssertCurrentThreadOwns(); + + if (mLocaleCollation) + return mLocaleCollation; + + nsCOMPtr<nsILocaleService> svc(do_GetService(NS_LOCALESERVICE_CONTRACTID)); + if (!svc) { + NS_WARNING("Could not get locale service"); + return nullptr; + } + + nsCOMPtr<nsILocale> appLocale; + nsresult rv = svc->GetApplicationLocale(getter_AddRefs(appLocale)); + if (NS_FAILED(rv)) { + NS_WARNING("Could not get application locale"); + return nullptr; + } + + nsCOMPtr<nsICollationFactory> collFact = + do_CreateInstance(NS_COLLATIONFACTORY_CONTRACTID); + if (!collFact) { + NS_WARNING("Could not create collation factory"); + return nullptr; + } + + rv = collFact->CreateCollation(appLocale, getter_AddRefs(mLocaleCollation)); + if (NS_FAILED(rv)) { + NS_WARNING("Could not create collation"); + return nullptr; + } + + return mLocaleCollation; +} + +//////////////////////////////////////////////////////////////////////////////// +//// mozIStorageService + + +NS_IMETHODIMP +Service::OpenSpecialDatabase(const char *aStorageKey, + mozIStorageConnection **_connection) +{ + nsresult rv; + + nsCOMPtr<nsIFile> storageFile; + if (::strcmp(aStorageKey, "memory") == 0) { + // just fall through with nullptr storageFile, this will cause the storage + // connection to use a memory DB. + } + else { + return NS_ERROR_INVALID_ARG; + } + + RefPtr<Connection> msc = new Connection(this, SQLITE_OPEN_READWRITE, false); + + rv = storageFile ? msc->initialize(storageFile) : msc->initialize(); + NS_ENSURE_SUCCESS(rv, rv); + + msc.forget(_connection); + return NS_OK; + +} + +namespace { + +class AsyncInitDatabase final : public Runnable +{ +public: + AsyncInitDatabase(Connection* aConnection, + nsIFile* aStorageFile, + int32_t aGrowthIncrement, + mozIStorageCompletionCallback* aCallback) + : mConnection(aConnection) + , mStorageFile(aStorageFile) + , mGrowthIncrement(aGrowthIncrement) + , mCallback(aCallback) + { + MOZ_ASSERT(NS_IsMainThread()); + } + + NS_IMETHOD Run() override + { + MOZ_ASSERT(!NS_IsMainThread()); + nsresult rv = mStorageFile ? mConnection->initialize(mStorageFile) + : mConnection->initialize(); + if (NS_FAILED(rv)) { + nsCOMPtr<nsIRunnable> closeRunnable = + NewRunnableMethod<mozIStorageCompletionCallback*>( + mConnection.get(), + &Connection::AsyncClose, + nullptr); + MOZ_ASSERT(closeRunnable); + MOZ_ALWAYS_SUCCEEDS(NS_DispatchToMainThread(closeRunnable)); + + return DispatchResult(rv, nullptr); + } + + if (mGrowthIncrement >= 0) { + // Ignore errors. In the future, we might wish to log them. + (void)mConnection->SetGrowthIncrement(mGrowthIncrement, EmptyCString()); + } + + return DispatchResult(NS_OK, NS_ISUPPORTS_CAST(mozIStorageAsyncConnection*, + mConnection)); + } + +private: + nsresult DispatchResult(nsresult aStatus, nsISupports* aValue) { + RefPtr<CallbackComplete> event = + new CallbackComplete(aStatus, + aValue, + mCallback.forget()); + return NS_DispatchToMainThread(event); + } + + ~AsyncInitDatabase() + { + NS_ReleaseOnMainThread(mStorageFile.forget()); + NS_ReleaseOnMainThread(mConnection.forget()); + + // Generally, the callback will be released by CallbackComplete. + // However, if for some reason Run() is not executed, we still + // need to ensure that it is released here. + NS_ReleaseOnMainThread(mCallback.forget()); + } + + RefPtr<Connection> mConnection; + nsCOMPtr<nsIFile> mStorageFile; + int32_t mGrowthIncrement; + RefPtr<mozIStorageCompletionCallback> mCallback; +}; + +} // namespace + +NS_IMETHODIMP +Service::OpenAsyncDatabase(nsIVariant *aDatabaseStore, + nsIPropertyBag2 *aOptions, + mozIStorageCompletionCallback *aCallback) +{ + if (!NS_IsMainThread()) { + return NS_ERROR_NOT_SAME_THREAD; + } + NS_ENSURE_ARG(aDatabaseStore); + NS_ENSURE_ARG(aCallback); + + nsresult rv; + bool shared = false; + bool readOnly = false; + bool ignoreLockingMode = false; + int32_t growthIncrement = -1; + +#define FAIL_IF_SET_BUT_INVALID(rv)\ + if (NS_FAILED(rv) && rv != NS_ERROR_NOT_AVAILABLE) { \ + return NS_ERROR_INVALID_ARG; \ + } + + // Deal with options first: + if (aOptions) { + rv = aOptions->GetPropertyAsBool(NS_LITERAL_STRING("readOnly"), &readOnly); + FAIL_IF_SET_BUT_INVALID(rv); + + rv = aOptions->GetPropertyAsBool(NS_LITERAL_STRING("ignoreLockingMode"), + &ignoreLockingMode); + FAIL_IF_SET_BUT_INVALID(rv); + // Specifying ignoreLockingMode will force use of the readOnly flag: + if (ignoreLockingMode) { + readOnly = true; + } + + rv = aOptions->GetPropertyAsBool(NS_LITERAL_STRING("shared"), &shared); + FAIL_IF_SET_BUT_INVALID(rv); + + // NB: we re-set to -1 if we don't have a storage file later on. + rv = aOptions->GetPropertyAsInt32(NS_LITERAL_STRING("growthIncrement"), + &growthIncrement); + FAIL_IF_SET_BUT_INVALID(rv); + } + int flags = readOnly ? SQLITE_OPEN_READONLY : SQLITE_OPEN_READWRITE; + + nsCOMPtr<nsIFile> storageFile; + nsCOMPtr<nsISupports> dbStore; + rv = aDatabaseStore->GetAsISupports(getter_AddRefs(dbStore)); + if (NS_SUCCEEDED(rv)) { + // Generally, aDatabaseStore holds the database nsIFile. + storageFile = do_QueryInterface(dbStore, &rv); + if (NS_FAILED(rv)) { + return NS_ERROR_INVALID_ARG; + } + + rv = storageFile->Clone(getter_AddRefs(storageFile)); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + + if (!readOnly) { + // Ensure that SQLITE_OPEN_CREATE is passed in for compatibility reasons. + flags |= SQLITE_OPEN_CREATE; + } + + // Apply the shared-cache option. + flags |= shared ? SQLITE_OPEN_SHAREDCACHE : SQLITE_OPEN_PRIVATECACHE; + } else { + // Sometimes, however, it's a special database name. + nsAutoCString keyString; + rv = aDatabaseStore->GetAsACString(keyString); + if (NS_FAILED(rv) || !keyString.EqualsLiteral("memory")) { + return NS_ERROR_INVALID_ARG; + } + + // Just fall through with nullptr storageFile, this will cause the storage + // connection to use a memory DB. + } + + if (!storageFile && growthIncrement >= 0) { + return NS_ERROR_INVALID_ARG; + } + + // Create connection on this thread, but initialize it on its helper thread. + RefPtr<Connection> msc = new Connection(this, flags, true, + ignoreLockingMode); + nsCOMPtr<nsIEventTarget> target = msc->getAsyncExecutionTarget(); + MOZ_ASSERT(target, "Cannot initialize a connection that has been closed already"); + + RefPtr<AsyncInitDatabase> asyncInit = + new AsyncInitDatabase(msc, + storageFile, + growthIncrement, + aCallback); + return target->Dispatch(asyncInit, nsIEventTarget::DISPATCH_NORMAL); +} + +NS_IMETHODIMP +Service::OpenDatabase(nsIFile *aDatabaseFile, + mozIStorageConnection **_connection) +{ + NS_ENSURE_ARG(aDatabaseFile); + + // Always ensure that SQLITE_OPEN_CREATE is passed in for compatibility + // reasons. + int flags = SQLITE_OPEN_READWRITE | SQLITE_OPEN_SHAREDCACHE | + SQLITE_OPEN_CREATE; + RefPtr<Connection> msc = new Connection(this, flags, false); + + nsresult rv = msc->initialize(aDatabaseFile); + NS_ENSURE_SUCCESS(rv, rv); + + msc.forget(_connection); + return NS_OK; +} + +NS_IMETHODIMP +Service::OpenUnsharedDatabase(nsIFile *aDatabaseFile, + mozIStorageConnection **_connection) +{ + NS_ENSURE_ARG(aDatabaseFile); + + // Always ensure that SQLITE_OPEN_CREATE is passed in for compatibility + // reasons. + int flags = SQLITE_OPEN_READWRITE | SQLITE_OPEN_PRIVATECACHE | + SQLITE_OPEN_CREATE; + RefPtr<Connection> msc = new Connection(this, flags, false); + + nsresult rv = msc->initialize(aDatabaseFile); + NS_ENSURE_SUCCESS(rv, rv); + + msc.forget(_connection); + return NS_OK; +} + +NS_IMETHODIMP +Service::OpenDatabaseWithFileURL(nsIFileURL *aFileURL, + mozIStorageConnection **_connection) +{ + NS_ENSURE_ARG(aFileURL); + + // Always ensure that SQLITE_OPEN_CREATE is passed in for compatibility + // reasons. + int flags = SQLITE_OPEN_READWRITE | SQLITE_OPEN_SHAREDCACHE | + SQLITE_OPEN_CREATE | SQLITE_OPEN_URI; + RefPtr<Connection> msc = new Connection(this, flags, false); + + nsresult rv = msc->initialize(aFileURL); + NS_ENSURE_SUCCESS(rv, rv); + + msc.forget(_connection); + return NS_OK; +} + +NS_IMETHODIMP +Service::BackupDatabaseFile(nsIFile *aDBFile, + const nsAString &aBackupFileName, + nsIFile *aBackupParentDirectory, + nsIFile **backup) +{ + nsresult rv; + nsCOMPtr<nsIFile> parentDir = aBackupParentDirectory; + if (!parentDir) { + // This argument is optional, and defaults to the same parent directory + // as the current file. + rv = aDBFile->GetParent(getter_AddRefs(parentDir)); + NS_ENSURE_SUCCESS(rv, rv); + } + + nsCOMPtr<nsIFile> backupDB; + rv = parentDir->Clone(getter_AddRefs(backupDB)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = backupDB->Append(aBackupFileName); + NS_ENSURE_SUCCESS(rv, rv); + + rv = backupDB->CreateUnique(nsIFile::NORMAL_FILE_TYPE, 0600); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoString fileName; + rv = backupDB->GetLeafName(fileName); + NS_ENSURE_SUCCESS(rv, rv); + + rv = backupDB->Remove(false); + NS_ENSURE_SUCCESS(rv, rv); + + backupDB.forget(backup); + + return aDBFile->CopyTo(parentDir, fileName); +} + +//////////////////////////////////////////////////////////////////////////////// +//// nsIObserver + +NS_IMETHODIMP +Service::Observe(nsISupports *, const char *aTopic, const char16_t *) +{ + if (strcmp(aTopic, "memory-pressure") == 0) { + minimizeMemory(); + } else if (strcmp(aTopic, "xpcom-shutdown") == 0) { + shutdown(); + } else if (strcmp(aTopic, "xpcom-shutdown-threads") == 0) { + nsCOMPtr<nsIObserverService> os = + mozilla::services::GetObserverService(); + + for (size_t i = 0; i < ArrayLength(sObserverTopics); ++i) { + (void)os->RemoveObserver(this, sObserverTopics[i]); + } + + bool anyOpen = false; + do { + nsTArray<RefPtr<Connection> > connections; + getConnections(connections); + anyOpen = false; + for (uint32_t i = 0; i < connections.Length(); i++) { + RefPtr<Connection> &conn = connections[i]; + if (conn->isClosing()) { + anyOpen = true; + break; + } + } + if (anyOpen) { + nsCOMPtr<nsIThread> thread = do_GetCurrentThread(); + NS_ProcessNextEvent(thread); + } + } while (anyOpen); + + if (gShutdownChecks == SCM_CRASH) { + nsTArray<RefPtr<Connection> > connections; + getConnections(connections); + for (uint32_t i = 0, n = connections.Length(); i < n; i++) { + if (!connections[i]->isClosed()) { + MOZ_CRASH(); + } + } + } + } + + return NS_OK; +} + +} // namespace storage +} // namespace mozilla diff --git a/storage/mozStorageService.h b/storage/mozStorageService.h new file mode 100644 index 000000000..effd330b1 --- /dev/null +++ b/storage/mozStorageService.h @@ -0,0 +1,197 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef MOZSTORAGESERVICE_H +#define MOZSTORAGESERVICE_H + +#include "nsCOMPtr.h" +#include "nsICollation.h" +#include "nsIFile.h" +#include "nsIMemoryReporter.h" +#include "nsIObserver.h" +#include "nsTArray.h" +#include "mozilla/Mutex.h" + +#include "mozIStorageService.h" + +class nsIMemoryReporter; +class nsIXPConnect; +struct sqlite3_vfs; + +namespace mozilla { +namespace storage { + +class Connection; +class Service : public mozIStorageService + , public nsIObserver + , public nsIMemoryReporter +{ +public: + /** + * Initializes the service. This must be called before any other function! + */ + nsresult initialize(); + + /** + * Compares two strings using the Service's locale-aware collation. + * + * @param aStr1 + * The string to be compared against aStr2. + * @param aStr2 + * The string to be compared against aStr1. + * @param aComparisonStrength + * The sorting strength, one of the nsICollation constants. + * @return aStr1 - aStr2. That is, if aStr1 < aStr2, returns a negative + * number. If aStr1 > aStr2, returns a positive number. If + * aStr1 == aStr2, returns 0. + */ + int localeCompareStrings(const nsAString &aStr1, + const nsAString &aStr2, + int32_t aComparisonStrength); + + static Service *getSingleton(); + + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_MOZISTORAGESERVICE + NS_DECL_NSIOBSERVER + NS_DECL_NSIMEMORYREPORTER + + /** + * Obtains an already AddRefed pointer to XPConnect. This is used by + * language helpers. + */ + static already_AddRefed<nsIXPConnect> getXPConnect(); + + /** + * Obtains the cached data for the toolkit.storage.synchronous preference. + */ + static int32_t getSynchronousPref(); + + /** + * Obtains the default page size for this platform. The default value is + * specified in the SQLite makefile (SQLITE_DEFAULT_PAGE_SIZE) but it may be + * overriden with the PREF_TS_PAGESIZE hidden preference. + */ + static int32_t getDefaultPageSize() + { + return sDefaultPageSize; + } + + /** + * Returns a boolean value indicating whether or not the given page size is + * valid (currently understood as a power of 2 between 512 and 65536). + */ + static bool pageSizeIsValid(int32_t aPageSize) + { + return aPageSize == 512 || aPageSize == 1024 || aPageSize == 2048 || + aPageSize == 4096 || aPageSize == 8192 || aPageSize == 16384 || + aPageSize == 32768 || aPageSize == 65536; + } + + /** + * Registers the connection with the storage service. Connections are + * registered so they can be iterated over. + * + * @pre mRegistrationMutex is not held + * + * @param aConnection + * The connection to register. + */ + void registerConnection(Connection *aConnection); + + /** + * Unregisters the connection with the storage service. + * + * @pre mRegistrationMutex is not held + * + * @param aConnection + * The connection to unregister. + */ + void unregisterConnection(Connection *aConnection); + + /** + * Gets the list of open connections. Note that you must test each + * connection with mozIStorageConnection::connectionReady before doing + * anything with it, and skip it if it's not ready. + * + * @pre mRegistrationMutex is not held + * + * @param aConnections + * An inout param; it is cleared and the connections are appended to + * it. + * @return The open connections. + */ + void getConnections(nsTArray<RefPtr<Connection> >& aConnections); + +private: + Service(); + virtual ~Service(); + + /** + * Used for 1) locking around calls when initializing connections so that we + * can ensure that the state of sqlite3_enable_shared_cache is sane and 2) + * synchronizing access to mLocaleCollation. + */ + Mutex mMutex; + + sqlite3_vfs *mSqliteVFS; + + /** + * Protects mConnections. + */ + Mutex mRegistrationMutex; + + /** + * The list of connections we have created. Modifications to it are + * protected by |mRegistrationMutex|. + */ + nsTArray<RefPtr<Connection> > mConnections; + + /** + * Frees as much heap memory as possible from all of the known open + * connections. + */ + void minimizeMemory(); + + /** + * Shuts down the storage service, freeing all of the acquired resources. + */ + void shutdown(); + + /** + * Lazily creates and returns a collation created from the application's + * locale that all statements of all Connections of this Service may use. + * Since the collation's lifetime is that of the Service and no statement may + * execute outside the lifetime of the Service, this method returns a raw + * pointer. + */ + nsICollation *getLocaleCollation(); + + /** + * Lazily created collation that all statements of all Connections of this + * Service may use. The collation is created from the application's locale. + * + * @note Collation implementations are platform-dependent and in general not + * thread-safe. Access to this collation should be synchronized. + */ + nsCOMPtr<nsICollation> mLocaleCollation; + + nsCOMPtr<nsIFile> mProfileStorageFile; + + nsCOMPtr<nsIMemoryReporter> mStorageSQLiteReporter; + + static Service *gService; + + static nsIXPConnect *sXPConnect; + + static int32_t sSynchronousPref; + static int32_t sDefaultPageSize; +}; + +} // namespace storage +} // namespace mozilla + +#endif /* MOZSTORAGESERVICE_H */ diff --git a/storage/mozStorageStatement.cpp b/storage/mozStorageStatement.cpp new file mode 100644 index 000000000..7210274d0 --- /dev/null +++ b/storage/mozStorageStatement.cpp @@ -0,0 +1,889 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include <limits.h> +#include <stdio.h> + +#include "nsError.h" +#include "nsMemory.h" +#include "nsThreadUtils.h" +#include "nsIClassInfoImpl.h" +#include "Variant.h" + +#include "mozIStorageError.h" + +#include "mozStorageBindingParams.h" +#include "mozStorageConnection.h" +#include "mozStorageStatementJSHelper.h" +#include "mozStoragePrivateHelpers.h" +#include "mozStorageStatementParams.h" +#include "mozStorageStatementRow.h" +#include "mozStorageStatement.h" +#include "GeckoProfiler.h" +#include "nsDOMClassInfo.h" + +#include "mozilla/Logging.h" + + +extern mozilla::LazyLogModule gStorageLog; + +namespace mozilla { +namespace storage { + +//////////////////////////////////////////////////////////////////////////////// +//// nsIClassInfo + +NS_IMPL_CI_INTERFACE_GETTER(Statement, + mozIStorageStatement, + mozIStorageBaseStatement, + mozIStorageBindingParams, + mozIStorageValueArray, + mozilla::storage::StorageBaseStatementInternal) + +class StatementClassInfo : public nsIClassInfo +{ +public: + constexpr StatementClassInfo() {} + + NS_DECL_ISUPPORTS_INHERITED + + NS_IMETHOD + GetInterfaces(uint32_t *_count, nsIID ***_array) override + { + return NS_CI_INTERFACE_GETTER_NAME(Statement)(_count, _array); + } + + NS_IMETHOD + GetScriptableHelper(nsIXPCScriptable **_helper) override + { + static StatementJSHelper sJSHelper; + *_helper = &sJSHelper; + return NS_OK; + } + + NS_IMETHOD + GetContractID(char **_contractID) override + { + *_contractID = nullptr; + return NS_OK; + } + + NS_IMETHOD + GetClassDescription(char **_desc) override + { + *_desc = nullptr; + return NS_OK; + } + + NS_IMETHOD + GetClassID(nsCID **_id) override + { + *_id = nullptr; + return NS_OK; + } + + NS_IMETHOD + GetFlags(uint32_t *_flags) override + { + *_flags = 0; + return NS_OK; + } + + NS_IMETHOD + GetClassIDNoAlloc(nsCID *_cid) override + { + return NS_ERROR_NOT_AVAILABLE; + } +}; + +NS_IMETHODIMP_(MozExternalRefCountType) StatementClassInfo::AddRef() { return 2; } +NS_IMETHODIMP_(MozExternalRefCountType) StatementClassInfo::Release() { return 1; } +NS_IMPL_QUERY_INTERFACE(StatementClassInfo, nsIClassInfo) + +static StatementClassInfo sStatementClassInfo; + +//////////////////////////////////////////////////////////////////////////////// +//// Statement + +Statement::Statement() +: StorageBaseStatementInternal() +, mDBStatement(nullptr) +, mColumnNames() +, mExecuting(false) +{ +} + +nsresult +Statement::initialize(Connection *aDBConnection, + sqlite3 *aNativeConnection, + const nsACString &aSQLStatement) +{ + MOZ_ASSERT(aDBConnection, "No database connection given!"); + MOZ_ASSERT(!aDBConnection->isClosed(), "Database connection should be valid"); + MOZ_ASSERT(!mDBStatement, "Statement already initialized!"); + MOZ_ASSERT(aNativeConnection, "No native connection given!"); + + int srv = aDBConnection->prepareStatement(aNativeConnection, + PromiseFlatCString(aSQLStatement), + &mDBStatement); + if (srv != SQLITE_OK) { + MOZ_LOG(gStorageLog, LogLevel::Error, + ("Sqlite statement prepare error: %d '%s'", srv, + ::sqlite3_errmsg(aNativeConnection))); + MOZ_LOG(gStorageLog, LogLevel::Error, + ("Statement was: '%s'", PromiseFlatCString(aSQLStatement).get())); + return NS_ERROR_FAILURE; + } + + MOZ_LOG(gStorageLog, LogLevel::Debug, ("Initialized statement '%s' (0x%p)", + PromiseFlatCString(aSQLStatement).get(), + mDBStatement)); + + mDBConnection = aDBConnection; + mNativeConnection = aNativeConnection; + mParamCount = ::sqlite3_bind_parameter_count(mDBStatement); + mResultColumnCount = ::sqlite3_column_count(mDBStatement); + mColumnNames.Clear(); + + nsCString* columnNames = mColumnNames.AppendElements(mResultColumnCount); + for (uint32_t i = 0; i < mResultColumnCount; i++) { + const char *name = ::sqlite3_column_name(mDBStatement, i); + columnNames[i].Assign(name); + } + +#ifdef DEBUG + // We want to try and test for LIKE and that consumers are using + // escapeStringForLIKE instead of just trusting user input. The idea to + // check to see if they are binding a parameter after like instead of just + // using a string. We only do this in debug builds because it's expensive! + const nsCaseInsensitiveCStringComparator c; + nsACString::const_iterator start, end, e; + aSQLStatement.BeginReading(start); + aSQLStatement.EndReading(end); + e = end; + while (::FindInReadable(NS_LITERAL_CSTRING(" LIKE"), start, e, c)) { + // We have a LIKE in here, so we perform our tests + // FindInReadable moves the iterator, so we have to get a new one for + // each test we perform. + nsACString::const_iterator s1, s2, s3; + s1 = s2 = s3 = start; + + if (!(::FindInReadable(NS_LITERAL_CSTRING(" LIKE ?"), s1, end, c) || + ::FindInReadable(NS_LITERAL_CSTRING(" LIKE :"), s2, end, c) || + ::FindInReadable(NS_LITERAL_CSTRING(" LIKE @"), s3, end, c))) { + // At this point, we didn't find a LIKE statement followed by ?, :, + // or @, all of which are valid characters for binding a parameter. + // We will warn the consumer that they may not be safely using LIKE. + NS_WARNING("Unsafe use of LIKE detected! Please ensure that you " + "are using mozIStorageStatement::escapeStringForLIKE " + "and that you are binding that result to the statement " + "to prevent SQL injection attacks."); + } + + // resetting start and e + start = e; + e = end; + } +#endif + + return NS_OK; +} + +mozIStorageBindingParams * +Statement::getParams() +{ + nsresult rv; + + // If we do not have an array object yet, make it. + if (!mParamsArray) { + nsCOMPtr<mozIStorageBindingParamsArray> array; + rv = NewBindingParamsArray(getter_AddRefs(array)); + NS_ENSURE_SUCCESS(rv, nullptr); + + mParamsArray = static_cast<BindingParamsArray *>(array.get()); + } + + // If there isn't already any rows added, we'll have to add one to use. + if (mParamsArray->length() == 0) { + RefPtr<BindingParams> params(new BindingParams(mParamsArray, this)); + NS_ENSURE_TRUE(params, nullptr); + + rv = mParamsArray->AddParams(params); + NS_ENSURE_SUCCESS(rv, nullptr); + + // We have to unlock our params because AddParams locks them. This is safe + // because no reference to the params object was, or ever will be given out. + params->unlock(this); + + // We also want to lock our array at this point - we don't want anything to + // be added to it. Nothing has, or will ever get a reference to it, but we + // will get additional safety checks via assertions by doing this. + mParamsArray->lock(); + } + + return *mParamsArray->begin(); +} + +Statement::~Statement() +{ + (void)internalFinalize(true); +} + +//////////////////////////////////////////////////////////////////////////////// +//// nsISupports + +NS_IMPL_ADDREF(Statement) +NS_IMPL_RELEASE(Statement) + +NS_INTERFACE_MAP_BEGIN(Statement) + NS_INTERFACE_MAP_ENTRY(mozIStorageStatement) + NS_INTERFACE_MAP_ENTRY(mozIStorageBaseStatement) + NS_INTERFACE_MAP_ENTRY(mozIStorageBindingParams) + NS_INTERFACE_MAP_ENTRY(mozIStorageValueArray) + NS_INTERFACE_MAP_ENTRY(mozilla::storage::StorageBaseStatementInternal) + if (aIID.Equals(NS_GET_IID(nsIClassInfo))) { + foundInterface = static_cast<nsIClassInfo *>(&sStatementClassInfo); + } + else + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, mozIStorageStatement) +NS_INTERFACE_MAP_END + + +//////////////////////////////////////////////////////////////////////////////// +//// StorageBaseStatementInternal + +Connection * +Statement::getOwner() +{ + return mDBConnection; +} + +int +Statement::getAsyncStatement(sqlite3_stmt **_stmt) +{ + // If we have no statement, we shouldn't be calling this method! + NS_ASSERTION(mDBStatement != nullptr, "We have no statement to clone!"); + + // If we do not yet have a cached async statement, clone our statement now. + if (!mAsyncStatement) { + nsDependentCString sql(::sqlite3_sql(mDBStatement)); + int rc = mDBConnection->prepareStatement(mNativeConnection, sql, + &mAsyncStatement); + if (rc != SQLITE_OK) { + *_stmt = nullptr; + return rc; + } + + MOZ_LOG(gStorageLog, LogLevel::Debug, + ("Cloned statement 0x%p to 0x%p", mDBStatement, mAsyncStatement)); + } + + *_stmt = mAsyncStatement; + return SQLITE_OK; +} + +nsresult +Statement::getAsynchronousStatementData(StatementData &_data) +{ + if (!mDBStatement) + return NS_ERROR_UNEXPECTED; + + sqlite3_stmt *stmt; + int rc = getAsyncStatement(&stmt); + if (rc != SQLITE_OK) + return convertResultCode(rc); + + _data = StatementData(stmt, bindingParamsArray(), this); + + return NS_OK; +} + +already_AddRefed<mozIStorageBindingParams> +Statement::newBindingParams(mozIStorageBindingParamsArray *aOwner) +{ + nsCOMPtr<mozIStorageBindingParams> params = new BindingParams(aOwner, this); + return params.forget(); +} + + +//////////////////////////////////////////////////////////////////////////////// +//// mozIStorageStatement + +// proxy to StorageBaseStatementInternal using its define helper. +MIXIN_IMPL_STORAGEBASESTATEMENTINTERNAL(Statement, (void)0;) + +NS_IMETHODIMP +Statement::Clone(mozIStorageStatement **_statement) +{ + RefPtr<Statement> statement(new Statement()); + NS_ENSURE_TRUE(statement, NS_ERROR_OUT_OF_MEMORY); + + nsAutoCString sql(::sqlite3_sql(mDBStatement)); + nsresult rv = statement->initialize(mDBConnection, mNativeConnection, sql); + NS_ENSURE_SUCCESS(rv, rv); + + statement.forget(_statement); + return NS_OK; +} + +NS_IMETHODIMP +Statement::Finalize() +{ + return internalFinalize(false); +} + +nsresult +Statement::internalFinalize(bool aDestructing) +{ + if (!mDBStatement) + return NS_OK; + + int srv = SQLITE_OK; + + if (!mDBConnection->isClosed()) { + // + // The connection is still open. While statement finalization and + // closing may, in some cases, take place in two distinct threads, + // we have a guarantee that the connection will remain open until + // this method terminates: + // + // a. The connection will be closed synchronously. In this case, + // there is no race condition, as everything takes place on the + // same thread. + // + // b. The connection is closed asynchronously and this code is + // executed on the opener thread. In this case, asyncClose() has + // not been called yet and will not be called before we return + // from this function. + // + // c. The connection is closed asynchronously and this code is + // executed on the async execution thread. In this case, + // AsyncCloseConnection::Run() has not been called yet and will + // not be called before we return from this function. + // + // In either case, the connection is still valid, hence closing + // here is safe. + // + MOZ_LOG(gStorageLog, LogLevel::Debug, ("Finalizing statement '%s' during garbage-collection", + ::sqlite3_sql(mDBStatement))); + srv = ::sqlite3_finalize(mDBStatement); + } +#ifdef DEBUG + else { + // + // The database connection is either closed or closing. The sqlite + // statement has either been finalized already by the connection + // or is about to be finalized by the connection. + // + // Finalizing it here would be useless and segfaultish. + // + + char *msg = ::PR_smprintf("SQL statement (%x) should have been finalized" + " before garbage-collection. For more details on this statement, set" + " NSPR_LOG_MESSAGES=mozStorage:5 .", + mDBStatement); + + // + // Note that we can't display the statement itself, as the data structure + // is not valid anymore. However, the address shown here should help + // developers correlate with the more complete debug message triggered + // by AsyncClose(). + // + +#if 0 + // Deactivate the warning until we have fixed the exising culprit + // (see bug 914070). + NS_WARNING(msg); +#endif // 0 + + MOZ_LOG(gStorageLog, LogLevel::Warning, (msg)); + + ::PR_smprintf_free(msg); + } + +#endif + + mDBStatement = nullptr; + + if (mAsyncStatement) { + // If the destructor called us, there are no pending async statements (they + // hold a reference to us) and we can/must just kill the statement directly. + if (aDestructing) + destructorAsyncFinalize(); + else + asyncFinalize(); + } + + // Release the holders, so they can release the reference to us. + mStatementParamsHolder = nullptr; + mStatementRowHolder = nullptr; + + return convertResultCode(srv); +} + +NS_IMETHODIMP +Statement::GetParameterCount(uint32_t *_parameterCount) +{ + if (!mDBStatement) + return NS_ERROR_NOT_INITIALIZED; + + *_parameterCount = mParamCount; + return NS_OK; +} + +NS_IMETHODIMP +Statement::GetParameterName(uint32_t aParamIndex, + nsACString &_name) +{ + if (!mDBStatement) + return NS_ERROR_NOT_INITIALIZED; + ENSURE_INDEX_VALUE(aParamIndex, mParamCount); + + const char *name = ::sqlite3_bind_parameter_name(mDBStatement, + aParamIndex + 1); + if (name == nullptr) { + // this thing had no name, so fake one + nsAutoCString fakeName(":"); + fakeName.AppendInt(aParamIndex); + _name.Assign(fakeName); + } + else { + _name.Assign(nsDependentCString(name)); + } + + return NS_OK; +} + +NS_IMETHODIMP +Statement::GetParameterIndex(const nsACString &aName, + uint32_t *_index) +{ + if (!mDBStatement) + return NS_ERROR_NOT_INITIALIZED; + + // We do not accept any forms of names other than ":name", but we need to add + // the colon for SQLite. + nsAutoCString name(":"); + name.Append(aName); + int ind = ::sqlite3_bind_parameter_index(mDBStatement, name.get()); + if (ind == 0) // Named parameter not found. + return NS_ERROR_INVALID_ARG; + + *_index = ind - 1; // SQLite indexes are 1-based, we are 0-based. + + return NS_OK; +} + +NS_IMETHODIMP +Statement::GetColumnCount(uint32_t *_columnCount) +{ + if (!mDBStatement) + return NS_ERROR_NOT_INITIALIZED; + + *_columnCount = mResultColumnCount; + return NS_OK; +} + +NS_IMETHODIMP +Statement::GetColumnName(uint32_t aColumnIndex, + nsACString &_name) +{ + if (!mDBStatement) + return NS_ERROR_NOT_INITIALIZED; + ENSURE_INDEX_VALUE(aColumnIndex, mResultColumnCount); + + const char *cname = ::sqlite3_column_name(mDBStatement, aColumnIndex); + _name.Assign(nsDependentCString(cname)); + + return NS_OK; +} + +NS_IMETHODIMP +Statement::GetColumnIndex(const nsACString &aName, + uint32_t *_index) +{ + if (!mDBStatement) + return NS_ERROR_NOT_INITIALIZED; + + // Surprisingly enough, SQLite doesn't provide an API for this. We have to + // determine it ourselves sadly. + for (uint32_t i = 0; i < mResultColumnCount; i++) { + if (mColumnNames[i].Equals(aName)) { + *_index = i; + return NS_OK; + } + } + + return NS_ERROR_INVALID_ARG; +} + +NS_IMETHODIMP +Statement::Reset() +{ + if (!mDBStatement) + return NS_ERROR_NOT_INITIALIZED; + +#ifdef DEBUG + MOZ_LOG(gStorageLog, LogLevel::Debug, ("Resetting statement: '%s'", + ::sqlite3_sql(mDBStatement))); + + checkAndLogStatementPerformance(mDBStatement); +#endif + + mParamsArray = nullptr; + (void)sqlite3_reset(mDBStatement); + (void)sqlite3_clear_bindings(mDBStatement); + + mExecuting = false; + + return NS_OK; +} + +NS_IMETHODIMP +Statement::BindParameters(mozIStorageBindingParamsArray *aParameters) +{ + NS_ENSURE_ARG_POINTER(aParameters); + + if (!mDBStatement) + return NS_ERROR_NOT_INITIALIZED; + + BindingParamsArray *array = static_cast<BindingParamsArray *>(aParameters); + if (array->getOwner() != this) + return NS_ERROR_UNEXPECTED; + + if (array->length() == 0) + return NS_ERROR_UNEXPECTED; + + mParamsArray = array; + mParamsArray->lock(); + + return NS_OK; +} + +NS_IMETHODIMP +Statement::Execute() +{ + if (!mDBStatement) + return NS_ERROR_NOT_INITIALIZED; + + bool ret; + nsresult rv = ExecuteStep(&ret); + nsresult rv2 = Reset(); + + return NS_FAILED(rv) ? rv : rv2; +} + +NS_IMETHODIMP +Statement::ExecuteStep(bool *_moreResults) +{ + PROFILER_LABEL("Statement", "ExecuteStep", + js::ProfileEntry::Category::STORAGE); + + if (!mDBStatement) + return NS_ERROR_NOT_INITIALIZED; + + // Bind any parameters first before executing. + if (mParamsArray) { + // If we have more than one row of parameters to bind, they shouldn't be + // calling this method (and instead use executeAsync). + if (mParamsArray->length() != 1) + return NS_ERROR_UNEXPECTED; + + BindingParamsArray::iterator row = mParamsArray->begin(); + nsCOMPtr<IStorageBindingParamsInternal> bindingInternal = + do_QueryInterface(*row); + nsCOMPtr<mozIStorageError> error = bindingInternal->bind(mDBStatement); + if (error) { + int32_t srv; + (void)error->GetResult(&srv); + return convertResultCode(srv); + } + + // We have bound, so now we can clear our array. + mParamsArray = nullptr; + } + int srv = mDBConnection->stepStatement(mNativeConnection, mDBStatement); + + if (srv != SQLITE_ROW && srv != SQLITE_DONE && MOZ_LOG_TEST(gStorageLog, LogLevel::Debug)) { + nsAutoCString errStr; + (void)mDBConnection->GetLastErrorString(errStr); + MOZ_LOG(gStorageLog, LogLevel::Debug, + ("Statement::ExecuteStep error: %s", errStr.get())); + } + + // SQLITE_ROW and SQLITE_DONE are non-errors + if (srv == SQLITE_ROW) { + // we got a row back + mExecuting = true; + *_moreResults = true; + return NS_OK; + } + else if (srv == SQLITE_DONE) { + // statement is done (no row returned) + mExecuting = false; + *_moreResults = false; + return NS_OK; + } + else if (srv == SQLITE_BUSY || srv == SQLITE_MISUSE) { + mExecuting = false; + } + else if (mExecuting) { + MOZ_LOG(gStorageLog, LogLevel::Error, + ("SQLite error after mExecuting was true!")); + mExecuting = false; + } + + return convertResultCode(srv); +} + +NS_IMETHODIMP +Statement::GetState(int32_t *_state) +{ + if (!mDBStatement) + *_state = MOZ_STORAGE_STATEMENT_INVALID; + else if (mExecuting) + *_state = MOZ_STORAGE_STATEMENT_EXECUTING; + else + *_state = MOZ_STORAGE_STATEMENT_READY; + + return NS_OK; +} + +//////////////////////////////////////////////////////////////////////////////// +//// mozIStorageValueArray (now part of mozIStorageStatement too) + +NS_IMETHODIMP +Statement::GetNumEntries(uint32_t *_length) +{ + *_length = mResultColumnCount; + return NS_OK; +} + +NS_IMETHODIMP +Statement::GetTypeOfIndex(uint32_t aIndex, + int32_t *_type) +{ + if (!mDBStatement) + return NS_ERROR_NOT_INITIALIZED; + + ENSURE_INDEX_VALUE(aIndex, mResultColumnCount); + + if (!mExecuting) + return NS_ERROR_UNEXPECTED; + + int t = ::sqlite3_column_type(mDBStatement, aIndex); + switch (t) { + case SQLITE_INTEGER: + *_type = mozIStorageStatement::VALUE_TYPE_INTEGER; + break; + case SQLITE_FLOAT: + *_type = mozIStorageStatement::VALUE_TYPE_FLOAT; + break; + case SQLITE_TEXT: + *_type = mozIStorageStatement::VALUE_TYPE_TEXT; + break; + case SQLITE_BLOB: + *_type = mozIStorageStatement::VALUE_TYPE_BLOB; + break; + case SQLITE_NULL: + *_type = mozIStorageStatement::VALUE_TYPE_NULL; + break; + default: + return NS_ERROR_FAILURE; + } + + return NS_OK; +} + +NS_IMETHODIMP +Statement::GetInt32(uint32_t aIndex, + int32_t *_value) +{ + if (!mDBStatement) + return NS_ERROR_NOT_INITIALIZED; + + ENSURE_INDEX_VALUE(aIndex, mResultColumnCount); + + if (!mExecuting) + return NS_ERROR_UNEXPECTED; + + *_value = ::sqlite3_column_int(mDBStatement, aIndex); + return NS_OK; +} + +NS_IMETHODIMP +Statement::GetInt64(uint32_t aIndex, + int64_t *_value) +{ + if (!mDBStatement) + return NS_ERROR_NOT_INITIALIZED; + + ENSURE_INDEX_VALUE(aIndex, mResultColumnCount); + + if (!mExecuting) + return NS_ERROR_UNEXPECTED; + + *_value = ::sqlite3_column_int64(mDBStatement, aIndex); + + return NS_OK; +} + +NS_IMETHODIMP +Statement::GetDouble(uint32_t aIndex, + double *_value) +{ + if (!mDBStatement) + return NS_ERROR_NOT_INITIALIZED; + + ENSURE_INDEX_VALUE(aIndex, mResultColumnCount); + + if (!mExecuting) + return NS_ERROR_UNEXPECTED; + + *_value = ::sqlite3_column_double(mDBStatement, aIndex); + + return NS_OK; +} + +NS_IMETHODIMP +Statement::GetUTF8String(uint32_t aIndex, + nsACString &_value) +{ + // Get type of Index will check aIndex for us, so we don't have to. + int32_t type; + nsresult rv = GetTypeOfIndex(aIndex, &type); + NS_ENSURE_SUCCESS(rv, rv); + if (type == mozIStorageStatement::VALUE_TYPE_NULL) { + // NULL columns should have IsVoid set to distinguish them from the empty + // string. + _value.SetIsVoid(true); + } + else { + const char *value = + reinterpret_cast<const char *>(::sqlite3_column_text(mDBStatement, + aIndex)); + _value.Assign(value, ::sqlite3_column_bytes(mDBStatement, aIndex)); + } + return NS_OK; +} + +NS_IMETHODIMP +Statement::GetString(uint32_t aIndex, + nsAString &_value) +{ + // Get type of Index will check aIndex for us, so we don't have to. + int32_t type; + nsresult rv = GetTypeOfIndex(aIndex, &type); + NS_ENSURE_SUCCESS(rv, rv); + if (type == mozIStorageStatement::VALUE_TYPE_NULL) { + // NULL columns should have IsVoid set to distinguish them from the empty + // string. + _value.SetIsVoid(true); + } else { + const char16_t *value = + static_cast<const char16_t *>(::sqlite3_column_text16(mDBStatement, + aIndex)); + _value.Assign(value, ::sqlite3_column_bytes16(mDBStatement, aIndex) / 2); + } + return NS_OK; +} + +NS_IMETHODIMP +Statement::GetBlob(uint32_t aIndex, + uint32_t *_size, + uint8_t **_blob) +{ + if (!mDBStatement) + return NS_ERROR_NOT_INITIALIZED; + + ENSURE_INDEX_VALUE(aIndex, mResultColumnCount); + + if (!mExecuting) + return NS_ERROR_UNEXPECTED; + + int size = ::sqlite3_column_bytes(mDBStatement, aIndex); + void *blob = nullptr; + if (size) { + blob = nsMemory::Clone(::sqlite3_column_blob(mDBStatement, aIndex), size); + NS_ENSURE_TRUE(blob, NS_ERROR_OUT_OF_MEMORY); + } + + *_blob = static_cast<uint8_t *>(blob); + *_size = size; + return NS_OK; +} + +NS_IMETHODIMP +Statement::GetBlobAsString(uint32_t aIndex, nsAString& aValue) +{ + return DoGetBlobAsString(this, aIndex, aValue); +} + +NS_IMETHODIMP +Statement::GetBlobAsUTF8String(uint32_t aIndex, nsACString& aValue) +{ + return DoGetBlobAsString(this, aIndex, aValue); +} + +NS_IMETHODIMP +Statement::GetSharedUTF8String(uint32_t aIndex, + uint32_t *_length, + const char **_value) +{ + if (_length) + *_length = ::sqlite3_column_bytes(mDBStatement, aIndex); + + *_value = reinterpret_cast<const char *>(::sqlite3_column_text(mDBStatement, + aIndex)); + return NS_OK; +} + +NS_IMETHODIMP +Statement::GetSharedString(uint32_t aIndex, + uint32_t *_length, + const char16_t **_value) +{ + if (_length) + *_length = ::sqlite3_column_bytes16(mDBStatement, aIndex); + + *_value = static_cast<const char16_t *>(::sqlite3_column_text16(mDBStatement, + aIndex)); + return NS_OK; +} + +NS_IMETHODIMP +Statement::GetSharedBlob(uint32_t aIndex, + uint32_t *_size, + const uint8_t **_blob) +{ + *_size = ::sqlite3_column_bytes(mDBStatement, aIndex); + *_blob = static_cast<const uint8_t *>(::sqlite3_column_blob(mDBStatement, + aIndex)); + return NS_OK; +} + +NS_IMETHODIMP +Statement::GetIsNull(uint32_t aIndex, + bool *_isNull) +{ + // Get type of Index will check aIndex for us, so we don't have to. + int32_t type; + nsresult rv = GetTypeOfIndex(aIndex, &type); + NS_ENSURE_SUCCESS(rv, rv); + *_isNull = (type == mozIStorageStatement::VALUE_TYPE_NULL); + return NS_OK; +} + +//////////////////////////////////////////////////////////////////////////////// +//// mozIStorageBindingParams + +BOILERPLATE_BIND_PROXIES( + Statement, + if (!mDBStatement) return NS_ERROR_NOT_INITIALIZED; +) + +} // namespace storage +} // namespace mozilla diff --git a/storage/mozStorageStatement.h b/storage/mozStorageStatement.h new file mode 100644 index 000000000..69b69c58d --- /dev/null +++ b/storage/mozStorageStatement.h @@ -0,0 +1,118 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozStorageStatement_h +#define mozStorageStatement_h + +#include "nsAutoPtr.h" +#include "nsString.h" + +#include "nsTArray.h" + +#include "mozStorageBindingParamsArray.h" +#include "mozStorageStatementData.h" +#include "mozIStorageStatement.h" +#include "mozIStorageValueArray.h" +#include "StorageBaseStatementInternal.h" +#include "mozilla/Attributes.h" + +class nsIXPConnectJSObjectHolder; +struct sqlite3_stmt; + +namespace mozilla { +namespace storage { +class StatementJSHelper; +class Connection; + +class Statement final : public mozIStorageStatement + , public mozIStorageValueArray + , public StorageBaseStatementInternal +{ +public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_MOZISTORAGESTATEMENT + NS_DECL_MOZISTORAGEBASESTATEMENT + NS_DECL_MOZISTORAGEBINDINGPARAMS + // NS_DECL_MOZISTORAGEVALUEARRAY (methods in mozIStorageStatement) + NS_DECL_STORAGEBASESTATEMENTINTERNAL + + Statement(); + + /** + * Initializes the object on aDBConnection by preparing the SQL statement + * given by aSQLStatement. + * + * @param aDBConnection + * The Connection object this statement is associated with. + * @param aNativeConnection + * The native Sqlite connection this statement is associated with. + * @param aSQLStatement + * The SQL statement to prepare that this object will represent. + */ + nsresult initialize(Connection *aDBConnection, + sqlite3* aNativeConnection, + const nsACString &aSQLStatement); + + + /** + * Obtains the native statement pointer. + */ + inline sqlite3_stmt *nativeStatement() { return mDBStatement; } + + /** + * Obtains and transfers ownership of the array of parameters that are bound + * to this statment. This can be null. + */ + inline already_AddRefed<BindingParamsArray> bindingParamsArray() + { + return mParamsArray.forget(); + } + +private: + ~Statement(); + + sqlite3_stmt *mDBStatement; + uint32_t mParamCount; + uint32_t mResultColumnCount; + nsTArray<nsCString> mColumnNames; + bool mExecuting; + + /** + * @return a pointer to the BindingParams object to use with our Bind* + * method. + */ + mozIStorageBindingParams *getParams(); + + /** + * Holds the array of parameters to bind to this statement when we execute + * it asynchronously. + */ + RefPtr<BindingParamsArray> mParamsArray; + + /** + * The following two members are only used with the JS helper. They cache + * the row and params objects. + */ + nsMainThreadPtrHandle<nsIXPConnectJSObjectHolder> mStatementParamsHolder; + nsMainThreadPtrHandle<nsIXPConnectJSObjectHolder> mStatementRowHolder; + + /** + * Internal version of finalize that allows us to tell it if it is being + * called from the destructor so it can know not to dispatch events that + * require a reference to us. + * + * @param aDestructing + * Is the destructor calling? + */ + nsresult internalFinalize(bool aDestructing); + + friend class StatementJSHelper; +}; + +} // namespace storage +} // namespace mozilla + +#endif // mozStorageStatement_h diff --git a/storage/mozStorageStatementData.h b/storage/mozStorageStatementData.h new file mode 100644 index 000000000..8baaf2fa7 --- /dev/null +++ b/storage/mozStorageStatementData.h @@ -0,0 +1,150 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 sts=2 et + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozStorageStatementData_h +#define mozStorageStatementData_h + +#include "sqlite3.h" + +#include "nsAutoPtr.h" +#include "nsTArray.h" +#include "nsIEventTarget.h" +#include "MainThreadUtils.h" + +#include "mozStorageBindingParamsArray.h" +#include "mozIStorageBaseStatement.h" +#include "mozStorageConnection.h" +#include "StorageBaseStatementInternal.h" + +struct sqlite3_stmt; + +namespace mozilla { +namespace storage { + +class StatementData +{ +public: + StatementData(sqlite3_stmt *aStatement, + already_AddRefed<BindingParamsArray> aParamsArray, + StorageBaseStatementInternal *aStatementOwner) + : mStatement(aStatement) + , mParamsArray(aParamsArray) + , mStatementOwner(aStatementOwner) + { + NS_PRECONDITION(mStatementOwner, "Must have a statement owner!"); + } + StatementData(const StatementData &aSource) + : mStatement(aSource.mStatement) + , mParamsArray(aSource.mParamsArray) + , mStatementOwner(aSource.mStatementOwner) + { + NS_PRECONDITION(mStatementOwner, "Must have a statement owner!"); + } + StatementData() + : mStatement(nullptr) + { + } + ~StatementData() + { + // We need to ensure that mParamsArray is released on the main thread, + // as the binding arguments may be XPConnect values, which are safe + // to release only on the main thread. + NS_ReleaseOnMainThread(mParamsArray.forget()); + } + + /** + * Return the sqlite statement, fetching it from the storage statement. In + * the case of AsyncStatements this may actually create the statement + */ + inline int getSqliteStatement(sqlite3_stmt **_stmt) + { + if (!mStatement) { + int rc = mStatementOwner->getAsyncStatement(&mStatement); + NS_ENSURE_TRUE(rc == SQLITE_OK, rc); + } + *_stmt = mStatement; + return SQLITE_OK; + } + + operator BindingParamsArray *() const { return mParamsArray; } + + /** + * NULLs out our sqlite3_stmt (it is held by the owner) after reseting it and + * clear all bindings to it. This is expected to occur on the async thread. + */ + inline void reset() + { + NS_PRECONDITION(mStatementOwner, "Must have a statement owner!"); +#ifdef DEBUG + { + nsCOMPtr<nsIEventTarget> asyncThread = + mStatementOwner->getOwner()->getAsyncExecutionTarget(); + // It's possible that we are shutting down the async thread, and this + // method would return nullptr as a result. + if (asyncThread) { + bool onAsyncThread; + NS_ASSERTION(NS_SUCCEEDED(asyncThread->IsOnCurrentThread(&onAsyncThread)) && onAsyncThread, + "This should only be running on the async thread!"); + } + } +#endif + // In the AsyncStatement case we may never have populated mStatement if the + // AsyncExecuteStatements got canceled or a failure occurred in constructing + // the statement. + if (mStatement) { + (void)::sqlite3_reset(mStatement); + (void)::sqlite3_clear_bindings(mStatement); + mStatement = nullptr; + } + } + + /** + * Indicates if this statement has parameters to be bound before it is + * executed. + * + * @return true if the statement has parameters to bind against, false + * otherwise. + */ + inline bool hasParametersToBeBound() const { return !!mParamsArray; } + /** + * Indicates the number of implicit statements generated by this statement + * requiring a transaction for execution. For example a single statement + * with N BindingParams will execute N implicit staments. + * + * @return number of statements requiring a transaction for execution. + * + * @note In the case of AsyncStatements this may actually create the + * statement. + */ + inline uint32_t needsTransaction() + { + MOZ_ASSERT(!NS_IsMainThread()); + // Be sure to use the getSqliteStatement helper, since sqlite3_stmt_readonly + // can only analyze prepared statements and AsyncStatements are prepared + // lazily. + sqlite3_stmt *stmt; + int rc = getSqliteStatement(&stmt); + if (SQLITE_OK != rc || ::sqlite3_stmt_readonly(stmt)) { + return 0; + } + return mParamsArray ? mParamsArray->length() : 1; + } + +private: + sqlite3_stmt *mStatement; + RefPtr<BindingParamsArray> mParamsArray; + + /** + * We hold onto a reference of the statement's owner so it doesn't get + * destroyed out from under us. + */ + nsCOMPtr<StorageBaseStatementInternal> mStatementOwner; +}; + +} // namespace storage +} // namespace mozilla + +#endif // mozStorageStatementData_h diff --git a/storage/mozStorageStatementJSHelper.cpp b/storage/mozStorageStatementJSHelper.cpp new file mode 100644 index 000000000..37e3bf517 --- /dev/null +++ b/storage/mozStorageStatementJSHelper.cpp @@ -0,0 +1,287 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsIXPConnect.h" +#include "mozStorageStatement.h" +#include "mozStorageService.h" + +#include "nsMemory.h" +#include "nsString.h" +#include "nsServiceManagerUtils.h" + +#include "mozStorageStatementJSHelper.h" + +#include "mozStorageStatementRow.h" +#include "mozStorageStatementParams.h" + +#include "jsapi.h" + +#include "xpc_make_class.h" + +namespace mozilla { +namespace storage { + +//////////////////////////////////////////////////////////////////////////////// +//// Global Functions + +static +bool +stepFunc(JSContext *aCtx, + uint32_t, + JS::Value *_vp) +{ + nsCOMPtr<nsIXPConnect> xpc(Service::getXPConnect()); + nsCOMPtr<nsIXPConnectWrappedNative> wrapper; + JSObject *obj = JS_THIS_OBJECT(aCtx, _vp); + if (!obj) { + return false; + } + + nsresult rv = + xpc->GetWrappedNativeOfJSObject(aCtx, obj, getter_AddRefs(wrapper)); + if (NS_FAILED(rv)) { + ::JS_ReportErrorASCII(aCtx, "mozIStorageStatement::step() could not obtain native statement"); + return false; + } + +#ifdef DEBUG + { + nsCOMPtr<mozIStorageStatement> isStatement( + do_QueryInterface(wrapper->Native()) + ); + NS_ASSERTION(isStatement, "How is this not a statement?!"); + } +#endif + + Statement *stmt = static_cast<Statement *>( + static_cast<mozIStorageStatement *>(wrapper->Native()) + ); + + bool hasMore = false; + rv = stmt->ExecuteStep(&hasMore); + if (NS_SUCCEEDED(rv) && !hasMore) { + _vp->setBoolean(false); + (void)stmt->Reset(); + return true; + } + + if (NS_FAILED(rv)) { + ::JS_ReportErrorASCII(aCtx, "mozIStorageStatement::step() returned an error"); + return false; + } + + _vp->setBoolean(hasMore); + return true; +} + +//////////////////////////////////////////////////////////////////////////////// +//// StatementJSHelper + +nsresult +StatementJSHelper::getRow(Statement *aStatement, + JSContext *aCtx, + JSObject *aScopeObj, + JS::Value *_row) +{ + MOZ_ASSERT(NS_IsMainThread()); + nsresult rv; + +#ifdef DEBUG + int32_t state; + (void)aStatement->GetState(&state); + NS_ASSERTION(state == mozIStorageStatement::MOZ_STORAGE_STATEMENT_EXECUTING, + "Invalid state to get the row object - all calls will fail!"); +#endif + + if (!aStatement->mStatementRowHolder) { + JS::RootedObject scope(aCtx, aScopeObj); + nsCOMPtr<mozIStorageStatementRow> row(new StatementRow(aStatement)); + NS_ENSURE_TRUE(row, NS_ERROR_OUT_OF_MEMORY); + + nsCOMPtr<nsIXPConnectJSObjectHolder> holder; + nsCOMPtr<nsIXPConnect> xpc(Service::getXPConnect()); + rv = xpc->WrapNativeHolder( + aCtx, + ::JS_GetGlobalForObject(aCtx, scope), + row, + NS_GET_IID(mozIStorageStatementRow), + getter_AddRefs(holder) + ); + NS_ENSURE_SUCCESS(rv, rv); + RefPtr<StatementRowHolder> rowHolder = new StatementRowHolder(holder); + aStatement->mStatementRowHolder = + new nsMainThreadPtrHolder<nsIXPConnectJSObjectHolder>(rowHolder); + } + + JS::Rooted<JSObject*> obj(aCtx); + obj = aStatement->mStatementRowHolder->GetJSObject(); + NS_ENSURE_STATE(obj); + + _row->setObject(*obj); + return NS_OK; +} + +nsresult +StatementJSHelper::getParams(Statement *aStatement, + JSContext *aCtx, + JSObject *aScopeObj, + JS::Value *_params) +{ + MOZ_ASSERT(NS_IsMainThread()); + nsresult rv; + +#ifdef DEBUG + int32_t state; + (void)aStatement->GetState(&state); + NS_ASSERTION(state == mozIStorageStatement::MOZ_STORAGE_STATEMENT_READY, + "Invalid state to get the params object - all calls will fail!"); +#endif + + if (!aStatement->mStatementParamsHolder) { + JS::RootedObject scope(aCtx, aScopeObj); + nsCOMPtr<mozIStorageStatementParams> params = + new StatementParams(aStatement); + NS_ENSURE_TRUE(params, NS_ERROR_OUT_OF_MEMORY); + + nsCOMPtr<nsIXPConnectJSObjectHolder> holder; + nsCOMPtr<nsIXPConnect> xpc(Service::getXPConnect()); + rv = xpc->WrapNativeHolder( + aCtx, + ::JS_GetGlobalForObject(aCtx, scope), + params, + NS_GET_IID(mozIStorageStatementParams), + getter_AddRefs(holder) + ); + NS_ENSURE_SUCCESS(rv, rv); + RefPtr<StatementParamsHolder> paramsHolder = + new StatementParamsHolder(holder); + aStatement->mStatementParamsHolder = + new nsMainThreadPtrHolder<nsIXPConnectJSObjectHolder>(paramsHolder); + } + + JS::Rooted<JSObject*> obj(aCtx); + obj = aStatement->mStatementParamsHolder->GetJSObject(); + NS_ENSURE_STATE(obj); + + _params->setObject(*obj); + return NS_OK; +} + +NS_IMETHODIMP_(MozExternalRefCountType) StatementJSHelper::AddRef() { return 2; } +NS_IMETHODIMP_(MozExternalRefCountType) StatementJSHelper::Release() { return 1; } +NS_INTERFACE_MAP_BEGIN(StatementJSHelper) + NS_INTERFACE_MAP_ENTRY(nsIXPCScriptable) + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +//////////////////////////////////////////////////////////////////////////////// +//// nsIXPCScriptable + +#define XPC_MAP_CLASSNAME StatementJSHelper +#define XPC_MAP_QUOTED_CLASSNAME "StatementJSHelper" +#define XPC_MAP_WANT_GETPROPERTY +#define XPC_MAP_WANT_RESOLVE +#define XPC_MAP_FLAGS nsIXPCScriptable::ALLOW_PROP_MODS_DURING_RESOLVE +#include "xpc_map_end.h" + +NS_IMETHODIMP +StatementJSHelper::GetProperty(nsIXPConnectWrappedNative *aWrapper, + JSContext *aCtx, + JSObject *aScopeObj, + jsid aId, + JS::Value *_result, + bool *_retval) +{ + if (!JSID_IS_STRING(aId)) + return NS_OK; + + JS::Rooted<JSObject*> scope(aCtx, aScopeObj); + JS::Rooted<jsid> id(aCtx, aId); + +#ifdef DEBUG + { + nsCOMPtr<mozIStorageStatement> isStatement( + do_QueryInterface(aWrapper->Native())); + NS_ASSERTION(isStatement, "How is this not a statement?!"); + } +#endif + + Statement *stmt = static_cast<Statement *>( + static_cast<mozIStorageStatement *>(aWrapper->Native()) + ); + + JSFlatString *str = JSID_TO_FLAT_STRING(id); + if (::JS_FlatStringEqualsAscii(str, "row")) + return getRow(stmt, aCtx, scope, _result); + + if (::JS_FlatStringEqualsAscii(str, "params")) + return getParams(stmt, aCtx, scope, _result); + + return NS_OK; +} + + +NS_IMETHODIMP +StatementJSHelper::Resolve(nsIXPConnectWrappedNative *aWrapper, + JSContext *aCtx, JSObject *aScopeObj, + jsid aId, bool *aResolvedp, + bool *_retval) +{ + if (!JSID_IS_STRING(aId)) + return NS_OK; + + JS::RootedObject scope(aCtx, aScopeObj); + if (::JS_FlatStringEqualsAscii(JSID_TO_FLAT_STRING(aId), "step")) { + *_retval = ::JS_DefineFunction(aCtx, scope, "step", stepFunc, + 0, JSPROP_RESOLVING) != nullptr; + *aResolvedp = true; + return NS_OK; + } + return NS_OK; +} + +//////////////////////////////////////////////////////////////////////////////// +//// StatementJSObjectHolder + +NS_IMPL_ISUPPORTS(StatementJSObjectHolder, nsIXPConnectJSObjectHolder); + +JSObject* +StatementJSObjectHolder::GetJSObject() +{ + return mHolder->GetJSObject(); +} + +StatementJSObjectHolder::StatementJSObjectHolder(nsIXPConnectJSObjectHolder* aHolder) + : mHolder(aHolder) +{ + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(mHolder); +} + +StatementParamsHolder::~StatementParamsHolder() +{ + MOZ_ASSERT(NS_IsMainThread()); + // We are considered dead at this point, so any wrappers for row or params + // need to lose their reference to the statement. + nsCOMPtr<nsIXPConnectWrappedNative> wrapper = do_QueryInterface(mHolder); + nsCOMPtr<mozIStorageStatementParams> iObj = do_QueryWrappedNative(wrapper); + StatementParams *obj = static_cast<StatementParams *>(iObj.get()); + obj->mStatement = nullptr; +} + +StatementRowHolder::~StatementRowHolder() +{ + MOZ_ASSERT(NS_IsMainThread()); + // We are considered dead at this point, so any wrappers for row or params + // need to lose their reference to the statement. + nsCOMPtr<nsIXPConnectWrappedNative> wrapper = do_QueryInterface(mHolder); + nsCOMPtr<mozIStorageStatementRow> iObj = do_QueryWrappedNative(wrapper); + StatementRow *obj = static_cast<StatementRow *>(iObj.get()); + obj->mStatement = nullptr; +} + +} // namespace storage +} // namespace mozilla diff --git a/storage/mozStorageStatementJSHelper.h b/storage/mozStorageStatementJSHelper.h new file mode 100644 index 000000000..c7948bfa8 --- /dev/null +++ b/storage/mozStorageStatementJSHelper.h @@ -0,0 +1,70 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef MOZSTORAGESTATEMENTJSHELPER_H +#define MOZSTORAGESTATEMENTJSHELPER_H + +#include "nsIXPCScriptable.h" +#include "nsIXPConnect.h" + +class Statement; + +namespace mozilla { +namespace storage { + +class StatementJSHelper : public nsIXPCScriptable +{ +public: + NS_DECL_ISUPPORTS + NS_DECL_NSIXPCSCRIPTABLE + +private: + nsresult getRow(Statement *, JSContext *, JSObject *, JS::Value *); + nsresult getParams(Statement *, JSContext *, JSObject *, JS::Value *); +}; + +/** + * Wrappers used to clean up the references JS helpers hold to the statement. + * For cycle-avoidance reasons they do not hold reference-counted references, + * so it is important we do this. + */ +class StatementJSObjectHolder : public nsIXPConnectJSObjectHolder +{ +public: + NS_DECL_ISUPPORTS + NS_DECL_NSIXPCONNECTJSOBJECTHOLDER + + explicit StatementJSObjectHolder(nsIXPConnectJSObjectHolder* aHolder); + +protected: + virtual ~StatementJSObjectHolder() {}; + nsCOMPtr<nsIXPConnectJSObjectHolder> mHolder; +}; + +class StatementParamsHolder final: public StatementJSObjectHolder { +public: + explicit StatementParamsHolder(nsIXPConnectJSObjectHolder* aHolder) + : StatementJSObjectHolder(aHolder) { + } + +private: + virtual ~StatementParamsHolder(); +}; + +class StatementRowHolder final: public StatementJSObjectHolder { +public: + explicit StatementRowHolder(nsIXPConnectJSObjectHolder* aHolder) + : StatementJSObjectHolder(aHolder) { + } + +private: + virtual ~StatementRowHolder(); +}; + +} // namespace storage +} // namespace mozilla + +#endif // MOZSTORAGESTATEMENTJSHELPER_H diff --git a/storage/mozStorageStatementParams.cpp b/storage/mozStorageStatementParams.cpp new file mode 100644 index 000000000..6815b47da --- /dev/null +++ b/storage/mozStorageStatementParams.cpp @@ -0,0 +1,182 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsJSUtils.h" +#include "nsMemory.h" +#include "nsString.h" + +#include "jsapi.h" + +#include "mozStoragePrivateHelpers.h" +#include "mozStorageStatementParams.h" +#include "mozIStorageStatement.h" + +namespace mozilla { +namespace storage { + +//////////////////////////////////////////////////////////////////////////////// +//// StatementParams + +StatementParams::StatementParams(mozIStorageStatement *aStatement) : + mStatement(aStatement), + mParamCount(0) +{ + NS_ASSERTION(mStatement != nullptr, "mStatement is null"); + (void)mStatement->GetParameterCount(&mParamCount); +} + +NS_IMPL_ISUPPORTS( + StatementParams, + mozIStorageStatementParams, + nsIXPCScriptable +) + +//////////////////////////////////////////////////////////////////////////////// +//// nsIXPCScriptable + +#define XPC_MAP_CLASSNAME StatementParams +#define XPC_MAP_QUOTED_CLASSNAME "StatementParams" +#define XPC_MAP_WANT_SETPROPERTY +#define XPC_MAP_WANT_NEWENUMERATE +#define XPC_MAP_WANT_RESOLVE +#define XPC_MAP_FLAGS nsIXPCScriptable::ALLOW_PROP_MODS_DURING_RESOLVE +#include "xpc_map_end.h" + +NS_IMETHODIMP +StatementParams::SetProperty(nsIXPConnectWrappedNative *aWrapper, + JSContext *aCtx, + JSObject *aScopeObj, + jsid aId, + JS::Value *_vp, + bool *_retval) +{ + NS_ENSURE_TRUE(mStatement, NS_ERROR_NOT_INITIALIZED); + + if (JSID_IS_INT(aId)) { + int idx = JSID_TO_INT(aId); + + nsCOMPtr<nsIVariant> variant(convertJSValToVariant(aCtx, *_vp)); + NS_ENSURE_TRUE(variant, NS_ERROR_UNEXPECTED); + nsresult rv = mStatement->BindByIndex(idx, variant); + NS_ENSURE_SUCCESS(rv, rv); + } + else if (JSID_IS_STRING(aId)) { + JSString *str = JSID_TO_STRING(aId); + nsAutoJSString autoStr; + if (!autoStr.init(aCtx, str)) { + return NS_ERROR_FAILURE; + } + + NS_ConvertUTF16toUTF8 name(autoStr); + + // check to see if there's a parameter with this name + nsCOMPtr<nsIVariant> variant(convertJSValToVariant(aCtx, *_vp)); + NS_ENSURE_TRUE(variant, NS_ERROR_UNEXPECTED); + nsresult rv = mStatement->BindByName(name, variant); + NS_ENSURE_SUCCESS(rv, rv); + } + else { + return NS_ERROR_INVALID_ARG; + } + + *_retval = true; + return NS_OK; +} + +NS_IMETHODIMP +StatementParams::NewEnumerate(nsIXPConnectWrappedNative *aWrapper, + JSContext *aCtx, + JSObject *aScopeObj, + JS::AutoIdVector &aProperties, + bool *_retval) +{ + NS_ENSURE_TRUE(mStatement, NS_ERROR_NOT_INITIALIZED); + JS::RootedObject scope(aCtx, aScopeObj); + + if (!aProperties.reserve(mParamCount)) { + *_retval = false; + return NS_OK; + } + + for (uint32_t i = 0; i < mParamCount; i++) { + // Get the name of our parameter. + nsAutoCString name; + nsresult rv = mStatement->GetParameterName(i, name); + NS_ENSURE_SUCCESS(rv, rv); + + // But drop the first character, which is going to be a ':'. + JS::RootedString jsname(aCtx, ::JS_NewStringCopyN(aCtx, &(name.get()[1]), + name.Length() - 1)); + NS_ENSURE_TRUE(jsname, NS_ERROR_OUT_OF_MEMORY); + + // Set our name. + JS::Rooted<jsid> id(aCtx); + if (!::JS_StringToId(aCtx, jsname, &id)) { + *_retval = false; + return NS_OK; + } + + aProperties.infallibleAppend(id); + } + + return NS_OK; +} + +NS_IMETHODIMP +StatementParams::Resolve(nsIXPConnectWrappedNative *aWrapper, + JSContext *aCtx, + JSObject *aScopeObj, + jsid aId, + bool *resolvedp, + bool *_retval) +{ + NS_ENSURE_TRUE(mStatement, NS_ERROR_NOT_INITIALIZED); + // We do not throw at any point after this unless our index is out of range + // because we want to allow the prototype chain to be checked for the + // property. + + JS::RootedObject scope(aCtx, aScopeObj); + JS::RootedId id(aCtx, aId); + bool resolved = false; + bool ok = true; + if (JSID_IS_INT(id)) { + uint32_t idx = JSID_TO_INT(id); + + // Ensure that our index is within range. We do not care about the + // prototype chain being checked here. + if (idx >= mParamCount) + return NS_ERROR_INVALID_ARG; + + ok = ::JS_DefineElement(aCtx, scope, idx, JS::UndefinedHandleValue, + JSPROP_ENUMERATE | JSPROP_RESOLVING); + resolved = true; + } + else if (JSID_IS_STRING(id)) { + JSString *str = JSID_TO_STRING(id); + nsAutoJSString autoStr; + if (!autoStr.init(aCtx, str)) { + return NS_ERROR_FAILURE; + } + + // Check to see if there's a parameter with this name, and if not, let + // the rest of the prototype chain be checked. + NS_ConvertUTF16toUTF8 name(autoStr); + uint32_t idx; + nsresult rv = mStatement->GetParameterIndex(name, &idx); + if (NS_SUCCEEDED(rv)) { + ok = ::JS_DefinePropertyById(aCtx, scope, id, JS::UndefinedHandleValue, + JSPROP_ENUMERATE | JSPROP_RESOLVING); + resolved = true; + } + } + + *_retval = ok; + *resolvedp = resolved && ok; + return NS_OK; +} + +} // namespace storage +} // namespace mozilla diff --git a/storage/mozStorageStatementParams.h b/storage/mozStorageStatementParams.h new file mode 100644 index 000000000..2627f8aa1 --- /dev/null +++ b/storage/mozStorageStatementParams.h @@ -0,0 +1,43 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef MOZSTORAGESTATEMENTPARAMS_H +#define MOZSTORAGESTATEMENTPARAMS_H + +#include "mozIStorageStatementParams.h" +#include "nsIXPCScriptable.h" +#include "mozilla/Attributes.h" + +class mozIStorageStatement; + +namespace mozilla { +namespace storage { + +class StatementParams final : public mozIStorageStatementParams + , public nsIXPCScriptable +{ +public: + explicit StatementParams(mozIStorageStatement *aStatement); + + // interfaces + NS_DECL_ISUPPORTS + NS_DECL_MOZISTORAGESTATEMENTPARAMS + NS_DECL_NSIXPCSCRIPTABLE + +protected: + ~StatementParams() {} + + mozIStorageStatement *mStatement; + uint32_t mParamCount; + + friend class StatementParamsHolder; + friend class StatementRowHolder; +}; + +} // namespace storage +} // namespace mozilla + +#endif /* MOZSTORAGESTATEMENTPARAMS_H */ diff --git a/storage/mozStorageStatementRow.cpp b/storage/mozStorageStatementRow.cpp new file mode 100644 index 000000000..6ace04bbf --- /dev/null +++ b/storage/mozStorageStatementRow.cpp @@ -0,0 +1,157 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsMemory.h" +#include "nsString.h" + +#include "mozStorageStatementRow.h" +#include "mozStorageStatement.h" + +#include "jsapi.h" + +#include "xpc_make_class.h" + +namespace mozilla { +namespace storage { + +//////////////////////////////////////////////////////////////////////////////// +//// StatementRow + +StatementRow::StatementRow(Statement *aStatement) +: mStatement(aStatement) +{ +} + +NS_IMPL_ISUPPORTS( + StatementRow, + mozIStorageStatementRow, + nsIXPCScriptable +) + +//////////////////////////////////////////////////////////////////////////////// +//// nsIXPCScriptable + +#define XPC_MAP_CLASSNAME StatementRow +#define XPC_MAP_QUOTED_CLASSNAME "StatementRow" +#define XPC_MAP_WANT_GETPROPERTY +#define XPC_MAP_WANT_RESOLVE +#define XPC_MAP_FLAGS nsIXPCScriptable::ALLOW_PROP_MODS_DURING_RESOLVE +#include "xpc_map_end.h" + +NS_IMETHODIMP +StatementRow::GetProperty(nsIXPConnectWrappedNative *aWrapper, + JSContext *aCtx, + JSObject *aScopeObj, + jsid aId, + JS::Value *_vp, + bool *_retval) +{ + NS_ENSURE_TRUE(mStatement, NS_ERROR_NOT_INITIALIZED); + + JS::RootedObject scope(aCtx, aScopeObj); + if (JSID_IS_STRING(aId)) { + ::JSAutoByteString idBytes(aCtx, JSID_TO_STRING(aId)); + NS_ENSURE_TRUE(!!idBytes, NS_ERROR_OUT_OF_MEMORY); + nsDependentCString jsid(idBytes.ptr()); + + uint32_t idx; + nsresult rv = mStatement->GetColumnIndex(jsid, &idx); + NS_ENSURE_SUCCESS(rv, rv); + int32_t type; + rv = mStatement->GetTypeOfIndex(idx, &type); + NS_ENSURE_SUCCESS(rv, rv); + + if (type == mozIStorageValueArray::VALUE_TYPE_INTEGER || + type == mozIStorageValueArray::VALUE_TYPE_FLOAT) { + double dval; + rv = mStatement->GetDouble(idx, &dval); + NS_ENSURE_SUCCESS(rv, rv); + *_vp = ::JS_NumberValue(dval); + } + else if (type == mozIStorageValueArray::VALUE_TYPE_TEXT) { + uint32_t bytes; + const char16_t *sval = reinterpret_cast<const char16_t *>( + static_cast<mozIStorageStatement *>(mStatement)-> + AsSharedWString(idx, &bytes) + ); + JSString *str = ::JS_NewUCStringCopyN(aCtx, sval, bytes / 2); + if (!str) { + *_retval = false; + return NS_OK; + } + _vp->setString(str); + } + else if (type == mozIStorageValueArray::VALUE_TYPE_BLOB) { + uint32_t length; + const uint8_t *blob = static_cast<mozIStorageStatement *>(mStatement)-> + AsSharedBlob(idx, &length); + JSObject *obj = ::JS_NewArrayObject(aCtx, length); + if (!obj) { + *_retval = false; + return NS_OK; + } + _vp->setObject(*obj); + + // Copy the blob over to the JS array. + for (uint32_t i = 0; i < length; i++) { + if (!::JS_DefineElement(aCtx, scope, i, blob[i], JSPROP_ENUMERATE)) { + *_retval = false; + return NS_OK; + } + } + } + else if (type == mozIStorageValueArray::VALUE_TYPE_NULL) { + _vp->setNull(); + } + else { + NS_ERROR("unknown column type returned, what's going on?"); + } + } + + return NS_OK; +} + +NS_IMETHODIMP +StatementRow::Resolve(nsIXPConnectWrappedNative *aWrapper, + JSContext *aCtx, + JSObject *aScopeObj, + jsid aId, + bool *aResolvedp, + bool *_retval) +{ + JS::Rooted<JSObject*> scopeObj(aCtx, aScopeObj); + + NS_ENSURE_TRUE(mStatement, NS_ERROR_NOT_INITIALIZED); + // We do not throw at any point after this because we want to allow the + // prototype chain to be checked for the property. + + if (JSID_IS_STRING(aId)) { + ::JSAutoByteString idBytes(aCtx, JSID_TO_STRING(aId)); + NS_ENSURE_TRUE(!!idBytes, NS_ERROR_OUT_OF_MEMORY); + nsDependentCString name(idBytes.ptr()); + + uint32_t idx; + nsresult rv = mStatement->GetColumnIndex(name, &idx); + if (NS_FAILED(rv)) { + // It's highly likely that the name doesn't exist, so let the JS engine + // check the prototype chain and throw if that doesn't have the property + // either. + *aResolvedp = false; + return NS_OK; + } + + JS::Rooted<jsid> id(aCtx, aId); + *_retval = ::JS_DefinePropertyById(aCtx, scopeObj, id, JS::UndefinedHandleValue, + JSPROP_RESOLVING); + *aResolvedp = true; + return NS_OK; + } + + return NS_OK; +} + +} // namespace storage +} // namespace mozilla diff --git a/storage/mozStorageStatementRow.h b/storage/mozStorageStatementRow.h new file mode 100644 index 000000000..ea9e40348 --- /dev/null +++ b/storage/mozStorageStatementRow.h @@ -0,0 +1,40 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef MOZSTORAGESTATEMENTROW_H +#define MOZSTORAGESTATEMENTROW_H + +#include "mozIStorageStatementRow.h" +#include "nsIXPCScriptable.h" +#include "mozilla/Attributes.h" + +namespace mozilla { +namespace storage { + +class Statement; + +class StatementRow final : public mozIStorageStatementRow + , public nsIXPCScriptable +{ +public: + NS_DECL_ISUPPORTS + NS_DECL_MOZISTORAGESTATEMENTROW + NS_DECL_NSIXPCSCRIPTABLE + + explicit StatementRow(Statement *aStatement); +protected: + + ~StatementRow() {} + + Statement *mStatement; + + friend class StatementRowHolder; +}; + +} // namespace storage +} // namespace mozilla + +#endif /* MOZSTORAGESTATEMENTROW_H */ diff --git a/storage/storage.h b/storage/storage.h new file mode 100644 index 000000000..ec8037983 --- /dev/null +++ b/storage/storage.h @@ -0,0 +1,39 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_storage_h_ +#define mozilla_storage_h_ + +//////////////////////////////////////////////////////////////////////////////// +//// Public Interfaces + +#include "mozStorageCID.h" +#include "mozIStorageAggregateFunction.h" +#include "mozIStorageConnection.h" +#include "mozIStorageError.h" +#include "mozIStorageFunction.h" +#include "mozIStoragePendingStatement.h" +#include "mozIStorageProgressHandler.h" +#include "mozIStorageResultSet.h" +#include "mozIStorageRow.h" +#include "mozIStorageService.h" +#include "mozIStorageStatement.h" +#include "mozIStorageStatementCallback.h" +#include "mozIStorageBindingParamsArray.h" +#include "mozIStorageBindingParams.h" +#include "mozIStorageVacuumParticipant.h" +#include "mozIStorageCompletionCallback.h" +#include "mozIStorageAsyncStatement.h" +#include "mozIStorageAsyncConnection.h" + +//////////////////////////////////////////////////////////////////////////////// +//// Native Language Helpers + +#include "mozStorageHelper.h" +#include "mozilla/storage/StatementCache.h" +#include "mozilla/storage/Variant.h" + +#endif // mozilla_storage_h_ diff --git a/storage/style.txt b/storage/style.txt new file mode 100644 index 000000000..03652e606 --- /dev/null +++ b/storage/style.txt @@ -0,0 +1,141 @@ +Storage Module Style Guidelines + +These guidelines should be followed for all new code in this module. Reviewers +will be enforcing them, so please obey them! + +* All code should be contained within the namespace mozilla::storage at a + minimum. The use of namespaces is strongly encouraged. + +* All functions being called in the global namespace should be prefixed with + "::" to indicate that they are in the global namespace. + +* The indentation level to use in source code is two spaces. No tabs, please! + +* All files should have the following emacs and vim mode lines: + -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + +* All functions that are not XPCOM should start with a lowercase letter. + +* Function arguments that are not out parameters should be prefixed with a (for + pArameter), and use CamelCase. + +* Function arguments that are out parameters should be prefixed with an + underscore and have a descriptive name. + +* Function declarations should include javadoc style comments. + +* Javadoc @param tags should have the parameter description start on a new line + aligned with the variable name. See the example below. + +* Javadoc @return (note: non-plural) continuation lines should be lined up with + the initial comment. See the example below. + +* Javadoc @throws, like @param, should have the exception type on the same line + as the @throws and the description on a new line indented to line up with + the type of the exception. + +* For function implementations, each argument should be on its own line. + +* All variables should use camelCase. + +* The use of bool is encouraged whenever the variable does not have the + potential to go through xpconnect. + +* For pointer variable types, include a space after the type before the asterisk + and no space between the asterisk and variable name. + +* If any part of an if-else block requires braces, all blocks need braces. + +* Every else should be on a newline after a brace. + +* Bracing should start on the line after a function and class definition. This + goes for JavaScript code as well as C++ code. + +* If a return value is not going to be checked, the return value should be + explicitly casted to void (C style cast). + + +BIG EXAMPLE: + +*** Header *** + +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_storage_FILENAME_h_ +#define mozilla_storage_FILENAME_h_ + +namespace mozilla { +namespace storage { + +class Foo : public Bar + , public Baz +{ +public: + /** + * Brief function summary. + * + * @param aArg1 + * Description description description description description etc etc + * next line of description. + * @param aArg2 + * Description description description. + * @return Description description description description description etc etc + * next line of description. + * + * @throws NS_ERROR_FAILURE + * Okay, so this is for JavaScript code, but you probably get the + * idea. + */ + int chew(int aArg1, int aArg2); +}; + +} // storage +} // mozilla + +#endif // mozilla_storage_FILENAME_h_ + + +*** Implementation *** + +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +NS_IMPL_ISUPPORTS( + Foo +, IBar +, IBaz +) + +Foo::Foo( + LongArgumentLineThatWouldOtherwiseOverflow *aArgument1 +) +: mField1(0) +, mField2(0) +{ + someMethodWithLotsOfParamsOrJustLongParameters( + mLongFieldNameThatIsJustified, + mMaybeThisOneIsLessJustifiedButBoyIsItLong, + 15 + ); +} + +//////////////////////////////////////////////////////////////////////////////// +//// Separate sections of the file like this + +int +Foo::chew(int aArg1, int aArg2) +{ + (void)functionReturningAnIgnoredValue(); + + ::functionFromGlobalNamespaceWithVoidReturnValue(); + + return 0; +} diff --git a/storage/test/moz.build b/storage/test/moz.build new file mode 100644 index 000000000..cf8b49c50 --- /dev/null +++ b/storage/test/moz.build @@ -0,0 +1,37 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +XPCSHELL_TESTS_MANIFESTS += ['unit/xpcshell.ini'] + +GeckoCppUnitTests([ + 'test_AsXXX_helpers', + 'test_async_callbacks_with_spun_event_loops', + 'test_asyncStatementExecution_transaction', + 'test_binding_params', + 'test_file_perms', + 'test_mutex', + 'test_service_init_background_thread', + 'test_statement_scoper', + 'test_StatementCache', + 'test_transaction_helper', + 'test_true_async', + 'test_unlock_notify', +]) + +if CONFIG['MOZ_DEBUG'] and CONFIG['OS_ARCH'] not in ('WINNT', 'Darwin'): + # FIXME bug 523392: test_deadlock_detector doesn't like Windows + # FIXME bug 523378: also fails on OS X + GeckoCppUnitTests([ + 'test_deadlock_detector', + ]) + +LOCAL_INCLUDES += [ + '..', +] + +USE_LIBS += [ + 'sqlite', +] diff --git a/storage/test/storage_test_harness.h b/storage/test/storage_test_harness.h new file mode 100644 index 000000000..043652b1c --- /dev/null +++ b/storage/test/storage_test_harness.h @@ -0,0 +1,389 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "TestHarness.h" + +#include "nsMemory.h" +#include "prthread.h" +#include "nsThreadUtils.h" +#include "nsDirectoryServiceDefs.h" +#include "mozilla/ReentrantMonitor.h" + +#include "mozIStorageService.h" +#include "mozIStorageConnection.h" +#include "mozIStorageStatementCallback.h" +#include "mozIStorageCompletionCallback.h" +#include "mozIStorageBindingParamsArray.h" +#include "mozIStorageBindingParams.h" +#include "mozIStorageAsyncStatement.h" +#include "mozIStorageStatement.h" +#include "mozIStoragePendingStatement.h" +#include "mozIStorageError.h" +#include "nsIInterfaceRequestorUtils.h" +#include "nsIEventTarget.h" + +#include "sqlite3.h" + +static int gTotalTests = 0; +static int gPassedTests = 0; + +#define do_check_true(aCondition) \ + PR_BEGIN_MACRO \ + gTotalTests++; \ + if (aCondition) { \ + gPassedTests++; \ + } else { \ + fail("%s | Expected true, got false at line %d", __FILE__, __LINE__); \ + } \ + PR_END_MACRO + +#define do_check_false(aCondition) \ + PR_BEGIN_MACRO \ + gTotalTests++; \ + if (!aCondition) { \ + gPassedTests++; \ + } else { \ + fail("%s | Expected false, got true at line %d", __FILE__, __LINE__); \ + } \ + PR_END_MACRO + +#define do_check_success(aResult) \ + do_check_true(NS_SUCCEEDED(aResult)) + +#ifdef LINUX +// XXX Linux opt builds on tinderbox are orange due to linking with stdlib. +// This is sad and annoying, but it's a workaround that works. +#define do_check_eq(aExpected, aActual) \ + do_check_true(aExpected == aActual) +#else +#include <sstream> +// Print nsresult as uint32_t +std::ostream& operator<<(std::ostream& aStream, const nsresult aInput) +{ + return aStream << static_cast<uint32_t>(aInput); +} +#define do_check_eq(aExpected, aActual) \ + PR_BEGIN_MACRO \ + gTotalTests++; \ + if (aExpected == aActual) { \ + gPassedTests++; \ + } else { \ + std::ostringstream temp; \ + temp << __FILE__ << " | Expected '" << aExpected << "', got '"; \ + temp << aActual <<"' at line " << __LINE__; \ + fail(temp.str().c_str()); \ + } \ + PR_END_MACRO +#endif + +#define do_check_ok(aInvoc) do_check_true((aInvoc) == SQLITE_OK) + +already_AddRefed<mozIStorageService> +getService() +{ + nsCOMPtr<mozIStorageService> ss = + do_GetService("@mozilla.org/storage/service;1"); + do_check_true(ss); + return ss.forget(); +} + +already_AddRefed<mozIStorageConnection> +getMemoryDatabase() +{ + nsCOMPtr<mozIStorageService> ss = getService(); + nsCOMPtr<mozIStorageConnection> conn; + nsresult rv = ss->OpenSpecialDatabase("memory", getter_AddRefs(conn)); + do_check_success(rv); + return conn.forget(); +} + +already_AddRefed<mozIStorageConnection> +getDatabase() +{ + nsCOMPtr<nsIFile> dbFile; + (void)NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR, + getter_AddRefs(dbFile)); + NS_ASSERTION(dbFile, "The directory doesn't exists?!"); + + nsresult rv = dbFile->Append(NS_LITERAL_STRING("storage_test_db.sqlite")); + do_check_success(rv); + + nsCOMPtr<mozIStorageService> ss = getService(); + nsCOMPtr<mozIStorageConnection> conn; + rv = ss->OpenDatabase(dbFile, getter_AddRefs(conn)); + do_check_success(rv); + return conn.forget(); +} + + +class AsyncStatementSpinner : public mozIStorageStatementCallback + , public mozIStorageCompletionCallback +{ +public: + NS_DECL_ISUPPORTS + NS_DECL_MOZISTORAGESTATEMENTCALLBACK + NS_DECL_MOZISTORAGECOMPLETIONCALLBACK + + AsyncStatementSpinner(); + + void SpinUntilCompleted(); + + uint16_t completionReason; + +protected: + virtual ~AsyncStatementSpinner() {} + volatile bool mCompleted; +}; + +NS_IMPL_ISUPPORTS(AsyncStatementSpinner, + mozIStorageStatementCallback, + mozIStorageCompletionCallback) + +AsyncStatementSpinner::AsyncStatementSpinner() +: completionReason(0) +, mCompleted(false) +{ +} + +NS_IMETHODIMP +AsyncStatementSpinner::HandleResult(mozIStorageResultSet *aResultSet) +{ + return NS_OK; +} + +NS_IMETHODIMP +AsyncStatementSpinner::HandleError(mozIStorageError *aError) +{ + int32_t result; + nsresult rv = aError->GetResult(&result); + NS_ENSURE_SUCCESS(rv, rv); + nsAutoCString message; + rv = aError->GetMessage(message); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoCString warnMsg; + warnMsg.AppendLiteral("An error occurred while executing an async statement: "); + warnMsg.AppendInt(result); + warnMsg.Append(' '); + warnMsg.Append(message); + NS_WARNING(warnMsg.get()); + + return NS_OK; +} + +NS_IMETHODIMP +AsyncStatementSpinner::HandleCompletion(uint16_t aReason) +{ + completionReason = aReason; + mCompleted = true; + return NS_OK; +} + +NS_IMETHODIMP +AsyncStatementSpinner::Complete(nsresult, nsISupports*) +{ + mCompleted = true; + return NS_OK; +} + +void AsyncStatementSpinner::SpinUntilCompleted() +{ + nsCOMPtr<nsIThread> thread(::do_GetCurrentThread()); + nsresult rv = NS_OK; + bool processed = true; + while (!mCompleted && NS_SUCCEEDED(rv)) { + rv = thread->ProcessNextEvent(true, &processed); + } +} + +#define NS_DECL_ASYNCSTATEMENTSPINNER \ + NS_IMETHOD HandleResult(mozIStorageResultSet *aResultSet) override; + +//////////////////////////////////////////////////////////////////////////////// +//// Async Helpers + +/** + * Execute an async statement, blocking the main thread until we get the + * callback completion notification. + */ +void +blocking_async_execute(mozIStorageBaseStatement *stmt) +{ + RefPtr<AsyncStatementSpinner> spinner(new AsyncStatementSpinner()); + + nsCOMPtr<mozIStoragePendingStatement> pendy; + (void)stmt->ExecuteAsync(spinner, getter_AddRefs(pendy)); + spinner->SpinUntilCompleted(); +} + +/** + * Invoke AsyncClose on the given connection, blocking the main thread until we + * get the completion notification. + */ +void +blocking_async_close(mozIStorageConnection *db) +{ + RefPtr<AsyncStatementSpinner> spinner(new AsyncStatementSpinner()); + + db->AsyncClose(spinner); + spinner->SpinUntilCompleted(); +} + +//////////////////////////////////////////////////////////////////////////////// +//// Mutex Watching + +/** + * Verify that mozIStorageAsyncStatement's life-cycle never triggers a mutex on + * the caller (generally main) thread. We do this by decorating the sqlite + * mutex logic with our own code that checks what thread it is being invoked on + * and sets a flag if it is invoked on the main thread. We are able to easily + * decorate the SQLite mutex logic because SQLite allows us to retrieve the + * current function pointers being used and then provide a new set. + */ + +sqlite3_mutex_methods orig_mutex_methods; +sqlite3_mutex_methods wrapped_mutex_methods; + +bool mutex_used_on_watched_thread = false; +PRThread *watched_thread = nullptr; +/** + * Ugly hack to let us figure out what a connection's async thread is. If we + * were MOZILLA_INTERNAL_API and linked as such we could just include + * mozStorageConnection.h and just ask Connection directly. But that turns out + * poorly. + * + * When the thread a mutex is invoked on isn't watched_thread we save it to this + * variable. + */ +PRThread *last_non_watched_thread = nullptr; + +/** + * Set a flag if the mutex is used on the thread we are watching, but always + * call the real mutex function. + */ +extern "C" void wrapped_MutexEnter(sqlite3_mutex *mutex) +{ + PRThread *curThread = ::PR_GetCurrentThread(); + if (curThread == watched_thread) + mutex_used_on_watched_thread = true; + else + last_non_watched_thread = curThread; + orig_mutex_methods.xMutexEnter(mutex); +} + +extern "C" int wrapped_MutexTry(sqlite3_mutex *mutex) +{ + if (::PR_GetCurrentThread() == watched_thread) + mutex_used_on_watched_thread = true; + return orig_mutex_methods.xMutexTry(mutex); +} + +void hook_sqlite_mutex() +{ + // We need to initialize and teardown SQLite to get it to set up the + // default mutex handlers for us so we can steal them and wrap them. + do_check_ok(sqlite3_initialize()); + do_check_ok(sqlite3_shutdown()); + do_check_ok(::sqlite3_config(SQLITE_CONFIG_GETMUTEX, &orig_mutex_methods)); + do_check_ok(::sqlite3_config(SQLITE_CONFIG_GETMUTEX, &wrapped_mutex_methods)); + wrapped_mutex_methods.xMutexEnter = wrapped_MutexEnter; + wrapped_mutex_methods.xMutexTry = wrapped_MutexTry; + do_check_ok(::sqlite3_config(SQLITE_CONFIG_MUTEX, &wrapped_mutex_methods)); +} + +/** + * Call to clear the watch state and to set the watching against this thread. + * + * Check |mutex_used_on_watched_thread| to see if the mutex has fired since + * this method was last called. Since we're talking about the current thread, + * there are no race issues to be concerned about + */ +void watch_for_mutex_use_on_this_thread() +{ + watched_thread = ::PR_GetCurrentThread(); + mutex_used_on_watched_thread = false; +} + + +//////////////////////////////////////////////////////////////////////////////// +//// Thread Wedgers + +/** + * A runnable that blocks until code on another thread invokes its unwedge + * method. By dispatching this to a thread you can ensure that no subsequent + * runnables dispatched to the thread will execute until you invoke unwedge. + * + * The wedger is self-dispatching, just construct it with its target. + */ +class ThreadWedger : public mozilla::Runnable +{ +public: + explicit ThreadWedger(nsIEventTarget *aTarget) + : mReentrantMonitor("thread wedger") + , unwedged(false) + { + aTarget->Dispatch(this, aTarget->NS_DISPATCH_NORMAL); + } + + NS_IMETHOD Run() override + { + mozilla::ReentrantMonitorAutoEnter automon(mReentrantMonitor); + + if (!unwedged) + automon.Wait(); + + return NS_OK; + } + + void unwedge() + { + mozilla::ReentrantMonitorAutoEnter automon(mReentrantMonitor); + unwedged = true; + automon.Notify(); + } + +private: + mozilla::ReentrantMonitor mReentrantMonitor; + bool unwedged; +}; + +//////////////////////////////////////////////////////////////////////////////// +//// Async Helpers + +/** + * A horrible hack to figure out what the connection's async thread is. By + * creating a statement and async dispatching we can tell from the mutex who + * is the async thread, PRThread style. Then we map that to an nsIThread. + */ +already_AddRefed<nsIThread> +get_conn_async_thread(mozIStorageConnection *db) +{ + // Make sure we are tracking the current thread as the watched thread + watch_for_mutex_use_on_this_thread(); + + // - statement with nothing to bind + nsCOMPtr<mozIStorageAsyncStatement> stmt; + db->CreateAsyncStatement( + NS_LITERAL_CSTRING("SELECT 1"), + getter_AddRefs(stmt)); + blocking_async_execute(stmt); + stmt->Finalize(); + + nsCOMPtr<nsIThreadManager> threadMan = + do_GetService("@mozilla.org/thread-manager;1"); + nsCOMPtr<nsIThread> asyncThread; + threadMan->GetThreadFromPRThread(last_non_watched_thread, + getter_AddRefs(asyncThread)); + + // Additionally, check that the thread we get as the background thread is the + // same one as the one we report from getInterface. + nsCOMPtr<nsIEventTarget> target = do_GetInterface(db); + nsCOMPtr<nsIThread> allegedAsyncThread = do_QueryInterface(target); + PRThread *allegedPRThread; + (void)allegedAsyncThread->GetPRThread(&allegedPRThread); + do_check_eq(allegedPRThread, last_non_watched_thread); + return asyncThread.forget(); +} diff --git a/storage/test/storage_test_harness_tail.h b/storage/test/storage_test_harness_tail.h new file mode 100644 index 000000000..4ad25c719 --- /dev/null +++ b/storage/test/storage_test_harness_tail.h @@ -0,0 +1,28 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef TEST_NAME +#error "Must #define TEST_NAME before including storage_test_harness_tail.h" +#endif + +#ifndef TEST_FILE +#error "Must #define TEST_FILE before include storage_test_harness_tail.h" +#endif + +int +main(int aArgc, + char **aArgv) +{ + ScopedXPCOM xpcom(TEST_NAME); + + for (size_t i = 0; i < mozilla::ArrayLength(gTests); i++) + gTests[i](); + + if (gPassedTests == gTotalTests) + passed(TEST_FILE); + + (void)printf("%i of %i tests passed\n", gPassedTests, gTotalTests); +} diff --git a/storage/test/test_AsXXX_helpers.cpp b/storage/test/test_AsXXX_helpers.cpp new file mode 100644 index 000000000..4edf7b93a --- /dev/null +++ b/storage/test/test_AsXXX_helpers.cpp @@ -0,0 +1,127 @@ +/* + *Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +#include "storage_test_harness.h" +#include "mozIStorageRow.h" +#include "mozIStorageResultSet.h" + +/** + * This file tests AsXXX (AsInt32, AsInt64, ...) helpers. + */ + +//////////////////////////////////////////////////////////////////////////////// +//// Event Loop Spinning + +class Spinner : public AsyncStatementSpinner +{ +public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_ASYNCSTATEMENTSPINNER + Spinner() {} +protected: + virtual ~Spinner() {} +}; + +NS_IMPL_ISUPPORTS_INHERITED0(Spinner, + AsyncStatementSpinner) + +NS_IMETHODIMP +Spinner::HandleResult(mozIStorageResultSet *aResultSet) +{ + nsCOMPtr<mozIStorageRow> row; + do_check_true(NS_SUCCEEDED(aResultSet->GetNextRow(getter_AddRefs(row))) && row); + + do_check_eq(row->AsInt32(0), 0); + do_check_eq(row->AsInt64(0), 0); + do_check_eq(row->AsDouble(0), 0.0); + + uint32_t len = 100; + do_check_eq(row->AsSharedUTF8String(0, &len), (const char*)nullptr); + do_check_eq(len, 0); + len = 100; + do_check_eq(row->AsSharedWString(0, &len), (const char16_t*)nullptr); + do_check_eq(len, 0); + len = 100; + do_check_eq(row->AsSharedBlob(0, &len), (const uint8_t*)nullptr); + do_check_eq(len, 0); + + do_check_eq(row->IsNull(0), true); + return NS_OK; +} + +void +test_NULLFallback() +{ + nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase()); + + nsCOMPtr<mozIStorageStatement> stmt; + (void)db->CreateStatement(NS_LITERAL_CSTRING( + "SELECT NULL" + ), getter_AddRefs(stmt)); + + nsCOMPtr<mozIStorageValueArray> valueArray = do_QueryInterface(stmt); + do_check_true(valueArray); + + bool hasMore; + do_check_true(NS_SUCCEEDED(stmt->ExecuteStep(&hasMore)) && hasMore); + + do_check_eq(stmt->AsInt32(0), 0); + do_check_eq(stmt->AsInt64(0), 0); + do_check_eq(stmt->AsDouble(0), 0.0); + uint32_t len = 100; + do_check_eq(stmt->AsSharedUTF8String(0, &len), (const char*)nullptr); + do_check_eq(len, 0); + len = 100; + do_check_eq(stmt->AsSharedWString(0, &len), (const char16_t*)nullptr); + do_check_eq(len, 0); + len = 100; + do_check_eq(stmt->AsSharedBlob(0, &len), (const uint8_t*)nullptr); + do_check_eq(len, 0); + do_check_eq(stmt->IsNull(0), true); + + do_check_eq(valueArray->AsInt32(0), 0); + do_check_eq(valueArray->AsInt64(0), 0); + do_check_eq(valueArray->AsDouble(0), 0.0); + len = 100; + do_check_eq(valueArray->AsSharedUTF8String(0, &len), (const char*)nullptr); + do_check_eq(len, 0); + len = 100; + do_check_eq(valueArray->AsSharedWString(0, &len), (const char16_t*)nullptr); + do_check_eq(len, 0); + len = 100; + do_check_eq(valueArray->AsSharedBlob(0, &len), (const uint8_t*)nullptr); + do_check_eq(len, 0); + do_check_eq(valueArray->IsNull(0), true); +} + +void +test_asyncNULLFallback() +{ + nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase()); + + nsCOMPtr<mozIStorageAsyncStatement> stmt; + (void)db->CreateAsyncStatement(NS_LITERAL_CSTRING( + "SELECT NULL" + ), getter_AddRefs(stmt)); + + nsCOMPtr<mozIStoragePendingStatement> pendingStmt; + do_check_true(NS_SUCCEEDED(stmt->ExecuteAsync(nullptr, getter_AddRefs(pendingStmt)))); + do_check_true(pendingStmt); + stmt->Finalize(); + RefPtr<Spinner> asyncSpin(new Spinner()); + db->AsyncClose(asyncSpin); + asyncSpin->SpinUntilCompleted(); + +} + +void (*gTests[])(void) = { + test_NULLFallback +, test_asyncNULLFallback +}; + +const char *file = __FILE__; +#define TEST_NAME "AsXXX helpers" +#define TEST_FILE file +#include "storage_test_harness_tail.h" diff --git a/storage/test/test_StatementCache.cpp b/storage/test/test_StatementCache.cpp new file mode 100644 index 000000000..731a858de --- /dev/null +++ b/storage/test/test_StatementCache.cpp @@ -0,0 +1,163 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "storage_test_harness.h" + +#include "mozilla/Attributes.h" +#include "mozilla/storage/StatementCache.h" +using namespace mozilla::storage; + +/** + * This file test our statement cache in StatementCache.h. + */ + +//////////////////////////////////////////////////////////////////////////////// +//// Helpers + +class SyncCache : public StatementCache<mozIStorageStatement> +{ +public: + explicit SyncCache(nsCOMPtr<mozIStorageConnection>& aConnection) + : StatementCache<mozIStorageStatement>(aConnection) + { + } +}; + +class AsyncCache : public StatementCache<mozIStorageAsyncStatement> +{ +public: + explicit AsyncCache(nsCOMPtr<mozIStorageConnection>& aConnection) + : StatementCache<mozIStorageAsyncStatement>(aConnection) + { + } +}; + +/** + * Wraps nsCString so we can not implement the same functions twice for each + * type. + */ +class StringWrapper : public nsCString +{ +public: + MOZ_IMPLICIT StringWrapper(const char* aOther) + { + this->Assign(aOther); + } +}; + +//////////////////////////////////////////////////////////////////////////////// +//// Test Functions + +template<typename StringType> +void +test_GetCachedStatement() +{ + nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase()); + SyncCache cache(db); + + StringType sql = "SELECT * FROM sqlite_master"; + + // Make sure we get a statement back with the right state. + nsCOMPtr<mozIStorageStatement> stmt = cache.GetCachedStatement(sql); + do_check_true(stmt); + int32_t state; + do_check_success(stmt->GetState(&state)); + do_check_eq(mozIStorageBaseStatement::MOZ_STORAGE_STATEMENT_READY, state); + + // Check to make sure we get the same copy the second time we ask. + nsCOMPtr<mozIStorageStatement> stmt2 = cache.GetCachedStatement(sql); + do_check_true(stmt2); + do_check_eq(stmt.get(), stmt2.get()); +} + +template <typename StringType> +void +test_FinalizeStatements() +{ + nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase()); + SyncCache cache(db); + + StringType sql = "SELECT * FROM sqlite_master"; + + // Get a statement, and then tell the cache to finalize. + nsCOMPtr<mozIStorageStatement> stmt = cache.GetCachedStatement(sql); + do_check_true(stmt); + + cache.FinalizeStatements(); + + // We should be in an invalid state at this point. + int32_t state; + do_check_success(stmt->GetState(&state)); + do_check_eq(mozIStorageBaseStatement::MOZ_STORAGE_STATEMENT_INVALID, state); + + // Should be able to close the database now too. + do_check_success(db->Close()); +} + +template<typename StringType> +void +test_GetCachedAsyncStatement() +{ + nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase()); + AsyncCache cache(db); + + StringType sql = "SELECT * FROM sqlite_master"; + + // Make sure we get a statement back with the right state. + nsCOMPtr<mozIStorageAsyncStatement> stmt = cache.GetCachedStatement(sql); + do_check_true(stmt); + int32_t state; + do_check_success(stmt->GetState(&state)); + do_check_eq(mozIStorageBaseStatement::MOZ_STORAGE_STATEMENT_READY, state); + + // Check to make sure we get the same copy the second time we ask. + nsCOMPtr<mozIStorageAsyncStatement> stmt2 = cache.GetCachedStatement(sql); + do_check_true(stmt2); + do_check_eq(stmt.get(), stmt2.get()); +} + +template <typename StringType> +void +test_FinalizeAsyncStatements() +{ + nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase()); + AsyncCache cache(db); + + StringType sql = "SELECT * FROM sqlite_master"; + + // Get a statement, and then tell the cache to finalize. + nsCOMPtr<mozIStorageAsyncStatement> stmt = cache.GetCachedStatement(sql); + do_check_true(stmt); + + cache.FinalizeStatements(); + + // We should be in an invalid state at this point. + int32_t state; + do_check_success(stmt->GetState(&state)); + do_check_eq(mozIStorageBaseStatement::MOZ_STORAGE_STATEMENT_INVALID, state); + + // Should be able to close the database now too. + do_check_success(db->AsyncClose(nullptr)); +} + +//////////////////////////////////////////////////////////////////////////////// +//// Test Harness Stuff + +void (*gTests[])(void) = { + test_GetCachedStatement<const char []>, + test_GetCachedStatement<StringWrapper>, + test_FinalizeStatements<const char []>, + test_FinalizeStatements<StringWrapper>, + test_GetCachedAsyncStatement<const char []>, + test_GetCachedAsyncStatement<StringWrapper>, + test_FinalizeAsyncStatements<const char []>, + test_FinalizeAsyncStatements<StringWrapper>, +}; + +const char *file = __FILE__; +#define TEST_NAME "StatementCache" +#define TEST_FILE file +#include "storage_test_harness_tail.h" diff --git a/storage/test/test_asyncStatementExecution_transaction.cpp b/storage/test/test_asyncStatementExecution_transaction.cpp new file mode 100644 index 000000000..7e94bd80e --- /dev/null +++ b/storage/test/test_asyncStatementExecution_transaction.cpp @@ -0,0 +1,509 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +#include "storage_test_harness.h" + +#include "nsIEventTarget.h" +#include "mozStorageConnection.h" + +#include "sqlite3.h" + +using namespace mozilla; +using namespace mozilla::storage; + +//////////////////////////////////////////////////////////////////////////////// +//// Helpers + +/** + * Commit hook to detect transactions. + * + * @param aArg + * An integer pointer that will be incremented for each commit. + */ +int commit_hook(void *aArg) +{ + int *arg = static_cast<int *>(aArg); + (*arg)++; + return 0; +} + +/** + * Executes the passed-in statements and checks if a transaction is created. + * When done statements are finalized and database connection is closed. + * + * @param aDB + * The database connection. + * @param aStmts + * Vector of statements. + * @param aStmtsLen + * Number of statements. + * @param aTransactionExpected + * Whether a transaction is expected or not. + */ +void +check_transaction(mozIStorageConnection *aDB, + mozIStorageBaseStatement **aStmts, + uint32_t aStmtsLen, + bool aTransactionExpected) +{ + // -- install a transaction commit hook. + int commit = 0; + static_cast<Connection *>(aDB)->setCommitHook(commit_hook, &commit); + + RefPtr<AsyncStatementSpinner> asyncSpin(new AsyncStatementSpinner()); + nsCOMPtr<mozIStoragePendingStatement> asyncPend; + do_check_success(aDB->ExecuteAsync(aStmts, aStmtsLen, asyncSpin, + getter_AddRefs(asyncPend))); + do_check_true(asyncPend); + + // -- complete the execution + asyncSpin->SpinUntilCompleted(); + + // -- uninstall the transaction commit hook. + static_cast<Connection *>(aDB)->setCommitHook(nullptr); + + // -- check transaction + do_check_eq(aTransactionExpected, !!commit); + + // -- check that only one transaction was created. + if (aTransactionExpected) { + do_check_eq(1, commit); + } + + // -- cleanup + for (uint32_t i = 0; i < aStmtsLen; ++i) { + aStmts[i]->Finalize(); + } + blocking_async_close(aDB); +} + +//////////////////////////////////////////////////////////////////////////////// +//// Tests + +/** + * Test that executing multiple readonly AsyncStatements doesn't create a + * transaction. + */ +void +test_MultipleAsyncReadStatements() +{ + nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase()); + + // -- create statements and execute them + nsCOMPtr<mozIStorageAsyncStatement> stmt1; + db->CreateAsyncStatement(NS_LITERAL_CSTRING( + "SELECT * FROM sqlite_master" + ), getter_AddRefs(stmt1)); + + nsCOMPtr<mozIStorageAsyncStatement> stmt2; + db->CreateAsyncStatement(NS_LITERAL_CSTRING( + "SELECT * FROM sqlite_master" + ), getter_AddRefs(stmt2)); + + mozIStorageBaseStatement *stmts[] = { + stmt1, + stmt2, + }; + + check_transaction(db, stmts, ArrayLength(stmts), false); +} + +/** + * Test that executing multiple readonly Statements doesn't create a + * transaction. + */ +void +test_MultipleReadStatements() +{ + nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase()); + + // -- create statements and execute them + nsCOMPtr<mozIStorageStatement> stmt1; + db->CreateStatement(NS_LITERAL_CSTRING( + "SELECT * FROM sqlite_master" + ), getter_AddRefs(stmt1)); + + nsCOMPtr<mozIStorageStatement> stmt2; + db->CreateStatement(NS_LITERAL_CSTRING( + "SELECT * FROM sqlite_master" + ), getter_AddRefs(stmt2)); + + mozIStorageBaseStatement *stmts[] = { + stmt1, + stmt2, + }; + + check_transaction(db, stmts, ArrayLength(stmts), false); +} + +/** + * Test that executing multiple AsyncStatements causing writes creates a + * transaction. + */ +void +test_MultipleAsyncReadWriteStatements() +{ + nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase()); + + // -- create statements and execute them + nsCOMPtr<mozIStorageAsyncStatement> stmt1; + db->CreateAsyncStatement(NS_LITERAL_CSTRING( + "SELECT * FROM sqlite_master" + ), getter_AddRefs(stmt1)); + + nsCOMPtr<mozIStorageAsyncStatement> stmt2; + db->CreateAsyncStatement(NS_LITERAL_CSTRING( + "CREATE TABLE test (id INTEGER PRIMARY KEY)" + ), getter_AddRefs(stmt2)); + + mozIStorageBaseStatement *stmts[] = { + stmt1, + stmt2, + }; + + check_transaction(db, stmts, ArrayLength(stmts), true); +} + +/** + * Test that executing multiple Statements causing writes creates a transaction. + */ +void +test_MultipleReadWriteStatements() +{ + nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase()); + + // -- create statements and execute them + nsCOMPtr<mozIStorageStatement> stmt1; + db->CreateStatement(NS_LITERAL_CSTRING( + "SELECT * FROM sqlite_master" + ), getter_AddRefs(stmt1)); + + nsCOMPtr<mozIStorageStatement> stmt2; + db->CreateStatement(NS_LITERAL_CSTRING( + "CREATE TABLE test (id INTEGER PRIMARY KEY)" + ), getter_AddRefs(stmt2)); + + mozIStorageBaseStatement *stmts[] = { + stmt1, + stmt2, + }; + + check_transaction(db, stmts, ArrayLength(stmts), true); +} + +/** + * Test that executing multiple AsyncStatements causing writes creates a + * single transaction. + */ +void +test_MultipleAsyncWriteStatements() +{ + nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase()); + + // -- create statements and execute them + nsCOMPtr<mozIStorageAsyncStatement> stmt1; + db->CreateAsyncStatement(NS_LITERAL_CSTRING( + "CREATE TABLE test1 (id INTEGER PRIMARY KEY)" + ), getter_AddRefs(stmt1)); + + nsCOMPtr<mozIStorageAsyncStatement> stmt2; + db->CreateAsyncStatement(NS_LITERAL_CSTRING( + "CREATE TABLE test2 (id INTEGER PRIMARY KEY)" + ), getter_AddRefs(stmt2)); + + mozIStorageBaseStatement *stmts[] = { + stmt1, + stmt2, + }; + + check_transaction(db, stmts, ArrayLength(stmts), true); +} + +/** + * Test that executing multiple Statements causing writes creates a + * single transaction. + */ +void +test_MultipleWriteStatements() +{ + nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase()); + + // -- create statements and execute them + nsCOMPtr<mozIStorageStatement> stmt1; + db->CreateStatement(NS_LITERAL_CSTRING( + "CREATE TABLE test1 (id INTEGER PRIMARY KEY)" + ), getter_AddRefs(stmt1)); + + nsCOMPtr<mozIStorageStatement> stmt2; + db->CreateStatement(NS_LITERAL_CSTRING( + "CREATE TABLE test2 (id INTEGER PRIMARY KEY)" + ), getter_AddRefs(stmt2)); + + mozIStorageBaseStatement *stmts[] = { + stmt1, + stmt2, + }; + + check_transaction(db, stmts, ArrayLength(stmts), true); +} + +/** + * Test that executing a single read-only AsyncStatement doesn't create a + * transaction. + */ +void +test_SingleAsyncReadStatement() +{ + nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase()); + + // -- create statements and execute them + nsCOMPtr<mozIStorageAsyncStatement> stmt; + db->CreateAsyncStatement(NS_LITERAL_CSTRING( + "SELECT * FROM sqlite_master" + ), getter_AddRefs(stmt)); + + mozIStorageBaseStatement *stmts[] = { + stmt, + }; + + check_transaction(db, stmts, ArrayLength(stmts), false); +} + +/** + * Test that executing a single read-only Statement doesn't create a + * transaction. + */ +void +test_SingleReadStatement() +{ + nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase()); + + // -- create statements and execute them + nsCOMPtr<mozIStorageStatement> stmt; + db->CreateStatement(NS_LITERAL_CSTRING( + "SELECT * FROM sqlite_master" + ), getter_AddRefs(stmt)); + + mozIStorageBaseStatement *stmts[] = { + stmt, + }; + + check_transaction(db, stmts, ArrayLength(stmts), false); +} + +/** + * Test that executing a single AsyncStatement causing writes creates a + * transaction. + */ +void +test_SingleAsyncWriteStatement() +{ + nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase()); + + // -- create statements and execute them + nsCOMPtr<mozIStorageAsyncStatement> stmt; + db->CreateAsyncStatement(NS_LITERAL_CSTRING( + "CREATE TABLE test (id INTEGER PRIMARY KEY)" + ), getter_AddRefs(stmt)); + + mozIStorageBaseStatement *stmts[] = { + stmt, + }; + + check_transaction(db, stmts, ArrayLength(stmts), true); +} + +/** + * Test that executing a single Statement causing writes creates a transaction. + */ +void +test_SingleWriteStatement() +{ + nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase()); + + // -- create statements and execute them + nsCOMPtr<mozIStorageStatement> stmt; + db->CreateStatement(NS_LITERAL_CSTRING( + "CREATE TABLE test (id INTEGER PRIMARY KEY)" + ), getter_AddRefs(stmt)); + + mozIStorageBaseStatement *stmts[] = { + stmt, + }; + + check_transaction(db, stmts, ArrayLength(stmts), true); +} + +/** + * Test that executing a single read-only AsyncStatement with multiple params + * doesn't create a transaction. + */ +void +test_MultipleParamsAsyncReadStatement() +{ + nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase()); + + // -- create statements and execute them + nsCOMPtr<mozIStorageAsyncStatement> stmt; + db->CreateAsyncStatement(NS_LITERAL_CSTRING( + "SELECT :param FROM sqlite_master" + ), getter_AddRefs(stmt)); + + // -- bind multiple BindingParams + nsCOMPtr<mozIStorageBindingParamsArray> paramsArray; + stmt->NewBindingParamsArray(getter_AddRefs(paramsArray)); + for (int32_t i = 0; i < 2; i++) { + nsCOMPtr<mozIStorageBindingParams> params; + paramsArray->NewBindingParams(getter_AddRefs(params)); + params->BindInt32ByName(NS_LITERAL_CSTRING("param"), 1); + paramsArray->AddParams(params); + } + stmt->BindParameters(paramsArray); + paramsArray = nullptr; + + mozIStorageBaseStatement *stmts[] = { + stmt, + }; + + check_transaction(db, stmts, ArrayLength(stmts), false); +} + +/** + * Test that executing a single read-only Statement with multiple params + * doesn't create a transaction. + */ +void +test_MultipleParamsReadStatement() +{ + nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase()); + + // -- create statements and execute them + nsCOMPtr<mozIStorageStatement> stmt; + db->CreateStatement(NS_LITERAL_CSTRING( + "SELECT :param FROM sqlite_master" + ), getter_AddRefs(stmt)); + + // -- bind multiple BindingParams + nsCOMPtr<mozIStorageBindingParamsArray> paramsArray; + stmt->NewBindingParamsArray(getter_AddRefs(paramsArray)); + for (int32_t i = 0; i < 2; i++) { + nsCOMPtr<mozIStorageBindingParams> params; + paramsArray->NewBindingParams(getter_AddRefs(params)); + params->BindInt32ByName(NS_LITERAL_CSTRING("param"), 1); + paramsArray->AddParams(params); + } + stmt->BindParameters(paramsArray); + paramsArray = nullptr; + + mozIStorageBaseStatement *stmts[] = { + stmt, + }; + + check_transaction(db, stmts, ArrayLength(stmts), false); +} + +/** + * Test that executing a single write AsyncStatement with multiple params + * creates a transaction. + */ +void +test_MultipleParamsAsyncWriteStatement() +{ + nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase()); + + // -- create a table for writes + nsCOMPtr<mozIStorageStatement> tableStmt; + db->CreateStatement(NS_LITERAL_CSTRING( + "CREATE TABLE test (id INTEGER PRIMARY KEY)" + ), getter_AddRefs(tableStmt)); + tableStmt->Execute(); + tableStmt->Finalize(); + + // -- create statements and execute them + nsCOMPtr<mozIStorageAsyncStatement> stmt; + db->CreateAsyncStatement(NS_LITERAL_CSTRING( + "DELETE FROM test WHERE id = :param" + ), getter_AddRefs(stmt)); + + // -- bind multiple BindingParams + nsCOMPtr<mozIStorageBindingParamsArray> paramsArray; + stmt->NewBindingParamsArray(getter_AddRefs(paramsArray)); + for (int32_t i = 0; i < 2; i++) { + nsCOMPtr<mozIStorageBindingParams> params; + paramsArray->NewBindingParams(getter_AddRefs(params)); + params->BindInt32ByName(NS_LITERAL_CSTRING("param"), 1); + paramsArray->AddParams(params); + } + stmt->BindParameters(paramsArray); + paramsArray = nullptr; + + mozIStorageBaseStatement *stmts[] = { + stmt, + }; + + check_transaction(db, stmts, ArrayLength(stmts), true); +} + +/** + * Test that executing a single write Statement with multiple params + * creates a transaction. + */ +void +test_MultipleParamsWriteStatement() +{ + nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase()); + + // -- create a table for writes + nsCOMPtr<mozIStorageStatement> tableStmt; + db->CreateStatement(NS_LITERAL_CSTRING( + "CREATE TABLE test (id INTEGER PRIMARY KEY)" + ), getter_AddRefs(tableStmt)); + tableStmt->Execute(); + tableStmt->Finalize(); + + // -- create statements and execute them + nsCOMPtr<mozIStorageStatement> stmt; + db->CreateStatement(NS_LITERAL_CSTRING( + "DELETE FROM test WHERE id = :param" + ), getter_AddRefs(stmt)); + + // -- bind multiple BindingParams + nsCOMPtr<mozIStorageBindingParamsArray> paramsArray; + stmt->NewBindingParamsArray(getter_AddRefs(paramsArray)); + for (int32_t i = 0; i < 2; i++) { + nsCOMPtr<mozIStorageBindingParams> params; + paramsArray->NewBindingParams(getter_AddRefs(params)); + params->BindInt32ByName(NS_LITERAL_CSTRING("param"), 1); + paramsArray->AddParams(params); + } + stmt->BindParameters(paramsArray); + paramsArray = nullptr; + + mozIStorageBaseStatement *stmts[] = { + stmt, + }; + + check_transaction(db, stmts, ArrayLength(stmts), true); +} + +void (*gTests[])(void) = { + test_MultipleAsyncReadStatements, + test_MultipleReadStatements, + test_MultipleAsyncReadWriteStatements, + test_MultipleReadWriteStatements, + test_MultipleAsyncWriteStatements, + test_MultipleWriteStatements, + test_SingleAsyncReadStatement, + test_SingleReadStatement, + test_SingleAsyncWriteStatement, + test_SingleWriteStatement, + test_MultipleParamsAsyncReadStatement, + test_MultipleParamsReadStatement, + test_MultipleParamsAsyncWriteStatement, + test_MultipleParamsWriteStatement, +}; + +const char *file = __FILE__; +#define TEST_NAME "async statement execution transaction" +#define TEST_FILE file +#include "storage_test_harness_tail.h" diff --git a/storage/test/test_async_callbacks_with_spun_event_loops.cpp b/storage/test/test_async_callbacks_with_spun_event_loops.cpp new file mode 100644 index 000000000..75687c7f3 --- /dev/null +++ b/storage/test/test_async_callbacks_with_spun_event_loops.cpp @@ -0,0 +1,174 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +#include "storage_test_harness.h" +#include "prthread.h" +#include "nsIEventTarget.h" +#include "nsIInterfaceRequestorUtils.h" +#include "mozilla/Attributes.h" + +#include "sqlite3.h" + +//////////////////////////////////////////////////////////////////////////////// +//// Async Helpers + +/** + * Spins the events loop for current thread until aCondition is true. + */ +void +spin_events_loop_until_true(const bool* const aCondition) +{ + nsCOMPtr<nsIThread> thread(::do_GetCurrentThread()); + nsresult rv = NS_OK; + bool processed = true; + while (!(*aCondition) && NS_SUCCEEDED(rv)) { + rv = thread->ProcessNextEvent(true, &processed); + } +} + +//////////////////////////////////////////////////////////////////////////////// +//// mozIStorageStatementCallback implementation + +class UnownedCallback final : public mozIStorageStatementCallback +{ +public: + NS_DECL_ISUPPORTS + + // Whether the object has been destroyed. + static bool sAlive; + // Whether the first result was received. + static bool sResult; + // Whether an error was received. + static bool sError; + + explicit UnownedCallback(mozIStorageConnection* aDBConn) + : mDBConn(aDBConn) + , mCompleted(false) + { + sAlive = true; + sResult = false; + sError = false; + } + +private: + ~UnownedCallback() + { + sAlive = false; + blocking_async_close(mDBConn); + } + +public: + NS_IMETHOD HandleResult(mozIStorageResultSet* aResultSet) override + { + sResult = true; + spin_events_loop_until_true(&mCompleted); + if (!sAlive) { + NS_RUNTIMEABORT("The statement callback was destroyed prematurely."); + } + return NS_OK; + } + + NS_IMETHOD HandleError(mozIStorageError* aError) override + { + sError = true; + spin_events_loop_until_true(&mCompleted); + if (!sAlive) { + NS_RUNTIMEABORT("The statement callback was destroyed prematurely."); + } + return NS_OK; + } + + NS_IMETHOD HandleCompletion(uint16_t aReason) override + { + mCompleted = true; + return NS_OK; + } + +protected: + nsCOMPtr<mozIStorageConnection> mDBConn; + bool mCompleted; +}; + +NS_IMPL_ISUPPORTS(UnownedCallback, mozIStorageStatementCallback) + +bool UnownedCallback::sAlive = false; +bool UnownedCallback::sResult = false; +bool UnownedCallback::sError = false; + +//////////////////////////////////////////////////////////////////////////////// +//// Tests + +void +test_SpinEventsLoopInHandleResult() +{ + nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase()); + + // Create a test table and populate it. + nsCOMPtr<mozIStorageStatement> stmt; + db->CreateStatement(NS_LITERAL_CSTRING( + "CREATE TABLE test (id INTEGER PRIMARY KEY)" + ), getter_AddRefs(stmt)); + stmt->Execute(); + stmt->Finalize(); + + db->CreateStatement(NS_LITERAL_CSTRING( + "INSERT INTO test (id) VALUES (?)" + ), getter_AddRefs(stmt)); + for (int32_t i = 0; i < 30; ++i) { + stmt->BindInt32ByIndex(0, i); + stmt->Execute(); + stmt->Reset(); + } + stmt->Finalize(); + + db->CreateStatement(NS_LITERAL_CSTRING( + "SELECT * FROM test" + ), getter_AddRefs(stmt)); + nsCOMPtr<mozIStoragePendingStatement> ps; + do_check_success(stmt->ExecuteAsync(new UnownedCallback(db), + getter_AddRefs(ps))); + stmt->Finalize(); + + spin_events_loop_until_true(&UnownedCallback::sResult); +} + +void +test_SpinEventsLoopInHandleError() +{ + nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase()); + + // Create a test table and populate it. + nsCOMPtr<mozIStorageStatement> stmt; + db->CreateStatement(NS_LITERAL_CSTRING( + "CREATE TABLE test (id INTEGER PRIMARY KEY)" + ), getter_AddRefs(stmt)); + stmt->Execute(); + stmt->Finalize(); + + db->CreateStatement(NS_LITERAL_CSTRING( + "INSERT INTO test (id) VALUES (1)" + ), getter_AddRefs(stmt)); + stmt->Execute(); + stmt->Finalize(); + + // This will cause a constraint error. + db->CreateStatement(NS_LITERAL_CSTRING( + "INSERT INTO test (id) VALUES (1)" + ), getter_AddRefs(stmt)); + nsCOMPtr<mozIStoragePendingStatement> ps; + do_check_success(stmt->ExecuteAsync(new UnownedCallback(db), + getter_AddRefs(ps))); + stmt->Finalize(); + + spin_events_loop_until_true(&UnownedCallback::sError); +} + +void (*gTests[])(void) = { + test_SpinEventsLoopInHandleResult, + test_SpinEventsLoopInHandleError, +}; + +const char *file = __FILE__; +#define TEST_NAME "test async callbacks with spun event loops" +#define TEST_FILE file +#include "storage_test_harness_tail.h" diff --git a/storage/test/test_binding_params.cpp b/storage/test/test_binding_params.cpp new file mode 100644 index 000000000..e5af6009d --- /dev/null +++ b/storage/test/test_binding_params.cpp @@ -0,0 +1,215 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "storage_test_harness.h" + +#include "mozStorageHelper.h" + +using namespace mozilla; + +/** + * This file tests binding and reading out string parameters through the + * mozIStorageStatement API. + */ + +void +test_ASCIIString() +{ + nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase()); + + // Create table with a single string column. + (void)db->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "CREATE TABLE test (str STRING)" + )); + + // Create statements to INSERT and SELECT the string. + nsCOMPtr<mozIStorageStatement> insert, select; + (void)db->CreateStatement(NS_LITERAL_CSTRING( + "INSERT INTO test (str) VALUES (?1)" + ), getter_AddRefs(insert)); + (void)db->CreateStatement(NS_LITERAL_CSTRING( + "SELECT str FROM test" + ), getter_AddRefs(select)); + + // Roundtrip a string through the table, and ensure it comes out as expected. + nsAutoCString inserted("I'm an ASCII string"); + { + mozStorageStatementScoper scoper(insert); + bool hasResult; + do_check_true(NS_SUCCEEDED(insert->BindUTF8StringByIndex(0, inserted))); + do_check_true(NS_SUCCEEDED(insert->ExecuteStep(&hasResult))); + do_check_false(hasResult); + } + + nsAutoCString result; + { + mozStorageStatementScoper scoper(select); + bool hasResult; + do_check_true(NS_SUCCEEDED(select->ExecuteStep(&hasResult))); + do_check_true(hasResult); + do_check_true(NS_SUCCEEDED(select->GetUTF8String(0, result))); + } + + do_check_true(result == inserted); + + (void)db->ExecuteSimpleSQL(NS_LITERAL_CSTRING("DELETE FROM test")); +} + +void +test_CString() +{ + nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase()); + + // Create table with a single string column. + (void)db->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "CREATE TABLE test (str STRING)" + )); + + // Create statements to INSERT and SELECT the string. + nsCOMPtr<mozIStorageStatement> insert, select; + (void)db->CreateStatement(NS_LITERAL_CSTRING( + "INSERT INTO test (str) VALUES (?1)" + ), getter_AddRefs(insert)); + (void)db->CreateStatement(NS_LITERAL_CSTRING( + "SELECT str FROM test" + ), getter_AddRefs(select)); + + // Roundtrip a string through the table, and ensure it comes out as expected. + static const char sCharArray[] = + "I'm not a \xff\x00\xac\xde\xbb ASCII string!"; + nsAutoCString inserted(sCharArray, ArrayLength(sCharArray) - 1); + do_check_true(inserted.Length() == ArrayLength(sCharArray) - 1); + { + mozStorageStatementScoper scoper(insert); + bool hasResult; + do_check_true(NS_SUCCEEDED(insert->BindUTF8StringByIndex(0, inserted))); + do_check_true(NS_SUCCEEDED(insert->ExecuteStep(&hasResult))); + do_check_false(hasResult); + } + + { + nsAutoCString result; + + mozStorageStatementScoper scoper(select); + bool hasResult; + do_check_true(NS_SUCCEEDED(select->ExecuteStep(&hasResult))); + do_check_true(hasResult); + do_check_true(NS_SUCCEEDED(select->GetUTF8String(0, result))); + + do_check_true(result == inserted); + } + + (void)db->ExecuteSimpleSQL(NS_LITERAL_CSTRING("DELETE FROM test")); +} + +void +test_UTFStrings() +{ + nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase()); + + // Create table with a single string column. + (void)db->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "CREATE TABLE test (str STRING)" + )); + + // Create statements to INSERT and SELECT the string. + nsCOMPtr<mozIStorageStatement> insert, select; + (void)db->CreateStatement(NS_LITERAL_CSTRING( + "INSERT INTO test (str) VALUES (?1)" + ), getter_AddRefs(insert)); + (void)db->CreateStatement(NS_LITERAL_CSTRING( + "SELECT str FROM test" + ), getter_AddRefs(select)); + + // Roundtrip a UTF8 string through the table, using UTF8 input and output. + static const char sCharArray[] = + "I'm a \xc3\xbb\xc3\xbc\xc3\xa2\xc3\xa4\xc3\xa7 UTF8 string!"; + nsAutoCString insertedUTF8(sCharArray, ArrayLength(sCharArray) - 1); + do_check_true(insertedUTF8.Length() == ArrayLength(sCharArray) - 1); + NS_ConvertUTF8toUTF16 insertedUTF16(insertedUTF8); + do_check_true(insertedUTF8 == NS_ConvertUTF16toUTF8(insertedUTF16)); + { + mozStorageStatementScoper scoper(insert); + bool hasResult; + do_check_true(NS_SUCCEEDED(insert->BindUTF8StringByIndex(0, insertedUTF8))); + do_check_true(NS_SUCCEEDED(insert->ExecuteStep(&hasResult))); + do_check_false(hasResult); + } + + { + nsAutoCString result; + + mozStorageStatementScoper scoper(select); + bool hasResult; + do_check_true(NS_SUCCEEDED(select->ExecuteStep(&hasResult))); + do_check_true(hasResult); + do_check_true(NS_SUCCEEDED(select->GetUTF8String(0, result))); + + do_check_true(result == insertedUTF8); + } + + // Use UTF8 input and UTF16 output. + { + nsAutoString result; + + mozStorageStatementScoper scoper(select); + bool hasResult; + do_check_true(NS_SUCCEEDED(select->ExecuteStep(&hasResult))); + do_check_true(hasResult); + do_check_true(NS_SUCCEEDED(select->GetString(0, result))); + + do_check_true(result == insertedUTF16); + } + + (void)db->ExecuteSimpleSQL(NS_LITERAL_CSTRING("DELETE FROM test")); + + // Roundtrip the same string using UTF16 input and UTF8 output. + { + mozStorageStatementScoper scoper(insert); + bool hasResult; + do_check_true(NS_SUCCEEDED(insert->BindStringByIndex(0, insertedUTF16))); + do_check_true(NS_SUCCEEDED(insert->ExecuteStep(&hasResult))); + do_check_false(hasResult); + } + + { + nsAutoCString result; + + mozStorageStatementScoper scoper(select); + bool hasResult; + do_check_true(NS_SUCCEEDED(select->ExecuteStep(&hasResult))); + do_check_true(hasResult); + do_check_true(NS_SUCCEEDED(select->GetUTF8String(0, result))); + + do_check_true(result == insertedUTF8); + } + + // Use UTF16 input and UTF16 output. + { + nsAutoString result; + + mozStorageStatementScoper scoper(select); + bool hasResult; + do_check_true(NS_SUCCEEDED(select->ExecuteStep(&hasResult))); + do_check_true(hasResult); + do_check_true(NS_SUCCEEDED(select->GetString(0, result))); + + do_check_true(result == insertedUTF16); + } + + (void)db->ExecuteSimpleSQL(NS_LITERAL_CSTRING("DELETE FROM test")); +} + +void (*gTests[])(void) = { + test_ASCIIString, + test_CString, + test_UTFStrings, +}; + +const char *file = __FILE__; +#define TEST_NAME "binding string params" +#define TEST_FILE file +#include "storage_test_harness_tail.h" diff --git a/storage/test/test_deadlock_detector.cpp b/storage/test/test_deadlock_detector.cpp new file mode 100644 index 000000000..1cbd79467 --- /dev/null +++ b/storage/test/test_deadlock_detector.cpp @@ -0,0 +1,602 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- + * vim: sw=4 ts=4 et : + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Note: This file is a copy of xpcom/tests/TestDeadlockDetector.cpp, but all + * mutexes were turned into SQLiteMutexes. + */ + +#include "prenv.h" +#include "prerror.h" +#include "prio.h" +#include "prproces.h" + +#include "nsMemory.h" + +#include "mozilla/CondVar.h" +#include "mozilla/ReentrantMonitor.h" +#include "SQLiteMutex.h" + +#include "TestHarness.h" + +using namespace mozilla; + +/** + * Helper class to allocate a sqlite3_mutex for our SQLiteMutex. Also makes + * keeping the test files in sync easier. + */ +class TestMutex : public mozilla::storage::SQLiteMutex +{ +public: + explicit TestMutex(const char* aName) + : mozilla::storage::SQLiteMutex(aName) + , mInner(sqlite3_mutex_alloc(SQLITE_MUTEX_FAST)) + { + NS_ASSERTION(mInner, "could not allocate a sqlite3_mutex"); + initWithMutex(mInner); + } + + ~TestMutex() + { + sqlite3_mutex_free(mInner); + } + + void Lock() + { + lock(); + } + + void Unlock() + { + unlock(); + } +private: + sqlite3_mutex *mInner; +}; + +static PRThread* +spawn(void (*run)(void*), void* arg) +{ + return PR_CreateThread(PR_SYSTEM_THREAD, + run, + arg, + PR_PRIORITY_NORMAL, + PR_GLOBAL_THREAD, + PR_JOINABLE_THREAD, + 0); +} + +#define PASS() \ + do { \ + passed(__FUNCTION__); \ + return NS_OK; \ + } while (0) + +#define FAIL(why) \ + do { \ + fail("%s | %s - %s", __FILE__, __FUNCTION__, why); \ + return NS_ERROR_FAILURE; \ + } while (0) + +//----------------------------------------------------------------------------- + +static const char* sPathToThisBinary; +static const char* sAssertBehaviorEnv = "XPCOM_DEBUG_BREAK=abort"; + +class Subprocess +{ +public: + // not available until process finishes + int32_t mExitCode; + nsCString mStdout; + nsCString mStderr; + + explicit Subprocess(const char* aTestName) { + // set up stdio redirection + PRFileDesc* readStdin; PRFileDesc* writeStdin; + PRFileDesc* readStdout; PRFileDesc* writeStdout; + PRFileDesc* readStderr; PRFileDesc* writeStderr; + PRProcessAttr* pattr = PR_NewProcessAttr(); + + NS_ASSERTION(pattr, "couldn't allocate process attrs"); + + NS_ASSERTION(PR_SUCCESS == PR_CreatePipe(&readStdin, &writeStdin), + "couldn't create child stdin pipe"); + NS_ASSERTION(PR_SUCCESS == PR_SetFDInheritable(readStdin, true), + "couldn't set child stdin inheritable"); + PR_ProcessAttrSetStdioRedirect(pattr, PR_StandardInput, readStdin); + + NS_ASSERTION(PR_SUCCESS == PR_CreatePipe(&readStdout, &writeStdout), + "couldn't create child stdout pipe"); + NS_ASSERTION(PR_SUCCESS == PR_SetFDInheritable(writeStdout, true), + "couldn't set child stdout inheritable"); + PR_ProcessAttrSetStdioRedirect(pattr, PR_StandardOutput, writeStdout); + + NS_ASSERTION(PR_SUCCESS == PR_CreatePipe(&readStderr, &writeStderr), + "couldn't create child stderr pipe"); + NS_ASSERTION(PR_SUCCESS == PR_SetFDInheritable(writeStderr, true), + "couldn't set child stderr inheritable"); + PR_ProcessAttrSetStdioRedirect(pattr, PR_StandardError, writeStderr); + + // set up argv with test name to run + char* const newArgv[3] = { + strdup(sPathToThisBinary), + strdup(aTestName), + 0 + }; + + // make sure the child will abort if an assertion fails + NS_ASSERTION(PR_SUCCESS == PR_SetEnv(sAssertBehaviorEnv), + "couldn't set XPCOM_DEBUG_BREAK env var"); + + PRProcess* proc; + NS_ASSERTION(proc = PR_CreateProcess(sPathToThisBinary, + newArgv, + 0, // inherit environment + pattr), + "couldn't create process"); + PR_Close(readStdin); + PR_Close(writeStdout); + PR_Close(writeStderr); + + mProc = proc; + mStdinfd = writeStdin; + mStdoutfd = readStdout; + mStderrfd = readStderr; + + free(newArgv[0]); + free(newArgv[1]); + PR_DestroyProcessAttr(pattr); + } + + void RunToCompletion(uint32_t aWaitMs) + { + PR_Close(mStdinfd); + + PRPollDesc pollfds[2]; + int32_t nfds; + bool stdoutOpen = true, stderrOpen = true; + char buf[4096]; + + PRIntervalTime now = PR_IntervalNow(); + PRIntervalTime deadline = now + PR_MillisecondsToInterval(aWaitMs); + + while ((stdoutOpen || stderrOpen) && now < deadline) { + nfds = 0; + if (stdoutOpen) { + pollfds[nfds].fd = mStdoutfd; + pollfds[nfds].in_flags = PR_POLL_READ; + pollfds[nfds].out_flags = 0; + ++nfds; + } + if (stderrOpen) { + pollfds[nfds].fd = mStderrfd; + pollfds[nfds].in_flags = PR_POLL_READ; + pollfds[nfds].out_flags = 0; + ++nfds; + } + + int32_t rv = PR_Poll(pollfds, nfds, deadline - now); + NS_ASSERTION(0 <= rv, PR_ErrorToName(PR_GetError())); + + if (0 == rv) { // timeout + fputs("(timed out!)\n", stderr); + Finish(false); // abnormal + return; + } + + for (int32_t i = 0; i < nfds; ++i) { + if (!pollfds[i].out_flags) + continue; + + bool isStdout = mStdoutfd == pollfds[i].fd; + int32_t len = 0; + + if (PR_POLL_READ & pollfds[i].out_flags) { + len = PR_Read(pollfds[i].fd, buf, sizeof(buf) - 1); + NS_ASSERTION(0 <= len, PR_ErrorToName(PR_GetError())); + } + else if (!(PR_POLL_HUP & pollfds[i].out_flags)) { + NS_ERROR(PR_ErrorToName(PR_GetError())); + } + + if (0 < len) { + buf[len] = '\0'; + if (isStdout) + mStdout += buf; + else + mStderr += buf; + } + else if (isStdout) { + stdoutOpen = false; + } + else { + stderrOpen = false; + } + } + + now = PR_IntervalNow(); + } + + if (stdoutOpen) + fputs("(stdout still open!)\n", stderr); + if (stderrOpen) + fputs("(stderr still open!)\n", stderr); + if (now > deadline) + fputs("(timed out!)\n", stderr); + + Finish(!stdoutOpen && !stderrOpen && now <= deadline); + } + +private: + void Finish(bool normalExit) { + if (!normalExit) { + PR_KillProcess(mProc); + mExitCode = -1; + int32_t dummy; + PR_WaitProcess(mProc, &dummy); + } + else { + PR_WaitProcess(mProc, &mExitCode); // this had better not block ... + } + + PR_Close(mStdoutfd); + PR_Close(mStderrfd); + } + + PRProcess* mProc; + PRFileDesc* mStdinfd; // writeable + PRFileDesc* mStdoutfd; // readable + PRFileDesc* mStderrfd; // readable +}; + +//----------------------------------------------------------------------------- +// Harness for checking detector errors +bool +CheckForDeadlock(const char* test, const char* const* findTokens) +{ + Subprocess proc(test); + proc.RunToCompletion(5000); + + if (0 == proc.mExitCode) + return false; + + int32_t idx = 0; + for (const char* const* tp = findTokens; *tp; ++tp) { + const char* const token = *tp; +#ifdef MOZILLA_INTERNAL_API + idx = proc.mStderr.Find(token, false, idx); +#else + nsCString tokenCString(token); + idx = proc.mStderr.Find(tokenCString, idx); +#endif + if (-1 == idx) { + printf("(missed token '%s' in output)\n", token); + puts("----------------------------------\n"); + puts(proc.mStderr.get()); + puts("----------------------------------\n"); + return false; + } + idx += strlen(token); + } + + return true; +} + +//----------------------------------------------------------------------------- +// Single-threaded sanity tests + +// Stupidest possible deadlock. +int +Sanity_Child() +{ + TestMutex m1("dd.sanity.m1"); + m1.Lock(); + m1.Lock(); + return 0; // not reached +} + +nsresult +Sanity() +{ + const char* const tokens[] = { + "###!!! ERROR: Potential deadlock detected", + "=== Cyclical dependency starts at\n--- Mutex : dd.sanity.m1", + "=== Cycle completed at\n--- Mutex : dd.sanity.m1", + "###!!! Deadlock may happen NOW!", // better catch these easy cases... + "###!!! ASSERTION: Potential deadlock detected", + 0 + }; + if (CheckForDeadlock("Sanity", tokens)) { + PASS(); + } else { + FAIL("deadlock not detected"); + } +} + +// Slightly less stupid deadlock. +int +Sanity2_Child() +{ + TestMutex m1("dd.sanity2.m1"); + TestMutex m2("dd.sanity2.m2"); + m1.Lock(); + m2.Lock(); + m1.Lock(); + return 0; // not reached +} + +nsresult +Sanity2() +{ + const char* const tokens[] = { + "###!!! ERROR: Potential deadlock detected", + "=== Cyclical dependency starts at\n--- Mutex : dd.sanity2.m1", + "--- Next dependency:\n--- Mutex : dd.sanity2.m2", + "=== Cycle completed at\n--- Mutex : dd.sanity2.m1", + "###!!! Deadlock may happen NOW!", // better catch these easy cases... + "###!!! ASSERTION: Potential deadlock detected", + 0 + }; + if (CheckForDeadlock("Sanity2", tokens)) { + PASS(); + } else { + FAIL("deadlock not detected"); + } +} + + +int +Sanity3_Child() +{ + TestMutex m1("dd.sanity3.m1"); + TestMutex m2("dd.sanity3.m2"); + TestMutex m3("dd.sanity3.m3"); + TestMutex m4("dd.sanity3.m4"); + + m1.Lock(); + m2.Lock(); + m3.Lock(); + m4.Lock(); + m4.Unlock(); + m3.Unlock(); + m2.Unlock(); + m1.Unlock(); + + m4.Lock(); + m1.Lock(); + return 0; +} + +nsresult +Sanity3() +{ + const char* const tokens[] = { + "###!!! ERROR: Potential deadlock detected", + "=== Cyclical dependency starts at\n--- Mutex : dd.sanity3.m1", + "--- Next dependency:\n--- Mutex : dd.sanity3.m2", + "--- Next dependency:\n--- Mutex : dd.sanity3.m3", + "--- Next dependency:\n--- Mutex : dd.sanity3.m4", + "=== Cycle completed at\n--- Mutex : dd.sanity3.m1", + "###!!! ASSERTION: Potential deadlock detected", + 0 + }; + if (CheckForDeadlock("Sanity3", tokens)) { + PASS(); + } else { + FAIL("deadlock not detected"); + } +} + + +int +Sanity4_Child() +{ + mozilla::ReentrantMonitor m1("dd.sanity4.m1"); + TestMutex m2("dd.sanity4.m2"); + m1.Enter(); + m2.Lock(); + m1.Enter(); + return 0; +} + +nsresult +Sanity4() +{ + const char* const tokens[] = { + "Re-entering ReentrantMonitor after acquiring other resources", + "###!!! ERROR: Potential deadlock detected", + "=== Cyclical dependency starts at\n--- ReentrantMonitor : dd.sanity4.m1", + "--- Next dependency:\n--- Mutex : dd.sanity4.m2", + "=== Cycle completed at\n--- ReentrantMonitor : dd.sanity4.m1", + "###!!! ASSERTION: Potential deadlock detected", + 0 + }; + if (CheckForDeadlock("Sanity4", tokens)) { + PASS(); + } else { + FAIL("deadlock not detected"); + } +} + +//----------------------------------------------------------------------------- +// Multithreaded tests + +TestMutex* ttM1; +TestMutex* ttM2; + +static void +TwoThreads_thread(void* arg) +{ + int32_t m1First = NS_PTR_TO_INT32(arg); + if (m1First) { + ttM1->Lock(); + ttM2->Lock(); + ttM2->Unlock(); + ttM1->Unlock(); + } + else { + ttM2->Lock(); + ttM1->Lock(); + ttM1->Unlock(); + ttM2->Unlock(); + } +} + +int +TwoThreads_Child() +{ + ttM1 = new TestMutex("dd.twothreads.m1"); + ttM2 = new TestMutex("dd.twothreads.m2"); + if (!ttM1 || !ttM2) + NS_RUNTIMEABORT("couldn't allocate mutexes"); + + PRThread* t1 = spawn(TwoThreads_thread, (void*) 0); + PR_JoinThread(t1); + + PRThread* t2 = spawn(TwoThreads_thread, (void*) 1); + PR_JoinThread(t2); + + return 0; +} + +nsresult +TwoThreads() +{ + const char* const tokens[] = { + "###!!! ERROR: Potential deadlock detected", + "=== Cyclical dependency starts at\n--- Mutex : dd.twothreads.m2", + "--- Next dependency:\n--- Mutex : dd.twothreads.m1", + "=== Cycle completed at\n--- Mutex : dd.twothreads.m2", + "###!!! ASSERTION: Potential deadlock detected", + 0 + }; + + if (CheckForDeadlock("TwoThreads", tokens)) { + PASS(); + } else { + FAIL("deadlock not detected"); + } +} + + +TestMutex* cndMs[4]; +const uint32_t K = 100000; + +static void +ContentionNoDeadlock_thread(void* arg) +{ + int32_t starti = NS_PTR_TO_INT32(arg); + + for (uint32_t k = 0; k < K; ++k) { + for (int32_t i = starti; i < (int32_t) ArrayLength(cndMs); ++i) + cndMs[i]->Lock(); + // comment out the next two lines for deadlocking fun! + for (int32_t i = ArrayLength(cndMs) - 1; i >= starti; --i) + cndMs[i]->Unlock(); + + starti = (starti + 1) % 3; + } +} + +int +ContentionNoDeadlock_Child() +{ + PRThread* threads[3]; + + for (uint32_t i = 0; i < ArrayLength(cndMs); ++i) + cndMs[i] = new TestMutex("dd.cnd.ms"); + + for (int32_t i = 0; i < (int32_t) ArrayLength(threads); ++i) + threads[i] = spawn(ContentionNoDeadlock_thread, NS_INT32_TO_PTR(i)); + + for (uint32_t i = 0; i < ArrayLength(threads); ++i) + PR_JoinThread(threads[i]); + + for (uint32_t i = 0; i < ArrayLength(cndMs); ++i) + delete cndMs[i]; + + return 0; +} + +nsresult +ContentionNoDeadlock() +{ + const char * func = __func__; + Subprocess proc(func); + proc.RunToCompletion(60000); + if (0 != proc.mExitCode) { + printf("(expected 0 == return code, got %d)\n", proc.mExitCode); + puts("(output)\n----------------------------------\n"); + puts(proc.mStdout.get()); + puts("----------------------------------\n"); + puts("(error output)\n----------------------------------\n"); + puts(proc.mStderr.get()); + puts("----------------------------------\n"); + + FAIL("deadlock"); + } + PASS(); +} + + + +//----------------------------------------------------------------------------- + +int +main(int argc, char** argv) +{ + if (1 < argc) { + // XXX can we run w/o scoped XPCOM? + const char* test = argv[1]; + ScopedXPCOM xpcom(test); + if (xpcom.failed()) + return 1; + + // running in a spawned process. call the specificed child function. + if (!strcmp("Sanity", test)) + return Sanity_Child(); + if (!strcmp("Sanity2", test)) + return Sanity2_Child(); + if (!strcmp("Sanity3", test)) + return Sanity3_Child(); + if (!strcmp("Sanity4", test)) + return Sanity4_Child(); + + if (!strcmp("TwoThreads", test)) + return TwoThreads_Child(); + if (!strcmp("ContentionNoDeadlock", test)) + return ContentionNoDeadlock_Child(); + + fail("%s | %s - unknown child test", __FILE__, __FUNCTION__); + return 1; + } + + ScopedXPCOM xpcom("Storage deadlock detector correctness (" __FILE__ ")"); + if (xpcom.failed()) + return 1; + + // in the first invocation of this process. we will be the "driver". + int rv = 0; + + sPathToThisBinary = argv[0]; + + if (NS_FAILED(Sanity())) + rv = 1; + if (NS_FAILED(Sanity2())) + rv = 1; + if (NS_FAILED(Sanity3())) + rv = 1; + if (NS_FAILED(Sanity4())) + rv = 1; + + if (NS_FAILED(TwoThreads())) + rv = 1; + if (NS_FAILED(ContentionNoDeadlock())) + rv = 1; + + return rv; +} diff --git a/storage/test/test_file_perms.cpp b/storage/test/test_file_perms.cpp new file mode 100644 index 000000000..1235fe09b --- /dev/null +++ b/storage/test/test_file_perms.cpp @@ -0,0 +1,44 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "storage_test_harness.h" +#include "nsIFile.h" +#include "prio.h" + +/** + * This file tests that the file permissions of the sqlite files match what + * we request they be + */ + +void +test_file_perms() +{ + nsCOMPtr<mozIStorageConnection> db(getDatabase()); + nsCOMPtr<nsIFile> dbFile; + do_check_success(db->GetDatabaseFile(getter_AddRefs(dbFile))); + + uint32_t perms = 0; + do_check_success(dbFile->GetPermissions(&perms)); + + // This reflexts the permissions defined by SQLITE_DEFAULT_FILE_PERMISSIONS in + // db/sqlite3/src/Makefile.in and must be kept in sync with that +#ifdef ANDROID + do_check_true(perms == (PR_IRUSR | PR_IWUSR)); +#elif defined(XP_WIN) + do_check_true(perms == (PR_IRUSR | PR_IWUSR | PR_IRGRP | PR_IWGRP | PR_IROTH | PR_IWOTH)); +#else + do_check_true(perms == (PR_IRUSR | PR_IWUSR | PR_IRGRP | PR_IROTH)); +#endif +} + +void (*gTests[])(void) = { + test_file_perms, +}; + +const char *file = __FILE__; +#define TEST_NAME "file perms" +#define TEST_FILE file +#include "storage_test_harness_tail.h" diff --git a/storage/test/test_mutex.cpp b/storage/test/test_mutex.cpp new file mode 100644 index 000000000..8fd0eb3f3 --- /dev/null +++ b/storage/test/test_mutex.cpp @@ -0,0 +1,85 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "storage_test_harness.h" + +#include "SQLiteMutex.h" + +using namespace mozilla; +using namespace mozilla::storage; + +/** + * This file test our sqlite3_mutex wrapper in SQLiteMutex.h. + */ + +void +test_AutoLock() +{ + int lockTypes[] = { + SQLITE_MUTEX_FAST, + SQLITE_MUTEX_RECURSIVE, + }; + for (size_t i = 0; i < ArrayLength(lockTypes); i++) { + // Get our test mutex (we have to allocate a SQLite mutex of the right type + // too!). + SQLiteMutex mutex("TestMutex"); + sqlite3_mutex *inner = sqlite3_mutex_alloc(lockTypes[i]); + do_check_true(inner); + mutex.initWithMutex(inner); + + // And test that our automatic locking wrapper works as expected. + mutex.assertNotCurrentThreadOwns(); + { + SQLiteMutexAutoLock lockedScope(mutex); + mutex.assertCurrentThreadOwns(); + } + mutex.assertNotCurrentThreadOwns(); + + // Free the wrapped mutex - we don't need it anymore. + sqlite3_mutex_free(inner); + } +} + +void +test_AutoUnlock() +{ + int lockTypes[] = { + SQLITE_MUTEX_FAST, + SQLITE_MUTEX_RECURSIVE, + }; + for (size_t i = 0; i < ArrayLength(lockTypes); i++) { + // Get our test mutex (we have to allocate a SQLite mutex of the right type + // too!). + SQLiteMutex mutex("TestMutex"); + sqlite3_mutex *inner = sqlite3_mutex_alloc(lockTypes[i]); + do_check_true(inner); + mutex.initWithMutex(inner); + + // And test that our automatic unlocking wrapper works as expected. + { + SQLiteMutexAutoLock lockedScope(mutex); + + { + SQLiteMutexAutoUnlock unlockedScope(mutex); + mutex.assertNotCurrentThreadOwns(); + } + mutex.assertCurrentThreadOwns(); + } + + // Free the wrapped mutex - we don't need it anymore. + sqlite3_mutex_free(inner); + } +} + +void (*gTests[])(void) = { + test_AutoLock, + test_AutoUnlock, +}; + +const char *file = __FILE__; +#define TEST_NAME "SQLiteMutex" +#define TEST_FILE file +#include "storage_test_harness_tail.h" diff --git a/storage/test/test_service_init_background_thread.cpp b/storage/test/test_service_init_background_thread.cpp new file mode 100644 index 000000000..56e61a19b --- /dev/null +++ b/storage/test/test_service_init_background_thread.cpp @@ -0,0 +1,58 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim set:sw=2 ts=2 et lcs=trail\:.,tab\:>~ : */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "storage_test_harness.h" + +#include "nsThreadUtils.h" + +/** + * This file tests that the storage service can be initialized off of the main + * thread without issue. + */ + +//////////////////////////////////////////////////////////////////////////////// +//// Helpers + +class ServiceInitializer : public mozilla::Runnable +{ +public: + NS_IMETHOD Run() override + { + // Use an explicit do_GetService instead of getService so that the check in + // getService doesn't blow up. + nsCOMPtr<mozIStorageService> service = do_GetService("@mozilla.org/storage/service;1"); + do_check_false(service); + return NS_OK; + } +}; + +//////////////////////////////////////////////////////////////////////////////// +//// Test Functions + +void +test_service_initialization_on_background_thread() +{ + nsCOMPtr<nsIRunnable> event = new ServiceInitializer(); + do_check_true(event); + + nsCOMPtr<nsIThread> thread; + do_check_success(NS_NewThread(getter_AddRefs(thread))); + + do_check_success(thread->Dispatch(event, NS_DISPATCH_NORMAL)); + + // Shutting down the thread will spin the event loop until all work in its + // event queue is completed. This will act as our thread synchronization. + do_check_success(thread->Shutdown()); +} + +void (*gTests[])(void) = { + test_service_initialization_on_background_thread, +}; + +const char *file = __FILE__; +#define TEST_NAME "Background Thread Initialization" +#define TEST_FILE file +#include "storage_test_harness_tail.h" diff --git a/storage/test/test_statement_scoper.cpp b/storage/test/test_statement_scoper.cpp new file mode 100644 index 000000000..784d8d772 --- /dev/null +++ b/storage/test/test_statement_scoper.cpp @@ -0,0 +1,104 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "storage_test_harness.h" + +#include "mozStorageHelper.h" + +/** + * This file test our statement scoper in mozStorageHelper.h. + */ + +void +test_automatic_reset() +{ + nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase()); + + // Need to create a table to populate sqlite_master with an entry. + (void)db->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "CREATE TABLE test (id INTEGER PRIMARY KEY)" + )); + + nsCOMPtr<mozIStorageStatement> stmt; + (void)db->CreateStatement(NS_LITERAL_CSTRING( + "SELECT * FROM sqlite_master" + ), getter_AddRefs(stmt)); + + // Reality check - make sure we start off in the right state. + int32_t state = -1; + (void)stmt->GetState(&state); + do_check_true(state == mozIStorageStatement::MOZ_STORAGE_STATEMENT_READY); + + // Start executing the statement, which will put it into an executing state. + { + mozStorageStatementScoper scoper(stmt); + bool hasMore; + do_check_true(NS_SUCCEEDED(stmt->ExecuteStep(&hasMore))); + + // Reality check that we are executing. + state = -1; + (void)stmt->GetState(&state); + do_check_true(state == + mozIStorageStatement::MOZ_STORAGE_STATEMENT_EXECUTING); + } + + // And we should be ready again. + state = -1; + (void)stmt->GetState(&state); + do_check_true(state == mozIStorageStatement::MOZ_STORAGE_STATEMENT_READY); +} + +void +test_Abandon() +{ + nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase()); + + // Need to create a table to populate sqlite_master with an entry. + (void)db->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "CREATE TABLE test (id INTEGER PRIMARY KEY)" + )); + + nsCOMPtr<mozIStorageStatement> stmt; + (void)db->CreateStatement(NS_LITERAL_CSTRING( + "SELECT * FROM sqlite_master" + ), getter_AddRefs(stmt)); + + // Reality check - make sure we start off in the right state. + int32_t state = -1; + (void)stmt->GetState(&state); + do_check_true(state == mozIStorageStatement::MOZ_STORAGE_STATEMENT_READY); + + // Start executing the statement, which will put it into an executing state. + { + mozStorageStatementScoper scoper(stmt); + bool hasMore; + do_check_true(NS_SUCCEEDED(stmt->ExecuteStep(&hasMore))); + + // Reality check that we are executing. + state = -1; + (void)stmt->GetState(&state); + do_check_true(state == + mozIStorageStatement::MOZ_STORAGE_STATEMENT_EXECUTING); + + // And call Abandon. We should not reset now when we fall out of scope. + scoper.Abandon(); + } + + // And we should still be executing. + state = -1; + (void)stmt->GetState(&state); + do_check_true(state == mozIStorageStatement::MOZ_STORAGE_STATEMENT_EXECUTING); +} + +void (*gTests[])(void) = { + test_automatic_reset, + test_Abandon, +}; + +const char *file = __FILE__; +#define TEST_NAME "statement scoper" +#define TEST_FILE file +#include "storage_test_harness_tail.h" diff --git a/storage/test/test_transaction_helper.cpp b/storage/test/test_transaction_helper.cpp new file mode 100644 index 000000000..4277c8f94 --- /dev/null +++ b/storage/test/test_transaction_helper.cpp @@ -0,0 +1,175 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "storage_test_harness.h" + +#include "mozStorageHelper.h" +#include "mozStorageConnection.h" + +using namespace mozilla; +using namespace mozilla::storage; + +bool has_transaction(mozIStorageConnection* aDB) { + return !(static_cast<Connection *>(aDB)->getAutocommit()); +} + +/** + * This file test our Transaction helper in mozStorageHelper.h. + */ + +void +test_Commit() +{ + nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase()); + + // Create a table in a transaction, call Commit, and make sure that it does + // exists after the transaction falls out of scope. + { + mozStorageTransaction transaction(db, false); + do_check_true(has_transaction(db)); + (void)db->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "CREATE TABLE test (id INTEGER PRIMARY KEY)" + )); + (void)transaction.Commit(); + } + do_check_false(has_transaction(db)); + + bool exists = false; + (void)db->TableExists(NS_LITERAL_CSTRING("test"), &exists); + do_check_true(exists); +} + +void +test_Rollback() +{ + nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase()); + + // Create a table in a transaction, call Rollback, and make sure that it does + // not exists after the transaction falls out of scope. + { + mozStorageTransaction transaction(db, true); + do_check_true(has_transaction(db)); + (void)db->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "CREATE TABLE test (id INTEGER PRIMARY KEY)" + )); + (void)transaction.Rollback(); + } + do_check_false(has_transaction(db)); + + bool exists = true; + (void)db->TableExists(NS_LITERAL_CSTRING("test"), &exists); + do_check_false(exists); +} + +void +test_AutoCommit() +{ + nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase()); + + // Create a table in a transaction, and make sure that it exists after the + // transaction falls out of scope. This means the Commit was successful. + { + mozStorageTransaction transaction(db, true); + do_check_true(has_transaction(db)); + (void)db->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "CREATE TABLE test (id INTEGER PRIMARY KEY)" + )); + } + do_check_false(has_transaction(db)); + + bool exists = false; + (void)db->TableExists(NS_LITERAL_CSTRING("test"), &exists); + do_check_true(exists); +} + +void +test_AutoRollback() +{ + nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase()); + + // Create a table in a transaction, and make sure that it does not exists + // after the transaction falls out of scope. This means the Rollback was + // successful. + { + mozStorageTransaction transaction(db, false); + do_check_true(has_transaction(db)); + (void)db->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "CREATE TABLE test (id INTEGER PRIMARY KEY)" + )); + } + do_check_false(has_transaction(db)); + + bool exists = true; + (void)db->TableExists(NS_LITERAL_CSTRING("test"), &exists); + do_check_false(exists); +} + +void +test_null_database_connection() +{ + // We permit the use of the Transaction helper when passing a null database + // in, so we need to make sure this still works without crashing. + mozStorageTransaction transaction(nullptr, false); + do_check_true(NS_SUCCEEDED(transaction.Commit())); + do_check_true(NS_SUCCEEDED(transaction.Rollback())); +} + +void +test_async_Commit() +{ + // note this will be active for any following test. + hook_sqlite_mutex(); + + nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase()); + + // -- wedge the thread + nsCOMPtr<nsIThread> target(get_conn_async_thread(db)); + do_check_true(target); + RefPtr<ThreadWedger> wedger (new ThreadWedger(target)); + + { + mozStorageTransaction transaction(db, false, + mozIStorageConnection::TRANSACTION_DEFERRED, + true); + do_check_true(has_transaction(db)); + (void)db->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "CREATE TABLE test (id INTEGER PRIMARY KEY)" + )); + (void)transaction.Commit(); + } + do_check_true(has_transaction(db)); + + // -- unwedge the async thread + wedger->unwedge(); + + // Ensure the transaction has done its job by enqueueing an async execution. + nsCOMPtr<mozIStorageAsyncStatement> stmt; + (void)db->CreateAsyncStatement(NS_LITERAL_CSTRING( + "SELECT NULL" + ), getter_AddRefs(stmt)); + blocking_async_execute(stmt); + stmt->Finalize(); + do_check_false(has_transaction(db)); + bool exists = false; + (void)db->TableExists(NS_LITERAL_CSTRING("test"), &exists); + do_check_true(exists); + + blocking_async_close(db); +} + +void (*gTests[])(void) = { + test_Commit, + test_Rollback, + test_AutoCommit, + test_AutoRollback, + test_null_database_connection, + test_async_Commit, +}; + +const char *file = __FILE__; +#define TEST_NAME "transaction helper" +#define TEST_FILE file +#include "storage_test_harness_tail.h" diff --git a/storage/test/test_true_async.cpp b/storage/test/test_true_async.cpp new file mode 100644 index 000000000..fea00392d --- /dev/null +++ b/storage/test/test_true_async.cpp @@ -0,0 +1,186 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "storage_test_harness.h" + +//////////////////////////////////////////////////////////////////////////////// +//// Tests + +void +test_TrueAsyncStatement() +{ + // (only the first test needs to call this) + hook_sqlite_mutex(); + + nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase()); + + // Start watching for forbidden mutex usage. + watch_for_mutex_use_on_this_thread(); + + // - statement with nothing to bind + nsCOMPtr<mozIStorageAsyncStatement> stmt; + db->CreateAsyncStatement( + NS_LITERAL_CSTRING("CREATE TABLE test (id INTEGER PRIMARY KEY)"), + getter_AddRefs(stmt) + ); + blocking_async_execute(stmt); + stmt->Finalize(); + do_check_false(mutex_used_on_watched_thread); + + // - statement with something to bind ordinally + db->CreateAsyncStatement( + NS_LITERAL_CSTRING("INSERT INTO test (id) VALUES (?)"), + getter_AddRefs(stmt) + ); + stmt->BindInt32ByIndex(0, 1); + blocking_async_execute(stmt); + stmt->Finalize(); + do_check_false(mutex_used_on_watched_thread); + + // - statement with something to bind by name + db->CreateAsyncStatement( + NS_LITERAL_CSTRING("INSERT INTO test (id) VALUES (:id)"), + getter_AddRefs(stmt) + ); + nsCOMPtr<mozIStorageBindingParamsArray> paramsArray; + stmt->NewBindingParamsArray(getter_AddRefs(paramsArray)); + nsCOMPtr<mozIStorageBindingParams> params; + paramsArray->NewBindingParams(getter_AddRefs(params)); + params->BindInt32ByName(NS_LITERAL_CSTRING("id"), 2); + paramsArray->AddParams(params); + params = nullptr; + stmt->BindParameters(paramsArray); + paramsArray = nullptr; + blocking_async_execute(stmt); + stmt->Finalize(); + do_check_false(mutex_used_on_watched_thread); + + // - now, make sure creating a sync statement does trigger our guard. + // (If this doesn't happen, our test is bunk and it's important to know that.) + nsCOMPtr<mozIStorageStatement> syncStmt; + db->CreateStatement(NS_LITERAL_CSTRING("SELECT * FROM test"), + getter_AddRefs(syncStmt)); + syncStmt->Finalize(); + do_check_true(mutex_used_on_watched_thread); + + blocking_async_close(db); +} + +/** + * Test that cancellation before a statement is run successfully stops the + * statement from executing. + */ +void +test_AsyncCancellation() +{ + nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase()); + + // -- wedge the thread + nsCOMPtr<nsIThread> target(get_conn_async_thread(db)); + do_check_true(target); + RefPtr<ThreadWedger> wedger (new ThreadWedger(target)); + + // -- create statements and cancel them + // - async + nsCOMPtr<mozIStorageAsyncStatement> asyncStmt; + db->CreateAsyncStatement( + NS_LITERAL_CSTRING("CREATE TABLE asyncTable (id INTEGER PRIMARY KEY)"), + getter_AddRefs(asyncStmt) + ); + + RefPtr<AsyncStatementSpinner> asyncSpin(new AsyncStatementSpinner()); + nsCOMPtr<mozIStoragePendingStatement> asyncPend; + (void)asyncStmt->ExecuteAsync(asyncSpin, getter_AddRefs(asyncPend)); + do_check_true(asyncPend); + asyncPend->Cancel(); + + // - sync + nsCOMPtr<mozIStorageStatement> syncStmt; + db->CreateStatement( + NS_LITERAL_CSTRING("CREATE TABLE syncTable (id INTEGER PRIMARY KEY)"), + getter_AddRefs(syncStmt) + ); + + RefPtr<AsyncStatementSpinner> syncSpin(new AsyncStatementSpinner()); + nsCOMPtr<mozIStoragePendingStatement> syncPend; + (void)syncStmt->ExecuteAsync(syncSpin, getter_AddRefs(syncPend)); + do_check_true(syncPend); + syncPend->Cancel(); + + // -- unwedge the async thread + wedger->unwedge(); + + // -- verify that both statements report they were canceled + asyncSpin->SpinUntilCompleted(); + do_check_true(asyncSpin->completionReason == + mozIStorageStatementCallback::REASON_CANCELED); + + syncSpin->SpinUntilCompleted(); + do_check_true(syncSpin->completionReason == + mozIStorageStatementCallback::REASON_CANCELED); + + // -- verify that neither statement constructed their tables + nsresult rv; + bool exists; + rv = db->TableExists(NS_LITERAL_CSTRING("asyncTable"), &exists); + do_check_true(rv == NS_OK); + do_check_false(exists); + rv = db->TableExists(NS_LITERAL_CSTRING("syncTable"), &exists); + do_check_true(rv == NS_OK); + do_check_false(exists); + + // -- cleanup + asyncStmt->Finalize(); + syncStmt->Finalize(); + blocking_async_close(db); +} + +/** + * Test that the destructor for an asynchronous statement which has a + * sqlite3_stmt will dispatch that statement to the async thread for + * finalization rather than trying to finalize it on the main thread + * (and thereby running afoul of our mutex use detector). + */ +void test_AsyncDestructorFinalizesOnAsyncThread() +{ + // test_TrueAsyncStatement called hook_sqlite_mutex() for us + + nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase()); + watch_for_mutex_use_on_this_thread(); + + // -- create an async statement + nsCOMPtr<mozIStorageAsyncStatement> stmt; + db->CreateAsyncStatement( + NS_LITERAL_CSTRING("CREATE TABLE test (id INTEGER PRIMARY KEY)"), + getter_AddRefs(stmt) + ); + + // -- execute it so it gets a sqlite3_stmt that needs to be finalized + blocking_async_execute(stmt); + do_check_false(mutex_used_on_watched_thread); + + // -- forget our reference + stmt = nullptr; + + // -- verify the mutex was not touched + do_check_false(mutex_used_on_watched_thread); + + // -- make sure the statement actually gets finalized / cleanup + // the close will assert if we failed to finalize! + blocking_async_close(db); +} + +void (*gTests[])(void) = { + // this test must be first because it hooks the mutex mechanics + test_TrueAsyncStatement, + test_AsyncCancellation, + test_AsyncDestructorFinalizesOnAsyncThread +}; + +const char *file = __FILE__; +#define TEST_NAME "true async statement" +#define TEST_FILE file +#include "storage_test_harness_tail.h" diff --git a/storage/test/test_unlock_notify.cpp b/storage/test/test_unlock_notify.cpp new file mode 100644 index 000000000..0a7ca67f9 --- /dev/null +++ b/storage/test/test_unlock_notify.cpp @@ -0,0 +1,266 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim set:sw=2 ts=2 et lcs=trail\:.,tab\:>~ : */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "storage_test_harness.h" + +#include "mozilla/ReentrantMonitor.h" +#include "nsThreadUtils.h" +#include "mozIStorageStatement.h" + +/** + * This file tests that our implementation around sqlite3_unlock_notify works + * as expected. + */ + +//////////////////////////////////////////////////////////////////////////////// +//// Helpers + +enum State { + STARTING, + WRITE_LOCK, + READ_LOCK, + TEST_DONE +}; + +class DatabaseLocker : public mozilla::Runnable +{ +public: + explicit DatabaseLocker(const char* aSQL) + : monitor("DatabaseLocker::monitor") + , mSQL(aSQL) + , mState(STARTING) + { + } + + void RunInBackground() + { + (void)NS_NewNamedThread("DatabaseLocker", getter_AddRefs(mThread)); + do_check_true(mThread); + + do_check_success(mThread->Dispatch(this, NS_DISPATCH_NORMAL)); + } + + void Shutdown() + { + if (mThread) { + mThread->Shutdown(); + } + } + + NS_IMETHOD Run() override + { + mozilla::ReentrantMonitorAutoEnter lock(monitor); + + nsCOMPtr<mozIStorageConnection> db(getDatabase()); + + nsCString sql(mSQL); + nsCOMPtr<mozIStorageStatement> stmt; + do_check_success(db->CreateStatement(sql, getter_AddRefs(stmt))); + + bool hasResult; + do_check_success(stmt->ExecuteStep(&hasResult)); + + Notify(WRITE_LOCK); + WaitFor(TEST_DONE); + + return NS_OK; + } + + void WaitFor(State aState) + { + monitor.AssertCurrentThreadIn(); + while (mState != aState) { + do_check_success(monitor.Wait()); + } + } + + void Notify(State aState) + { + monitor.AssertCurrentThreadIn(); + mState = aState; + do_check_success(monitor.Notify()); + } + + mozilla::ReentrantMonitor monitor; + +protected: + nsCOMPtr<nsIThread> mThread; + const char *const mSQL; + State mState; +}; + +class DatabaseTester : public DatabaseLocker +{ +public: + DatabaseTester(mozIStorageConnection *aConnection, + const char* aSQL) + : DatabaseLocker(aSQL) + , mConnection(aConnection) + { + } + + NS_IMETHOD Run() override + { + mozilla::ReentrantMonitorAutoEnter lock(monitor); + WaitFor(READ_LOCK); + + nsCString sql(mSQL); + nsCOMPtr<mozIStorageStatement> stmt; + do_check_success(mConnection->CreateStatement(sql, getter_AddRefs(stmt))); + + bool hasResult; + nsresult rv = stmt->ExecuteStep(&hasResult); + do_check_eq(rv, NS_ERROR_FILE_IS_LOCKED); + + // Finalize our statement and null out our connection before notifying to + // ensure that we close on the proper thread. + rv = stmt->Finalize(); + do_check_eq(rv, NS_ERROR_FILE_IS_LOCKED); + mConnection = nullptr; + + Notify(TEST_DONE); + + return NS_OK; + } + +private: + nsCOMPtr<mozIStorageConnection> mConnection; +}; + +//////////////////////////////////////////////////////////////////////////////// +//// Test Functions + +void +setup() +{ + nsCOMPtr<mozIStorageConnection> db(getDatabase()); + + // Create and populate a dummy table. + nsresult rv = db->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "CREATE TABLE test (id INTEGER PRIMARY KEY, data STRING)" + )); + do_check_success(rv); + rv = db->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "INSERT INTO test (data) VALUES ('foo')" + )); + do_check_success(rv); + rv = db->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "INSERT INTO test (data) VALUES ('bar')" + )); + do_check_success(rv); + rv = db->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "CREATE UNIQUE INDEX unique_data ON test (data)" + )); + do_check_success(rv); +} + +void +test_step_locked_does_not_block_main_thread() +{ + nsCOMPtr<mozIStorageConnection> db(getDatabase()); + + // Need to prepare our statement ahead of time so we make sure to only test + // step and not prepare. + nsCOMPtr<mozIStorageStatement> stmt; + nsresult rv = db->CreateStatement(NS_LITERAL_CSTRING( + "INSERT INTO test (data) VALUES ('test1')" + ), getter_AddRefs(stmt)); + do_check_success(rv); + + RefPtr<DatabaseLocker> locker(new DatabaseLocker("SELECT * FROM test")); + do_check_true(locker); + { + mozilla::ReentrantMonitorAutoEnter lock(locker->monitor); + locker->RunInBackground(); + + // Wait for the locker to notify us that it has locked the database properly. + locker->WaitFor(WRITE_LOCK); + + bool hasResult; + rv = stmt->ExecuteStep(&hasResult); + do_check_eq(rv, NS_ERROR_FILE_IS_LOCKED); + + locker->Notify(TEST_DONE); + } + locker->Shutdown(); +} + +void +test_drop_index_does_not_loop() +{ + nsCOMPtr<mozIStorageConnection> db(getDatabase()); + + // Need to prepare our statement ahead of time so we make sure to only test + // step and not prepare. + nsCOMPtr<mozIStorageStatement> stmt; + nsresult rv = db->CreateStatement(NS_LITERAL_CSTRING( + "SELECT * FROM test" + ), getter_AddRefs(stmt)); + do_check_success(rv); + + RefPtr<DatabaseTester> tester = + new DatabaseTester(db, "DROP INDEX unique_data"); + do_check_true(tester); + { + mozilla::ReentrantMonitorAutoEnter lock(tester->monitor); + tester->RunInBackground(); + + // Hold a read lock on the database, and then let the tester try to execute. + bool hasResult; + rv = stmt->ExecuteStep(&hasResult); + do_check_success(rv); + do_check_true(hasResult); + tester->Notify(READ_LOCK); + + // Make sure the tester finishes its test before we move on. + tester->WaitFor(TEST_DONE); + } + tester->Shutdown(); +} + +void +test_drop_table_does_not_loop() +{ + nsCOMPtr<mozIStorageConnection> db(getDatabase()); + + // Need to prepare our statement ahead of time so we make sure to only test + // step and not prepare. + nsCOMPtr<mozIStorageStatement> stmt; + nsresult rv = db->CreateStatement(NS_LITERAL_CSTRING( + "SELECT * FROM test" + ), getter_AddRefs(stmt)); + do_check_success(rv); + + RefPtr<DatabaseTester> tester(new DatabaseTester(db, "DROP TABLE test")); + do_check_true(tester); + { + mozilla::ReentrantMonitorAutoEnter lock(tester->monitor); + tester->RunInBackground(); + + // Hold a read lock on the database, and then let the tester try to execute. + bool hasResult; + rv = stmt->ExecuteStep(&hasResult); + do_check_success(rv); + do_check_true(hasResult); + tester->Notify(READ_LOCK); + + // Make sure the tester finishes its test before we move on. + tester->WaitFor(TEST_DONE); + } + tester->Shutdown(); +} + +void (*gTests[])(void) = { + setup, + test_step_locked_does_not_block_main_thread, + test_drop_index_does_not_loop, + test_drop_table_does_not_loop, +}; + +const char *file = __FILE__; +#define TEST_NAME "sqlite3_unlock_notify" +#define TEST_FILE file +#include "storage_test_harness_tail.h" diff --git a/storage/test/unit/corruptDB.sqlite b/storage/test/unit/corruptDB.sqlite Binary files differnew file mode 100644 index 000000000..b234246ca --- /dev/null +++ b/storage/test/unit/corruptDB.sqlite diff --git a/storage/test/unit/fakeDB.sqlite b/storage/test/unit/fakeDB.sqlite new file mode 100644 index 000000000..5f7498bfc --- /dev/null +++ b/storage/test/unit/fakeDB.sqlite @@ -0,0 +1 @@ +BACON diff --git a/storage/test/unit/head_storage.js b/storage/test/unit/head_storage.js new file mode 100644 index 000000000..40374afad --- /dev/null +++ b/storage/test/unit/head_storage.js @@ -0,0 +1,372 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const Ci = Components.interfaces; +const Cc = Components.classes; +const Cr = Components.results; +const Cu = Components.utils; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "Promise", + "resource://gre/modules/Promise.jsm"); + + +do_get_profile(); +var dirSvc = Cc["@mozilla.org/file/directory_service;1"]. + getService(Ci.nsIProperties); + +var gDBConn = null; + +function getTestDB() +{ + var db = dirSvc.get("ProfD", Ci.nsIFile); + db.append("test_storage.sqlite"); + return db; +} + +/** + * Obtains a corrupt database to test against. + */ +function getCorruptDB() +{ + return do_get_file("corruptDB.sqlite"); +} + +/** + * Obtains a fake (non-SQLite format) database to test against. + */ +function getFakeDB() +{ + return do_get_file("fakeDB.sqlite"); +} + +/** + * Delete the test database file. + */ +function deleteTestDB() +{ + print("*** Storage Tests: Trying to remove file!"); + var dbFile = getTestDB(); + if (dbFile.exists()) + try { dbFile.remove(false); } catch (e) { /* stupid windows box */ } +} + +function cleanup() +{ + // close the connection + print("*** Storage Tests: Trying to close!"); + getOpenedDatabase().close(); + + // we need to null out the database variable to get a new connection the next + // time getOpenedDatabase is called + gDBConn = null; + + // removing test db + deleteTestDB(); +} + +/** + * Use asyncClose to cleanup a connection. Synchronous by means of internally + * spinning an event loop. + */ +function asyncCleanup() +{ + let closed = false; + + // close the connection + print("*** Storage Tests: Trying to asyncClose!"); + getOpenedDatabase().asyncClose(function () { closed = true; }); + + let curThread = Components.classes["@mozilla.org/thread-manager;1"] + .getService().currentThread; + while (!closed) + curThread.processNextEvent(true); + + // we need to null out the database variable to get a new connection the next + // time getOpenedDatabase is called + gDBConn = null; + + // removing test db + deleteTestDB(); +} + +function getService() +{ + return Cc["@mozilla.org/storage/service;1"].getService(Ci.mozIStorageService); +} + +/** + * Get a connection to the test database. Creates and caches the connection + * if necessary, otherwise reuses the existing cached connection. This + * connection shares its cache. + * + * @returns the mozIStorageConnection for the file. + */ +function getOpenedDatabase() +{ + if (!gDBConn) { + gDBConn = getService().openDatabase(getTestDB()); + } + return gDBConn; +} + +/** + * Get a connection to the test database. Creates and caches the connection + * if necessary, otherwise reuses the existing cached connection. This + * connection doesn't share its cache. + * + * @returns the mozIStorageConnection for the file. + */ +function getOpenedUnsharedDatabase() +{ + if (!gDBConn) { + gDBConn = getService().openUnsharedDatabase(getTestDB()); + } + return gDBConn; +} + +/** + * Obtains a specific database to use. + * + * @param aFile + * The nsIFile representing the db file to open. + * @returns the mozIStorageConnection for the file. + */ +function getDatabase(aFile) +{ + return getService().openDatabase(aFile); +} + +function createStatement(aSQL) +{ + return getOpenedDatabase().createStatement(aSQL); +} + +/** + * Creates an asynchronous SQL statement. + * + * @param aSQL + * The SQL to parse into a statement. + * @returns a mozIStorageAsyncStatement from aSQL. + */ +function createAsyncStatement(aSQL) +{ + return getOpenedDatabase().createAsyncStatement(aSQL); +} + +/** + * Invoke the given function and assert that it throws an exception expressing + * the provided error code in its 'result' attribute. JS function expressions + * can be used to do this concisely. + * + * Example: + * expectError(Cr.NS_ERROR_INVALID_ARG, () => explodingFunction()); + * + * @param aErrorCode + * The error code to expect from invocation of aFunction. + * @param aFunction + * The function to invoke and expect an XPCOM-style error from. + */ +function expectError(aErrorCode, aFunction) +{ + let exceptionCaught = false; + try { + aFunction(); + } + catch (e) { + if (e.result != aErrorCode) { + do_throw("Got an exception, but the result code was not the expected " + + "one. Expected " + aErrorCode + ", got " + e.result); + } + exceptionCaught = true; + } + if (!exceptionCaught) + do_throw(aFunction + " should have thrown an exception but did not!"); +} + +/** + * Run a query synchronously and verify that we get back the expected results. + * + * @param aSQLString + * The SQL string for the query. + * @param aBind + * The value to bind at index 0. + * @param aResults + * A list of the expected values returned in the sole result row. + * Express blobs as lists. + */ +function verifyQuery(aSQLString, aBind, aResults) +{ + let stmt = getOpenedDatabase().createStatement(aSQLString); + stmt.bindByIndex(0, aBind); + try { + do_check_true(stmt.executeStep()); + let nCols = stmt.numEntries; + if (aResults.length != nCols) + do_throw("Expected " + aResults.length + " columns in result but " + + "there are only " + aResults.length + "!"); + for (let iCol = 0; iCol < nCols; iCol++) { + let expectedVal = aResults[iCol]; + let valType = stmt.getTypeOfIndex(iCol); + if (expectedVal === null) { + do_check_eq(stmt.VALUE_TYPE_NULL, valType); + do_check_true(stmt.getIsNull(iCol)); + } + else if (typeof expectedVal == "number") { + if (Math.floor(expectedVal) == expectedVal) { + do_check_eq(stmt.VALUE_TYPE_INTEGER, valType); + do_check_eq(expectedVal, stmt.getInt32(iCol)); + } + else { + do_check_eq(stmt.VALUE_TYPE_FLOAT, valType); + do_check_eq(expectedVal, stmt.getDouble(iCol)); + } + } + else if (typeof expectedVal == "string") { + do_check_eq(stmt.VALUE_TYPE_TEXT, valType); + do_check_eq(expectedVal, stmt.getUTF8String(iCol)); + } + else { // blob + do_check_eq(stmt.VALUE_TYPE_BLOB, valType); + let count = { value: 0 }, blob = { value: null }; + stmt.getBlob(iCol, count, blob); + do_check_eq(count.value, expectedVal.length); + for (let i = 0; i < count.value; i++) { + do_check_eq(expectedVal[i], blob.value[i]); + } + } + } + } + finally { + stmt.finalize(); + } +} + +/** + * Return the number of rows in the able with the given name using a synchronous + * query. + * + * @param aTableName + * The name of the table. + * @return The number of rows. + */ +function getTableRowCount(aTableName) +{ + var currentRows = 0; + var countStmt = getOpenedDatabase().createStatement( + "SELECT COUNT(1) AS count FROM " + aTableName + ); + try { + do_check_true(countStmt.executeStep()); + currentRows = countStmt.row.count; + } + finally { + countStmt.finalize(); + } + return currentRows; +} + +// Promise-Returning Functions + +function asyncClone(db, readOnly) { + let deferred = Promise.defer(); + db.asyncClone(readOnly, function (status, db2) { + if (Components.isSuccessCode(status)) { + deferred.resolve(db2); + } else { + deferred.reject(status); + } + }); + return deferred.promise; +} + +function asyncClose(db) { + let deferred = Promise.defer(); + db.asyncClose(function (status) { + if (Components.isSuccessCode(status)) { + deferred.resolve(); + } else { + deferred.reject(status); + } + }); + return deferred.promise; +} + +function openAsyncDatabase(file, options) { + let deferred = Promise.defer(); + let properties; + if (options) { + properties = Cc["@mozilla.org/hash-property-bag;1"]. + createInstance(Ci.nsIWritablePropertyBag); + for (let k in options) { + properties.setProperty(k, options[k]); + } + } + getService().openAsyncDatabase(file, properties, function (status, db) { + if (Components.isSuccessCode(status)) { + deferred.resolve(db.QueryInterface(Ci.mozIStorageAsyncConnection)); + } else { + deferred.reject(status); + } + }); + return deferred.promise; +} + +function executeAsync(statement, onResult) { + let deferred = Promise.defer(); + statement.executeAsync({ + handleError: function (error) { + deferred.reject(error); + }, + handleResult: function (result) { + if (onResult) { + onResult(result); + } + }, + handleCompletion: function (result) { + deferred.resolve(result); + } + }); + return deferred.promise; +} + +function executeMultipleStatementsAsync(db, statements, onResult) { + let deferred = Promise.defer(); + db.executeAsync(statements, statements.length, { + handleError: function (error) { + deferred.reject(error); + }, + handleResult: function (result) { + if (onResult) { + onResult(result); + } + }, + handleCompletion: function (result) { + deferred.resolve(result); + } + }); + return deferred.promise; +} + +function executeSimpleSQLAsync(db, query, onResult) { + let deferred = Promise.defer(); + db.executeSimpleSQLAsync(query, { + handleError(error) { + deferred.reject(error); + }, + handleResult(result) { + if (onResult) { + onResult(result); + } else { + do_throw("No results were expected"); + } + }, + handleCompletion(result) { + deferred.resolve(result); + } + }); + return deferred.promise; +} + +cleanup(); diff --git a/storage/test/unit/locale_collation.txt b/storage/test/unit/locale_collation.txt new file mode 100644 index 000000000..86f50579b --- /dev/null +++ b/storage/test/unit/locale_collation.txt @@ -0,0 +1,174 @@ + +! +" +# +$ +% +& +' +( +) +* ++ +, +- +. +/ +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +: +; +< += +> +? +@ +A +B +C +D +E +F +G +H +I +J +K +L +M +N +O +P +Q +R +S +T +U +V +W +X +Y +Z +[ +\ +] +^ +_ +` +a +b +c +d +e +f +g +h +i +j +k +l +m +n +o +p +q +r +s +t +u +v +w +x +y +z +{ +| +} +~ +ludwig van beethoven +Ludwig van Beethoven +Ludwig van beethoven +Jane +jane +JANE +jAne +jaNe +janE +JAne +JaNe +JanE +JANe +JaNE +JAnE +jANE +Umberto Eco +Umberto eco +umberto eco +umberto Eco +UMBERTO ECO +ace +bash +*ace +!ace +%ace +~ace +#ace +cork +denizen +[denizen] +(denizen) +{denizen} +/denizen/ +#denizen# +$denizen$ +@denizen +elf +full +gnome +gnomic investigation of typological factors in the grammaticalization process of Malayo-Polynesian substaratum in the protoAltaic vocabulary core in the proto-layers of pre-historic Japanese +gnomic investigation of typological factors in the grammaticalization process of Malayo-Polynesian substaratum in the protoAltaic vocabulary core in the proto-layers of pre-historic Javanese +hint +Internationalization +Zinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalization +Zinternationalization internationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizatio +n +Zinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalization internationalizationinternationalizationinternationalizationinternationalizationinternationalization +ZinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizaTioninternationalizationinternationalizationinternationalizationinternationalization +ZinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizaTion +jostle +kin +Laymen +lumens +Lumens +motleycrew +motley crew +niven's creative talents +nivens creative talents +opie +posh restaurants in surbanized and still urban incorporated subsection of this beautifl city in the Rockies +posh restaurants in surbanized and still urban incorporated subsection of this beautifl city in the Rokkies +posh restaurants in surbanized and still urban incorporated subsection of this beautifl city in the rockies +quilt's +quilts +quilt +Rondo +street +tamale oxidization and iodization in rapid progress +tamale oxidization and iodization in rapid Progress +until +vera +Wobble +Xanadu's legenary imaginary floccinaucinihilipilification in localization of theoretical portions of glottochronological understanding of the phoneme +Xanadu's legenary imaginary floccinaucinihilipilification in localization of theoretical portions of glottochronological understanding of the phoname +yearn +zodiac +a +å diff --git a/storage/test/unit/test_bug-365166.js b/storage/test/unit/test_bug-365166.js new file mode 100644 index 000000000..ebcdc6900 --- /dev/null +++ b/storage/test/unit/test_bug-365166.js @@ -0,0 +1,26 @@ +// Testcase for bug 365166 - crash [@ strlen] calling +// mozIStorageStatement::getColumnName of a statement created with +// "PRAGMA user_version" or "PRAGMA schema_version" +function run_test() { + test('user'); + test('schema'); + + function test(param) + { + var colName = param + "_version"; + var sql = "PRAGMA " + colName; + + var file = getTestDB(); + var storageService = Components.classes["@mozilla.org/storage/service;1"]. + getService(Components.interfaces.mozIStorageService); + var conn = storageService.openDatabase(file); + var statement = conn.createStatement(sql); + try { + // This shouldn't crash: + do_check_eq(statement.getColumnName(0), colName); + } finally { + statement.reset(); + statement.finalize(); + } + } +} diff --git a/storage/test/unit/test_bug-393952.js b/storage/test/unit/test_bug-393952.js new file mode 100644 index 000000000..f3c61f62b --- /dev/null +++ b/storage/test/unit/test_bug-393952.js @@ -0,0 +1,38 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// Testcase for bug 393952: crash when I try to VACUUM and one of the tables +// has a UNIQUE text column. StorageUnicodeFunctions::likeFunction() +// needs to handle null aArgv[0] and aArgv[1] + +function setup() +{ + getOpenedDatabase().createTable("t1", "x TEXT UNIQUE"); + + var stmt = createStatement("INSERT INTO t1 (x) VALUES ('a')"); + stmt.execute(); + stmt.reset(); + stmt.finalize(); +} + +function test_vacuum() +{ + var stmt = createStatement("VACUUM;"); + stmt.executeStep(); + stmt.reset(); + stmt.finalize(); +} + +var tests = [test_vacuum]; + +function run_test() +{ + setup(); + + for (var i = 0; i < tests.length; i++) + tests[i](); + + cleanup(); +} + diff --git a/storage/test/unit/test_bug-429521.js b/storage/test/unit/test_bug-429521.js new file mode 100644 index 000000000..a9eafc1c2 --- /dev/null +++ b/storage/test/unit/test_bug-429521.js @@ -0,0 +1,46 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +function setup() { + getOpenedDatabase().createTable("t1", "x TEXT"); + + var stmt = createStatement("INSERT INTO t1 (x) VALUES ('/mozilla.org/20070129_1/Europe/Berlin')"); + stmt.execute(); + stmt.finalize(); +} + +function test_bug429521() { + var stmt = createStatement( + "SELECT DISTINCT(zone) FROM (" + + "SELECT x AS zone FROM t1 WHERE x LIKE '/mozilla.org%'" + + ");"); + + print("*** test_bug429521: started"); + + try { + while (stmt.executeStep()) { + print("*** test_bug429521: step() Read wrapper.row.zone"); + + // BUG: the print commands after the following statement + // are never executed. Script stops immediately. + var tzId = stmt.row.zone; + + print("*** test_bug429521: step() Read wrapper.row.zone finished"); + } + } catch (e) { + print("*** test_bug429521: " + e); + } + + print("*** test_bug429521: finished"); + + stmt.finalize(); +} + +function run_test() { + setup(); + + test_bug429521(); + + cleanup(); +} diff --git a/storage/test/unit/test_bug-444233.js b/storage/test/unit/test_bug-444233.js new file mode 100644 index 000000000..eb315f934 --- /dev/null +++ b/storage/test/unit/test_bug-444233.js @@ -0,0 +1,51 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +function setup() { + // Create the table + getOpenedDatabase().createTable("test_bug444233", + "id INTEGER PRIMARY KEY, value TEXT"); + + // Insert dummy data, using wrapper methods + var stmt = createStatement("INSERT INTO test_bug444233 (value) VALUES (:value)"); + stmt.params.value = "value1"; + stmt.execute(); + stmt.finalize(); + + stmt = createStatement("INSERT INTO test_bug444233 (value) VALUES (:value)"); + stmt.params.value = "value2"; + stmt.execute(); + stmt.finalize(); +} + +function test_bug444233() { + print("*** test_bug444233: started"); + + // Check that there are 2 results + var stmt = createStatement("SELECT COUNT(*) AS number FROM test_bug444233"); + do_check_true(stmt.executeStep()); + do_check_eq(2, stmt.row.number); + stmt.reset(); + stmt.finalize(); + + print("*** test_bug444233: doing delete"); + + // Now try to delete using IN + // Cheating since we know ids are 1,2 + try { + var ids = [1, 2]; + stmt = createStatement("DELETE FROM test_bug444233 WHERE id IN (:ids)"); + stmt.params.ids = ids; + } catch (e) { + print("*** test_bug444233: successfully caught exception"); + } + stmt.finalize(); +} + +function run_test() { + setup(); + test_bug444233(); + cleanup(); +} + diff --git a/storage/test/unit/test_cache_size.js b/storage/test/unit/test_cache_size.js new file mode 100644 index 000000000..e0a5e8723 --- /dev/null +++ b/storage/test/unit/test_cache_size.js @@ -0,0 +1,72 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// This file tests that dbs of various page sizes are using the right cache +// size (bug 703113). + +/** + * In order to change the cache size, we must open a DB, change the page + * size, create a table, close the DB, then re-open the DB. We then check + * the cache size after reopening. + * + * @param dbOpener + * function that opens the DB specified in file + * @param file + * file holding the database + * @param pageSize + * the DB's page size + * @param expectedCacheSize + * the expected cache size for the given page size + */ +function check_size(dbOpener, file, pageSize, expectedCacheSize) +{ + // Open the DB, immediately change its page size. + let db = dbOpener(file); + db.executeSimpleSQL("PRAGMA page_size = " + pageSize); + + // Check the page size change worked. + let stmt = db.createStatement("PRAGMA page_size"); + do_check_true(stmt.executeStep()); + do_check_eq(stmt.row.page_size, pageSize); + stmt.finalize(); + + // Create a simple table. + db.executeSimpleSQL("CREATE TABLE test ( id INTEGER PRIMARY KEY )"); + + // Close and re-open the DB. + db.close(); + db = dbOpener(file); + + // Check cache size is as expected. + stmt = db.createStatement("PRAGMA cache_size"); + do_check_true(stmt.executeStep()); + do_check_eq(stmt.row.cache_size, expectedCacheSize); + stmt.finalize(); +} + +function new_file(name) +{ + let file = dirSvc.get("ProfD", Ci.nsIFile); + file.append(name + ".sqlite"); + do_check_false(file.exists()); + return file; +} + +function run_test() +{ + const kExpectedCacheSize = -2048; // 2MiB + + let pageSizes = [ + 1024, + 4096, + 32768, + ]; + + for (let i = 0; i < pageSizes.length; i++) { + let pageSize = pageSizes[i]; + check_size(getDatabase, + new_file("shared" + pageSize), pageSize, kExpectedCacheSize); + check_size(getService().openUnsharedDatabase, + new_file("unshared" + pageSize), pageSize, kExpectedCacheSize); + } +} diff --git a/storage/test/unit/test_chunk_growth.js b/storage/test/unit/test_chunk_growth.js new file mode 100644 index 000000000..2cf91fe16 --- /dev/null +++ b/storage/test/unit/test_chunk_growth.js @@ -0,0 +1,52 @@ +// This file tests SQLITE_FCNTL_CHUNK_SIZE behaves as expected + +function run_sql(d, sql) { + var stmt = d.createStatement(sql); + stmt.execute(); + stmt.finalize(); +} + +function new_file(name) { + var file = dirSvc.get("ProfD", Ci.nsIFile); + file.append(name); + return file; +} + +function get_size(name) { + return new_file(name).fileSize; +} + +function run_test() { + const filename = "chunked.sqlite"; + const CHUNK_SIZE = 512 * 1024; + var d = getDatabase(new_file(filename)); + try { + d.setGrowthIncrement(CHUNK_SIZE, ""); + } catch (e) { + if (e.result != Cr.NS_ERROR_FILE_TOO_BIG) { + throw e; + } + print("Too little free space to set CHUNK_SIZE!"); + return; + } + run_sql(d, "CREATE TABLE bloat(data varchar)"); + + var orig_size = get_size(filename); + /* Dump in at least 32K worth of data. + * While writing ensure that the file size growth in chunksize set above. + */ + const str1024 = new Array(1024).join("T"); + for (var i = 0; i < 32; i++) { + run_sql(d, "INSERT INTO bloat VALUES('" + str1024 + "')"); + var size = get_size(filename); + // Must not grow in small increments. + do_check_true(size == orig_size || size >= CHUNK_SIZE); + } + /* In addition to growing in chunk-size increments, the db + * should shrink in chunk-size increments too. + */ + run_sql(d, "DELETE FROM bloat"); + run_sql(d, "VACUUM"); + do_check_true(get_size(filename) >= CHUNK_SIZE); +} + diff --git a/storage/test/unit/test_connection_asyncClose.js b/storage/test/unit/test_connection_asyncClose.js new file mode 100644 index 000000000..7704dcc81 --- /dev/null +++ b/storage/test/unit/test_connection_asyncClose.js @@ -0,0 +1,125 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* + * Thorough branch coverage for asyncClose. + * + * Coverage of asyncClose by connection state at time of AsyncClose invocation: + * - (asyncThread && mDBConn) => AsyncCloseConnection used, actually closes + * - test_asyncClose_does_not_complete_before_statements + * - test_double_asyncClose_throws + * - test_asyncClose_does_not_throw_without_callback + * - (asyncThread && !mDBConn) => AsyncCloseConnection used, although no close + * is required. Note that this is only possible in the event that + * openAsyncDatabase was used and we failed to open the database. + * Additionally, the async connection will never be exposed to the caller and + * AsyncInitDatabase will be the one to (automatically) call AsyncClose. + * - test_asyncClose_failed_open + * - (!asyncThread && mDBConn) => Close() invoked, actually closes + * - test_asyncClose_on_sync_db + * - (!asyncThread && !mDBConn) => Close() invoked, no close needed, errors. + * This happens if the database has already been closed. + * - test_double_asyncClose_throws + */ + + +/** + * Sanity check that our close indeed happens after asynchronously executed + * statements scheduled during the same turn of the event loop. Note that we + * just care that the statement says it completed without error, we're not + * worried that the close will happen and then the statement will magically + * complete. + */ +add_task(function* test_asyncClose_does_not_complete_before_statements() { + let db = getService().openDatabase(getTestDB()); + let stmt = db.createStatement("SELECT * FROM sqlite_master"); + // Issue the executeAsync but don't yield for it... + let asyncStatementPromise = executeAsync(stmt); + stmt.finalize(); + + // Issue the close. (And now the order of yielding doesn't matter.) + // Branch coverage: (asyncThread && mDBConn) + yield asyncClose(db); + equal((yield asyncStatementPromise), + Ci.mozIStorageStatementCallback.REASON_FINISHED); +}); + +/** + * Open an async database (ensures the async thread is created) and then invoke + * AsyncClose() twice without yielding control flow. The first will initiate + * the actual async close after calling setClosedState which synchronously + * impacts what the second call will observe. The second call will then see the + * async thread is not available and fall back to invoking Close() which will + * notice the mDBConn is already gone. + */ +add_task(function* test_double_asyncClose_throws() { + let db = yield openAsyncDatabase(getTestDB()); + + // (Don't yield control flow yet, save the promise for after we make the + // second call.) + // Branch coverage: (asyncThread && mDBConn) + let realClosePromise = yield asyncClose(db); + try { + // Branch coverage: (!asyncThread && !mDBConn) + db.asyncClose(); + ok(false, "should have thrown"); + } catch (e) { + equal(e.result, Cr.NS_ERROR_NOT_INITIALIZED); + } + + yield realClosePromise; +}); + +/** + * Create a sync db connection and never take it asynchronous and then call + * asyncClose on it. This will bring the async thread to life to perform the + * shutdown to avoid blocking the main thread, although we won't be able to + * tell the difference between this happening and the method secretly shunting + * to close(). + */ +add_task(function* test_asyncClose_on_sync_db() { + let db = getService().openDatabase(getTestDB()); + + // Branch coverage: (!asyncThread && mDBConn) + yield asyncClose(db); + ok(true, 'closed sync connection asynchronously'); +}); + +/** + * Fail to asynchronously open a DB in order to get an async thread existing + * without having an open database and asyncClose invoked. As per the file + * doc-block, note that asyncClose will automatically be invoked by the + * AsyncInitDatabase when it fails to open the database. We will never be + * provided with a reference to the connection and so cannot call AsyncClose on + * it ourselves. + */ +add_task(function* test_asyncClose_failed_open() { + // This will fail and the promise will be rejected. + let openPromise = openAsyncDatabase(getFakeDB()); + yield openPromise.then( + () => { + ok(false, 'we should have failed to open the db; this test is broken!'); + }, + () => { + ok(true, 'correctly failed to open db; bg asyncClose should happen'); + } + ); + // (NB: we are unable to observe the thread shutdown, but since we never open + // a database, this test is not going to interfere with other tests so much.) +}); + +// THE TEST BELOW WANTS TO BE THE LAST TEST WE RUN. DO NOT MAKE IT SAD. +/** + * Verify that asyncClose without a callback does not explode. Without a + * callback the shutdown is not actually observable, so we run this test last + * in order to avoid weird overlaps. + */ +add_task(function* test_asyncClose_does_not_throw_without_callback() { + let db = yield openAsyncDatabase(getTestDB()); + // Branch coverage: (asyncThread && mDBConn) + db.asyncClose(); + ok(true, 'if we shutdown cleanly and do not crash, then we succeeded'); +}); +// OBEY SHOUTING UPPER-CASE COMMENTS. +// ADD TESTS ABOVE THE FORMER TEST, NOT BELOW IT. diff --git a/storage/test/unit/test_connection_executeAsync.js b/storage/test/unit/test_connection_executeAsync.js new file mode 100644 index 000000000..e56d98e55 --- /dev/null +++ b/storage/test/unit/test_connection_executeAsync.js @@ -0,0 +1,171 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* + * This file tests the functionality of mozIStorageConnection::executeAsync for + * both mozIStorageStatement and mozIStorageAsyncStatement. + * + * A single database connection is used for the entirety of the test, which is + * a legacy thing, but we otherwise use the modern promise-based driver and + * async helpers. + */ + +const INTEGER = 1; +const TEXT = "this is test text"; +const REAL = 3.23; +const BLOB = [1, 2]; + +add_task(function* test_first_create_and_add() { + // synchronously open the database and let gDBConn hold onto it because we + // use this database + let db = getOpenedDatabase(); + // synchronously set up our table *that will be used for the rest of the file* + db.executeSimpleSQL( + "CREATE TABLE test (" + + "id INTEGER, " + + "string TEXT, " + + "number REAL, " + + "nuller NULL, " + + "blober BLOB" + + ")" + ); + + let stmts = []; + stmts[0] = db.createStatement( + "INSERT INTO test (id, string, number, nuller, blober) VALUES (?, ?, ?, ?, ?)" + ); + stmts[0].bindByIndex(0, INTEGER); + stmts[0].bindByIndex(1, TEXT); + stmts[0].bindByIndex(2, REAL); + stmts[0].bindByIndex(3, null); + stmts[0].bindBlobByIndex(4, BLOB, BLOB.length); + stmts[1] = getOpenedDatabase().createAsyncStatement( + "INSERT INTO test (string, number, nuller, blober) VALUES (?, ?, ?, ?)" + ); + stmts[1].bindByIndex(0, TEXT); + stmts[1].bindByIndex(1, REAL); + stmts[1].bindByIndex(2, null); + stmts[1].bindBlobByIndex(3, BLOB, BLOB.length); + + // asynchronously execute the statements + let execResult = yield executeMultipleStatementsAsync( + db, + stmts, + function(aResultSet) { + ok(false, 'we only did inserts so we should not have gotten results!'); + }); + equal(Ci.mozIStorageStatementCallback.REASON_FINISHED, execResult, + 'execution should have finished successfully.'); + + // Check that the result is in the table + let stmt = db.createStatement( + "SELECT string, number, nuller, blober FROM test WHERE id = ?" + ); + stmt.bindByIndex(0, INTEGER); + try { + do_check_true(stmt.executeStep()); + do_check_eq(TEXT, stmt.getString(0)); + do_check_eq(REAL, stmt.getDouble(1)); + do_check_true(stmt.getIsNull(2)); + let count = { value: 0 }; + let blob = { value: null }; + stmt.getBlob(3, count, blob); + do_check_eq(BLOB.length, count.value); + for (let i = 0; i < BLOB.length; i++) + do_check_eq(BLOB[i], blob.value[i]); + } + finally { + stmt.finalize(); + } + + // Make sure we have two rows in the table + stmt = db.createStatement( + "SELECT COUNT(1) FROM test" + ); + try { + do_check_true(stmt.executeStep()); + do_check_eq(2, stmt.getInt32(0)); + } + finally { + stmt.finalize(); + } + + stmts[0].finalize(); + stmts[1].finalize(); +}); + +add_task(function* test_last_multiple_bindings_on_statements() { + // This tests to make sure that we pass all the statements multiply bound + // parameters when we call executeAsync. + const AMOUNT_TO_ADD = 5; + const ITERATIONS = 5; + + let stmts = []; + let db = getOpenedDatabase(); + let sqlString = "INSERT INTO test (id, string, number, nuller, blober) " + + "VALUES (:int, :text, :real, :null, :blob)"; + // We run the same statement twice, and should insert 2 * AMOUNT_TO_ADD. + for (let i = 0; i < ITERATIONS; i++) { + // alternate the type of statement we create + if (i % 2) + stmts[i] = db.createStatement(sqlString); + else + stmts[i] = db.createAsyncStatement(sqlString); + + let params = stmts[i].newBindingParamsArray(); + for (let j = 0; j < AMOUNT_TO_ADD; j++) { + let bp = params.newBindingParams(); + bp.bindByName("int", INTEGER); + bp.bindByName("text", TEXT); + bp.bindByName("real", REAL); + bp.bindByName("null", null); + bp.bindBlobByName("blob", BLOB, BLOB.length); + params.addParams(bp); + } + stmts[i].bindParameters(params); + } + + // Get our current number of rows in the table. + let currentRows = 0; + let countStmt = getOpenedDatabase().createStatement( + "SELECT COUNT(1) AS count FROM test" + ); + try { + do_check_true(countStmt.executeStep()); + currentRows = countStmt.row.count; + } + finally { + countStmt.reset(); + } + + // Execute asynchronously. + let execResult = yield executeMultipleStatementsAsync( + db, + stmts, + function(aResultSet) { + ok(false, 'we only did inserts so we should not have gotten results!'); + }); + equal(Ci.mozIStorageStatementCallback.REASON_FINISHED, execResult, + 'execution should have finished successfully.'); + + // Check to make sure we added all of our rows. + try { + do_check_true(countStmt.executeStep()); + do_check_eq(currentRows + (ITERATIONS * AMOUNT_TO_ADD), + countStmt.row.count); + } + finally { + countStmt.finalize(); + } + + stmts.forEach(stmt => stmt.finalize()); + + // we are the last test using this connection and since it has gone async + // we *must* call asyncClose on it. + yield asyncClose(db); + gDBConn = null; +}); + +// If you add a test down here you will need to move the asyncClose or clean +// things up a little more. diff --git a/storage/test/unit/test_connection_executeSimpleSQLAsync.js b/storage/test/unit/test_connection_executeSimpleSQLAsync.js new file mode 100644 index 000000000..142cc8e14 --- /dev/null +++ b/storage/test/unit/test_connection_executeSimpleSQLAsync.js @@ -0,0 +1,79 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* + * This file tests the functionality of + * mozIStorageAsyncConnection::executeSimpleSQLAsync. + */ + +const INTEGER = 1; +const TEXT = "this is test text"; +const REAL = 3.23; + +add_task(function* test_create_and_add() { + let adb = yield openAsyncDatabase(getTestDB()); + + let completion = yield executeSimpleSQLAsync(adb, + "CREATE TABLE test (id INTEGER, string TEXT, number REAL)"); + + do_check_eq(Ci.mozIStorageStatementCallback.REASON_FINISHED, completion); + + completion = yield executeSimpleSQLAsync(adb, + "INSERT INTO test (id, string, number) " + + "VALUES (" + INTEGER + ", \"" + TEXT + "\", " + REAL + ")"); + + do_check_eq(Ci.mozIStorageStatementCallback.REASON_FINISHED, completion); + + let result = null; + + completion = yield executeSimpleSQLAsync(adb, + "SELECT string, number FROM test WHERE id = 1", + function (aResultSet) { + result = aResultSet.getNextRow(); + do_check_eq(2, result.numEntries); + do_check_eq(TEXT, result.getString(0)); + do_check_eq(REAL, result.getDouble(1)); + } + ); + + do_check_eq(Ci.mozIStorageStatementCallback.REASON_FINISHED, completion); + do_check_neq(result, null); + result = null; + + yield executeSimpleSQLAsync(adb, "SELECT COUNT(0) FROM test", + function (aResultSet) { + result = aResultSet.getNextRow(); + do_check_eq(1, result.getInt32(0)); + }); + + do_check_neq(result, null); + + yield asyncClose(adb); +}); + + +add_task(function* test_asyncClose_does_not_complete_before_statement() { + let adb = yield openAsyncDatabase(getTestDB()); + let executed = false; + + let reason = yield executeSimpleSQLAsync(adb, "SELECT * FROM test", + function (aResultSet) { + let result = aResultSet.getNextRow(); + + do_check_neq(result, null); + do_check_eq(3, result.numEntries); + do_check_eq(INTEGER, result.getInt32(0)); + do_check_eq(TEXT, result.getString(1)); + do_check_eq(REAL, result.getDouble(2)); + executed = true; + } + ); + + do_check_eq(Ci.mozIStorageStatementCallback.REASON_FINISHED, reason); + + // Ensure that the statement executed to completion. + do_check_true(executed); + + yield asyncClose(adb); +}); diff --git a/storage/test/unit/test_js_helpers.js b/storage/test/unit/test_js_helpers.js new file mode 100644 index 000000000..dde9fac20 --- /dev/null +++ b/storage/test/unit/test_js_helpers.js @@ -0,0 +1,125 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sw=2 ts=2 sts=2 et : */ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/** + * This file tests that the JS language helpers in various ways. + */ + +// Test Functions + +function test_params_enumerate() +{ + let stmt = createStatement( + "SELECT * FROM test WHERE id IN (:a, :b, :c)" + ); + + // Make sure they are right. + let expected = ["a", "b", "c"]; + let index = 0; + for (let name in stmt.params) { + if (name == "QueryInterface") + continue; + do_check_eq(name, expected[index++]); + } +} + +function test_params_prototype() +{ + let stmt = createStatement( + "SELECT * FROM sqlite_master" + ); + + // Set a property on the prototype and make sure it exist (will not be a + // bindable parameter, however). + Object.getPrototypeOf(stmt.params).test = 2; + do_check_eq(stmt.params.test, 2); + stmt.finalize(); +} + +function test_row_prototype() +{ + let stmt = createStatement( + "SELECT * FROM sqlite_master" + ); + + do_check_true(stmt.executeStep()); + + // Set a property on the prototype and make sure it exists (will not be in the + // results, however). + Object.getPrototypeOf(stmt.row).test = 2; + do_check_eq(stmt.row.test, 2); + + // Clean up after ourselves. + delete Object.getPrototypeOf(stmt.row).test; + stmt.finalize(); +} + +function test_params_gets_sync() +{ + // Added for bug 562866. + /* + let stmt = createStatement( + "SELECT * FROM test WHERE id IN (:a, :b, :c)" + ); + + // Make sure we do not assert in getting the value. + let originalCount = Object.getOwnPropertyNames(stmt.params).length; + let expected = ["a", "b", "c"]; + for (let name of expected) { + stmt.params[name]; + } + + // Now make sure we didn't magically get any additional properties. + let finalCount = Object.getOwnPropertyNames(stmt.params).length; + do_check_eq(originalCount + expected.length, finalCount); + */ +} + +function test_params_gets_async() +{ + // Added for bug 562866. + /* + let stmt = createAsyncStatement( + "SELECT * FROM test WHERE id IN (:a, :b, :c)" + ); + + // Make sure we do not assert in getting the value. + let originalCount = Object.getOwnPropertyNames(stmt.params).length; + let expected = ["a", "b", "c"]; + for (let name of expected) { + stmt.params[name]; + } + + // Now make sure we didn't magically get any additional properties. + let finalCount = Object.getOwnPropertyNames(stmt.params).length; + do_check_eq(originalCount + expected.length, finalCount); + */ +} + +// Test Runner + +var tests = [ + test_params_enumerate, + test_params_prototype, + test_row_prototype, + test_params_gets_sync, + test_params_gets_async, +]; +function run_test() +{ + cleanup(); + + // Create our database. + getOpenedDatabase().executeSimpleSQL( + "CREATE TABLE test (" + + "id INTEGER PRIMARY KEY " + + ")" + ); + + // Run the tests. + tests.forEach(test => test()); +} diff --git a/storage/test/unit/test_levenshtein.js b/storage/test/unit/test_levenshtein.js new file mode 100644 index 000000000..ced141abd --- /dev/null +++ b/storage/test/unit/test_levenshtein.js @@ -0,0 +1,74 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// This file tests the Levenshtein Distance function we've registered. + +function createUtf16Database() +{ + print("Creating the in-memory UTF-16-encoded database."); + let conn = getService().openSpecialDatabase("memory"); + conn.executeSimpleSQL("PRAGMA encoding = 'UTF-16'"); + + print("Make sure the encoding was set correctly and is now UTF-16."); + let stmt = conn.createStatement("PRAGMA encoding"); + do_check_true(stmt.executeStep()); + let enc = stmt.getString(0); + stmt.finalize(); + + // The value returned will actually be UTF-16le or UTF-16be. + do_check_true(enc === "UTF-16le" || enc === "UTF-16be"); + + return conn; +} + +function check_levenshtein(db, s, t, expectedDistance) +{ + var stmt = db.createStatement("SELECT levenshteinDistance(:s, :t) AS result"); + stmt.params.s = s; + stmt.params.t = t; + try { + do_check_true(stmt.executeStep()); + do_check_eq(expectedDistance, stmt.row.result); + } finally { + stmt.reset(); + stmt.finalize(); + } +} + +function testLevenshtein(db) +{ + // Basic tests. + check_levenshtein(db, "", "", 0); + check_levenshtein(db, "foo", "", 3); + check_levenshtein(db, "", "bar", 3); + check_levenshtein(db, "yellow", "hello", 2); + check_levenshtein(db, "gumbo", "gambol", 2); + check_levenshtein(db, "kitten", "sitten", 1); + check_levenshtein(db, "sitten", "sittin", 1); + check_levenshtein(db, "sittin", "sitting", 1); + check_levenshtein(db, "kitten", "sitting", 3); + check_levenshtein(db, "Saturday", "Sunday", 3); + check_levenshtein(db, "YHCQPGK", "LAHYQQKPGKA", 6); + + // Test SQL NULL handling. + check_levenshtein(db, "foo", null, null); + check_levenshtein(db, null, "bar", null); + check_levenshtein(db, null, null, null); + + // The levenshteinDistance function allocates temporary memory on the stack + // if it can. Test some strings long enough to force a heap allocation. + var dots1000 = Array(1001).join("."); + var dashes1000 = Array(1001).join("-"); + check_levenshtein(db, dots1000, dashes1000, 1000); +} + +function run_test() +{ + testLevenshtein(getOpenedDatabase()); + testLevenshtein(createUtf16Database()); +} + + + + diff --git a/storage/test/unit/test_like.js b/storage/test/unit/test_like.js new file mode 100644 index 000000000..b42f2eb27 --- /dev/null +++ b/storage/test/unit/test_like.js @@ -0,0 +1,202 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// This file tests our LIKE implementation since we override it for unicode + +function setup() +{ + getOpenedDatabase().createTable("t1", "x TEXT"); + + var stmt = createStatement("INSERT INTO t1 (x) VALUES ('a')"); + stmt.execute(); + stmt.finalize(); + + stmt = createStatement("INSERT INTO t1 (x) VALUES ('ab')"); + stmt.execute(); + stmt.finalize(); + + stmt = createStatement("INSERT INTO t1 (x) VALUES ('abc')"); + stmt.execute(); + stmt.finalize(); + + stmt = createStatement("INSERT INTO t1 (x) VALUES ('abcd')"); + stmt.execute(); + stmt.finalize(); + + stmt = createStatement("INSERT INTO t1 (x) VALUES ('acd')"); + stmt.execute(); + stmt.finalize(); + + stmt = createStatement("INSERT INTO t1 (x) VALUES ('abd')"); + stmt.execute(); + stmt.finalize(); + + stmt = createStatement("INSERT INTO t1 (x) VALUES ('bc')"); + stmt.execute(); + stmt.finalize(); + + stmt = createStatement("INSERT INTO t1 (x) VALUES ('bcd')"); + stmt.execute(); + stmt.finalize(); + + stmt = createStatement("INSERT INTO t1 (x) VALUES ('xyz')"); + stmt.execute(); + stmt.finalize(); + + stmt = createStatement("INSERT INTO t1 (x) VALUES ('ABC')"); + stmt.execute(); + stmt.finalize(); + + stmt = createStatement("INSERT INTO t1 (x) VALUES ('CDE')"); + stmt.execute(); + stmt.finalize(); + + stmt = createStatement("INSERT INTO t1 (x) VALUES ('ABC abc xyz')"); + stmt.execute(); + stmt.finalize(); +} + +function test_count() +{ + var stmt = createStatement("SELECT count(*) FROM t1;"); + do_check_true(stmt.executeStep()); + do_check_eq(stmt.getInt32(0), 12); + stmt.reset(); + stmt.finalize(); +} + +function test_like_1() +{ + var stmt = createStatement("SELECT x FROM t1 WHERE x LIKE ?;"); + stmt.bindByIndex(0, 'abc'); + var solutions = ["abc", "ABC"]; + do_check_true(stmt.executeStep()); + do_check_true(solutions.indexOf(stmt.getString(0)) != -1); + do_check_true(stmt.executeStep()); + do_check_true(solutions.indexOf(stmt.getString(0)) != -1); + do_check_false(stmt.executeStep()); + stmt.reset(); + stmt.finalize(); +} + +function test_like_2() +{ + var stmt = createStatement("SELECT x FROM t1 WHERE x LIKE ?;"); + stmt.bindByIndex(0, 'ABC'); + var solutions = ["abc", "ABC"]; + do_check_true(stmt.executeStep()); + do_check_true(solutions.indexOf(stmt.getString(0)) != -1); + do_check_true(stmt.executeStep()); + do_check_true(solutions.indexOf(stmt.getString(0)) != -1); + do_check_false(stmt.executeStep()); + stmt.reset(); + stmt.finalize(); +} + +function test_like_3() +{ + var stmt = createStatement("SELECT x FROM t1 WHERE x LIKE ?;"); + stmt.bindByIndex(0, 'aBc'); + var solutions = ["abc", "ABC"]; + do_check_true(stmt.executeStep()); + do_check_true(solutions.indexOf(stmt.getString(0)) != -1); + do_check_true(stmt.executeStep()); + do_check_true(solutions.indexOf(stmt.getString(0)) != -1); + do_check_false(stmt.executeStep()); + stmt.reset(); + stmt.finalize(); +} + +function test_like_4() +{ + var stmt = createStatement("SELECT x FROM t1 WHERE x LIKE ?;"); + stmt.bindByIndex(0, 'abc%'); + var solutions = ["abc", "abcd", "ABC", "ABC abc xyz"]; + do_check_true(stmt.executeStep()); + do_check_true(solutions.indexOf(stmt.getString(0)) != -1); + do_check_true(stmt.executeStep()); + do_check_true(solutions.indexOf(stmt.getString(0)) != -1); + do_check_true(stmt.executeStep()); + do_check_true(solutions.indexOf(stmt.getString(0)) != -1); + do_check_true(stmt.executeStep()); + do_check_true(solutions.indexOf(stmt.getString(0)) != -1); + do_check_false(stmt.executeStep()); + stmt.reset(); + stmt.finalize(); +} + +function test_like_5() +{ + var stmt = createStatement("SELECT x FROM t1 WHERE x LIKE ?;"); + stmt.bindByIndex(0, 'a_c'); + var solutions = ["abc", "ABC"]; + do_check_true(stmt.executeStep()); + do_check_true(solutions.indexOf(stmt.getString(0)) != -1); + do_check_true(stmt.executeStep()); + do_check_true(solutions.indexOf(stmt.getString(0)) != -1); + do_check_false(stmt.executeStep()); + stmt.reset(); + stmt.finalize(); +} + +function test_like_6() +{ + var stmt = createStatement("SELECT x FROM t1 WHERE x LIKE ?;"); + stmt.bindByIndex(0, 'ab%d'); + var solutions = ["abcd", "abd"]; + do_check_true(stmt.executeStep()); + do_check_true(solutions.indexOf(stmt.getString(0)) != -1); + do_check_true(stmt.executeStep()); + do_check_true(solutions.indexOf(stmt.getString(0)) != -1); + do_check_false(stmt.executeStep()); + stmt.reset(); + stmt.finalize(); +} + +function test_like_7() +{ + var stmt = createStatement("SELECT x FROM t1 WHERE x LIKE ?;"); + stmt.bindByIndex(0, 'a_c%'); + var solutions = ["abc", "abcd", "ABC", "ABC abc xyz"]; + do_check_true(stmt.executeStep()); + do_check_true(solutions.indexOf(stmt.getString(0)) != -1); + do_check_true(stmt.executeStep()); + do_check_true(solutions.indexOf(stmt.getString(0)) != -1); + do_check_true(stmt.executeStep()); + do_check_true(solutions.indexOf(stmt.getString(0)) != -1); + do_check_true(stmt.executeStep()); + do_check_true(solutions.indexOf(stmt.getString(0)) != -1); + do_check_false(stmt.executeStep()); + stmt.reset(); + stmt.finalize(); +} + +function test_like_8() +{ + var stmt = createStatement("SELECT x FROM t1 WHERE x LIKE ?;"); + stmt.bindByIndex(0, '%bcd'); + var solutions = ["abcd", "bcd"]; + do_check_true(stmt.executeStep()); + do_check_true(solutions.indexOf(stmt.getString(0)) != -1); + do_check_true(stmt.executeStep()); + do_check_true(solutions.indexOf(stmt.getString(0)) != -1); + do_check_false(stmt.executeStep()); + stmt.reset(); + stmt.finalize(); +} + +var tests = [test_count, test_like_1, test_like_2, test_like_3, test_like_4, + test_like_5, test_like_6, test_like_7, test_like_8]; + +function run_test() +{ + setup(); + + for (var i = 0; i < tests.length; i++) { + tests[i](); + } + + cleanup(); +} + diff --git a/storage/test/unit/test_like_escape.js b/storage/test/unit/test_like_escape.js new file mode 100644 index 000000000..414f6237c --- /dev/null +++ b/storage/test/unit/test_like_escape.js @@ -0,0 +1,60 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const LATIN1_AE = "\xc6"; +const LATIN1_ae = "\xe6"; + +function setup() +{ + getOpenedDatabase().createTable("t1", "x TEXT"); + + var stmt = createStatement("INSERT INTO t1 (x) VALUES ('foo/bar_baz%20cheese')"); + stmt.execute(); + stmt.finalize(); + + stmt = createStatement("INSERT INTO t1 (x) VALUES (?1)"); + // insert LATIN_ae, but search on LATIN_AE + stmt.bindByIndex(0, "foo%20" + LATIN1_ae + "/_bar"); + stmt.execute(); + stmt.finalize(); +} + +function test_escape_for_like_ascii() +{ + var stmt = createStatement("SELECT x FROM t1 WHERE x LIKE ?1 ESCAPE '/'"); + var paramForLike = stmt.escapeStringForLIKE("oo/bar_baz%20chees", '/'); + // verify that we escaped / _ and % + do_check_eq(paramForLike, "oo//bar/_baz/%20chees"); + // prepend and append with % for "contains" + stmt.bindByIndex(0, "%" + paramForLike + "%"); + stmt.executeStep(); + do_check_eq("foo/bar_baz%20cheese", stmt.getString(0)); + stmt.finalize(); +} + +function test_escape_for_like_non_ascii() +{ + var stmt = createStatement("SELECT x FROM t1 WHERE x LIKE ?1 ESCAPE '/'"); + var paramForLike = stmt.escapeStringForLIKE("oo%20" + LATIN1_AE + "/_ba", '/'); + // verify that we escaped / _ and % + do_check_eq(paramForLike, "oo/%20" + LATIN1_AE + "///_ba"); + // prepend and append with % for "contains" + stmt.bindByIndex(0, "%" + paramForLike + "%"); + stmt.executeStep(); + do_check_eq("foo%20" + LATIN1_ae + "/_bar", stmt.getString(0)); + stmt.finalize(); +} + +var tests = [test_escape_for_like_ascii, test_escape_for_like_non_ascii]; + +function run_test() +{ + setup(); + + for (var i = 0; i < tests.length; i++) { + tests[i](); + } + + cleanup(); +} diff --git a/storage/test/unit/test_locale_collation.js b/storage/test/unit/test_locale_collation.js new file mode 100644 index 000000000..12ba2b943 --- /dev/null +++ b/storage/test/unit/test_locale_collation.js @@ -0,0 +1,304 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Bug 499990 - Locale-aware collation + * + * Tests our custom, locale-aware collating sequences. + */ + +// The name of the file containing the strings we'll sort during this test. +// The file's data is taken from intl/locale/tests/sort/us-ascii_base.txt and +// and intl/locale/tests/sort/us-ascii_sort.txt. +const DATA_BASENAME = "locale_collation.txt"; + +// The test data from DATA_BASENAME is read into this array. +var gStrings; + +// A collation created from the application's locale. Used by localeCompare(). +var gLocaleCollation; + +// A connection to our in-memory UTF-16-encoded database. +var gUtf16Conn; + +// Helper Functions + +/** + * Since we create a UTF-16 database we have to clean it up, in addition to + * the normal cleanup of Storage tests. + */ +function cleanupLocaleTests() +{ + print("-- Cleaning up test_locale_collation.js suite."); + gUtf16Conn.close(); + cleanup(); +} + +/** + * Creates a test database similar to the default one created in + * head_storage.js, except that this one uses UTF-16 encoding. + * + * @return A connection to the database. + */ +function createUtf16Database() +{ + print("Creating the in-memory UTF-16-encoded database."); + let conn = getService().openSpecialDatabase("memory"); + conn.executeSimpleSQL("PRAGMA encoding = 'UTF-16'"); + + print("Make sure the encoding was set correctly and is now UTF-16."); + let stmt = conn.createStatement("PRAGMA encoding"); + do_check_true(stmt.executeStep()); + let enc = stmt.getString(0); + stmt.finalize(); + + // The value returned will actually be UTF-16le or UTF-16be. + do_check_true(enc === "UTF-16le" || enc === "UTF-16be"); + + return conn; +} + +/** + * Compares aActual to aExpected, ensuring that the numbers and orderings of + * the two arrays' elements are the same. + * + * @param aActual + * An array of strings retrieved from the database. + * @param aExpected + * An array of strings to which aActual should be equivalent. + */ +function ensureResultsAreCorrect(aActual, aExpected) +{ + print("Actual results: " + aActual); + print("Expected results: " + aExpected); + + do_check_eq(aActual.length, aExpected.length); + for (let i = 0; i < aActual.length; i++) + do_check_eq(aActual[i], aExpected[i]); +} + +/** + * Synchronously SELECTs all rows from the test table of the given database + * using the given collation. + * + * @param aCollation + * The name of one of our custom locale collations. The rows are + * ordered by this collation. + * @param aConn + * A connection to either the UTF-8 database or the UTF-16 database. + * @return The resulting strings in an array. + */ +function getResults(aCollation, aConn) +{ + let results = []; + let stmt = aConn.createStatement("SELECT t FROM test " + + "ORDER BY t COLLATE " + aCollation + " ASC"); + while (stmt.executeStep()) + results.push(stmt.row.t); + stmt.finalize(); + return results; +} + +/** + * Inserts strings into our test table of the given database in the order given. + * + * @param aStrings + * An array of strings. + * @param aConn + * A connection to either the UTF-8 database or the UTF-16 database. + */ +function initTableWithStrings(aStrings, aConn) +{ + print("Initializing test table."); + + aConn.executeSimpleSQL("DROP TABLE IF EXISTS test"); + aConn.createTable("test", "t TEXT"); + let stmt = aConn.createStatement("INSERT INTO test (t) VALUES (:t)"); + aStrings.forEach(function (str) { + stmt.params.t = str; + stmt.execute(); + stmt.reset(); + }); + stmt.finalize(); +} + +/** + * Returns a sorting function suitable for passing to Array.prototype.sort(). + * The returned function uses the application's locale to compare strings. + * + * @param aCollation + * The name of one of our custom locale collations. The sorting + * strength is computed from this value. + * @return A function to use as a sorting callback. + */ +function localeCompare(aCollation) +{ + var strength; + + switch (aCollation) { + case "locale": + strength = Ci.nsICollation.kCollationCaseInSensitive; + break; + case "locale_case_sensitive": + strength = Ci.nsICollation.kCollationAccentInsenstive; + break; + case "locale_accent_sensitive": + strength = Ci.nsICollation.kCollationCaseInsensitiveAscii; + break; + case "locale_case_accent_sensitive": + strength = Ci.nsICollation.kCollationCaseSensitive; + break; + default: + do_throw("Error in test: unknown collation '" + aCollation + "'"); + break; + } + return function (aStr1, aStr2) { + return gLocaleCollation.compareString(strength, aStr1, aStr2); + }; +} + +/** + * Reads in the test data from the file DATA_BASENAME and returns it as an array + * of strings. + * + * @return The test data as an array of strings. + */ +function readTestData() +{ + print("Reading in test data."); + + let file = do_get_file(DATA_BASENAME); + + let istream = Cc["@mozilla.org/network/file-input-stream;1"]. + createInstance(Ci.nsIFileInputStream); + istream.init(file, -1, -1, 0); + istream.QueryInterface(Components.interfaces.nsILineInputStream); + + let line = {}; + let lines = []; + while (istream.readLine(line)) { + lines.push(line.value); + } + istream.close(); + + return lines; +} + +/** + * Gets the results from the given database using the given collation and + * ensures that they match gStrings sorted by the same collation. + * + * @param aCollation + * The name of one of our custom locale collations. The rows from the + * database and the expected results are ordered by this collation. + * @param aConn + * A connection to either the UTF-8 database or the UTF-16 database. + */ +function runTest(aCollation, aConn) +{ + ensureResultsAreCorrect(getResults(aCollation, aConn), + gStrings.slice(0).sort(localeCompare(aCollation))); +} + +/** + * Gets the results from the UTF-8 database using the given collation and + * ensures that they match gStrings sorted by the same collation. + * + * @param aCollation + * The name of one of our custom locale collations. The rows from the + * database and the expected results are ordered by this collation. + */ +function runUtf8Test(aCollation) +{ + runTest(aCollation, getOpenedDatabase()); +} + +/** + * Gets the results from the UTF-16 database using the given collation and + * ensures that they match gStrings sorted by the same collation. + * + * @param aCollation + * The name of one of our custom locale collations. The rows from the + * database and the expected results are ordered by this collation. + */ +function runUtf16Test(aCollation) +{ + runTest(aCollation, gUtf16Conn); +} + +/** + * Sets up the test suite. + */ +function setup() +{ + print("-- Setting up the test_locale_collation.js suite."); + + gStrings = readTestData(); + + initTableWithStrings(gStrings, getOpenedDatabase()); + + gUtf16Conn = createUtf16Database(); + initTableWithStrings(gStrings, gUtf16Conn); + + let localeSvc = Cc["@mozilla.org/intl/nslocaleservice;1"]. + getService(Ci.nsILocaleService); + let collFact = Cc["@mozilla.org/intl/collation-factory;1"]. + createInstance(Ci.nsICollationFactory); + gLocaleCollation = collFact.CreateCollation(localeSvc.getApplicationLocale()); +} + +// Test Runs + +var gTests = [ + { + desc: "Case and accent sensitive UTF-8", + run: () => runUtf8Test("locale_case_accent_sensitive") + }, + + { + desc: "Case sensitive, accent insensitive UTF-8", + run: () => runUtf8Test("locale_case_sensitive") + }, + + { + desc: "Case insensitive, accent sensitive UTF-8", + run: () => runUtf8Test("locale_accent_sensitive") + }, + + { + desc: "Case and accent insensitive UTF-8", + run: () => runUtf8Test("locale") + }, + + { + desc: "Case and accent sensitive UTF-16", + run: () => runUtf16Test("locale_case_accent_sensitive") + }, + + { + desc: "Case sensitive, accent insensitive UTF-16", + run: () => runUtf16Test("locale_case_sensitive") + }, + + { + desc: "Case insensitive, accent sensitive UTF-16", + run: () => runUtf16Test("locale_accent_sensitive") + }, + + { + desc: "Case and accent insensitive UTF-16", + run: () => runUtf16Test("locale") + }, +]; + +function run_test() +{ + setup(); + gTests.forEach(function (test) { + print("-- Running test: " + test.desc); + test.run(); + }); +} diff --git a/storage/test/unit/test_page_size_is_32k.js b/storage/test/unit/test_page_size_is_32k.js new file mode 100644 index 000000000..a2548d1e6 --- /dev/null +++ b/storage/test/unit/test_page_size_is_32k.js @@ -0,0 +1,35 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// This file tests that dbs are using 32k pagesize + +const kExpectedPageSize = 32768; // 32K +const kExpectedCacheSize = -2048; // 2MiB + +function check_size(db) +{ + var stmt = db.createStatement("PRAGMA page_size"); + stmt.executeStep(); + do_check_eq(stmt.getInt32(0), kExpectedPageSize); + stmt.finalize(); + stmt = db.createStatement("PRAGMA cache_size"); + stmt.executeStep(); + do_check_eq(stmt.getInt32(0), kExpectedCacheSize); + stmt.finalize(); +} + +function new_file(name) +{ + var file = dirSvc.get("ProfD", Ci.nsIFile); + file.append(name + ".sqlite"); + do_check_false(file.exists()); + return file; +} + +function run_test() +{ + check_size(getDatabase(new_file("shared32k"))); + check_size(getService().openUnsharedDatabase(new_file("unshared32k"))); +} + diff --git a/storage/test/unit/test_sqlite_secure_delete.js b/storage/test/unit/test_sqlite_secure_delete.js new file mode 100644 index 000000000..1eff34f70 --- /dev/null +++ b/storage/test/unit/test_sqlite_secure_delete.js @@ -0,0 +1,80 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- + *vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * This file tests to make sure that SQLite was compiled with + * SQLITE_SECURE_DELETE=1. + */ + +// Helper Methods + +/** + * Reads the contents of a file and returns it as a string. + * + * @param aFile + * The file to return from. + * @return the contents of the file in the form of a string. + */ +function getFileContents(aFile) +{ + let fstream = Cc["@mozilla.org/network/file-input-stream;1"]. + createInstance(Ci.nsIFileInputStream); + fstream.init(aFile, -1, 0, 0); + + let bstream = Cc["@mozilla.org/binaryinputstream;1"]. + createInstance(Ci.nsIBinaryInputStream); + bstream.setInputStream(fstream); + return bstream.readBytes(bstream.available()); +} + +// Tests + +add_test(function test_delete_removes_data() { + const TEST_STRING = "SomeRandomStringToFind"; + + let file = getTestDB(); + let db = getService().openDatabase(file); + + // Create the table and insert the data. + db.createTable("test", "data TEXT"); + let stmt = db.createStatement("INSERT INTO test VALUES(:data)"); + stmt.params.data = TEST_STRING; + try { + stmt.execute(); + } + finally { + stmt.finalize(); + } + + // Make sure this test is actually testing what it thinks by making sure the + // string shows up in the database. Because the previous statement was + // automatically wrapped in a transaction, the contents are already on disk. + let contents = getFileContents(file); + do_check_neq(-1, contents.indexOf(TEST_STRING)); + + // Delete the data, and then close the database. + stmt = db.createStatement("DELETE FROM test WHERE data = :data"); + stmt.params.data = TEST_STRING; + try { + stmt.execute(); + } + finally { + stmt.finalize(); + } + db.close(); + + // Check the file to see if the string can be found. + contents = getFileContents(file); + do_check_eq(-1, contents.indexOf(TEST_STRING)); + + run_next_test(); +}); + +function run_test() +{ + cleanup(); + run_next_test(); +} diff --git a/storage/test/unit/test_statement_executeAsync.js b/storage/test/unit/test_statement_executeAsync.js new file mode 100644 index 000000000..edcecb999 --- /dev/null +++ b/storage/test/unit/test_statement_executeAsync.js @@ -0,0 +1,998 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* + * This file tests the functionality of mozIStorageBaseStatement::executeAsync + * for both mozIStorageStatement and mozIStorageAsyncStatement. + */ + +const INTEGER = 1; +const TEXT = "this is test text"; +const REAL = 3.23; +const BLOB = [1, 2]; + +/** + * Execute the given statement asynchronously, spinning an event loop until the + * async statement completes. + * + * @param aStmt + * The statement to execute. + * @param [aOptions={}] + * @param [aOptions.error=false] + * If true we should expect an error whose code we do not care about. If + * a numeric value, that's the error code we expect and require. If we + * are expecting an error, we expect a completion reason of REASON_ERROR. + * Otherwise we expect no error notification and a completion reason of + * REASON_FINISHED. + * @param [aOptions.cancel] + * If true we cancel the pending statement and additionally return the + * pending statement in case you want to further manipulate it. + * @param [aOptions.returnPending=false] + * If true we keep the pending statement around and return it to you. We + * normally avoid doing this to try and minimize the amount of time a + * reference is held to the returned pending statement. + * @param [aResults] + * If omitted, we assume no results rows are expected. If it is a + * number, we assume it is the number of results rows expected. If it is + * a function, we assume it is a function that takes the 1) result row + * number, 2) result tuple, 3) call stack for the original call to + * execAsync as arguments. If it is a list, we currently assume it is a + * list of functions where each function is intended to evaluate the + * result row at that ordinal position and takes the result tuple and + * the call stack for the original call. + */ +function execAsync(aStmt, aOptions, aResults) +{ + let caller = Components.stack.caller; + if (aOptions == null) + aOptions = {}; + + let resultsExpected; + let resultsChecker; + if (aResults == null) { + resultsExpected = 0; + } + else if (typeof aResults == "number") { + resultsExpected = aResults; + } + else if (typeof aResults == "function") { + resultsChecker = aResults; + } + else { // array + resultsExpected = aResults.length; + resultsChecker = function (aResultNum, aTup, aCaller) { + aResults[aResultNum](aTup, aCaller); + }; + } + let resultsSeen = 0; + + let errorCodeExpected = false; + let reasonExpected = Ci.mozIStorageStatementCallback.REASON_FINISHED; + let altReasonExpected = null; + if ("error" in aOptions) { + errorCodeExpected = aOptions.error; + if (errorCodeExpected) + reasonExpected = Ci.mozIStorageStatementCallback.REASON_ERROR; + } + let errorCodeSeen = false; + + if ("cancel" in aOptions && aOptions.cancel) + altReasonExpected = Ci.mozIStorageStatementCallback.REASON_CANCELED; + + let completed = false; + + let listener = { + handleResult(aResultSet) + { + let row, resultsSeenThisCall = 0; + while ((row = aResultSet.getNextRow()) != null) { + if (resultsChecker) + resultsChecker(resultsSeen, row, caller); + resultsSeen++; + resultsSeenThisCall++; + } + + if (!resultsSeenThisCall) + do_throw("handleResult invoked with 0 result rows!"); + }, + handleError(aError) + { + if (errorCodeSeen != false) + do_throw("handleError called when we already had an error!"); + errorCodeSeen = aError.result; + }, + handleCompletion(aReason) + { + if (completed) // paranoia check + do_throw("Received a second handleCompletion notification!", caller); + + if (resultsSeen != resultsExpected) + do_throw("Expected " + resultsExpected + " rows of results but " + + "got " + resultsSeen + " rows!", caller); + + if (errorCodeExpected == true && errorCodeSeen == false) + do_throw("Expected an error, but did not see one.", caller); + else if (errorCodeExpected != errorCodeSeen) + do_throw("Expected error code " + errorCodeExpected + " but got " + + errorCodeSeen, caller); + + if (aReason != reasonExpected && aReason != altReasonExpected) + do_throw("Expected reason " + reasonExpected + + (altReasonExpected ? (" or " + altReasonExpected) : "") + + " but got " + aReason, caller); + + completed = true; + } + }; + + let pending; + // Only get a pending reference if we're supposed to do. + // (note: This does not stop XPConnect from holding onto one currently.) + if (("cancel" in aOptions && aOptions.cancel) || + ("returnPending" in aOptions && aOptions.returnPending)) { + pending = aStmt.executeAsync(listener); + } + else { + aStmt.executeAsync(listener); + } + + if ("cancel" in aOptions && aOptions.cancel) + pending.cancel(); + + let curThread = Components.classes["@mozilla.org/thread-manager;1"] + .getService().currentThread; + while (!completed && !_quit) + curThread.processNextEvent(true); + + return pending; +} + +/** + * Make sure that illegal SQL generates the expected runtime error and does not + * result in any crashes. Async-only since the synchronous case generates the + * error synchronously (and is tested elsewhere). + */ +function test_illegal_sql_async_deferred() +{ + // gibberish + let stmt = makeTestStatement("I AM A ROBOT. DO AS I SAY."); + execAsync(stmt, {error: Ci.mozIStorageError.ERROR}); + stmt.finalize(); + + // legal SQL syntax, but with semantics issues. + stmt = makeTestStatement("SELECT destination FROM funkytown"); + execAsync(stmt, {error: Ci.mozIStorageError.ERROR}); + stmt.finalize(); + + run_next_test(); +} +test_illegal_sql_async_deferred.asyncOnly = true; + +function test_create_table() +{ + // Ensure our table doesn't exist + do_check_false(getOpenedDatabase().tableExists("test")); + + var stmt = makeTestStatement( + "CREATE TABLE test (" + + "id INTEGER, " + + "string TEXT, " + + "number REAL, " + + "nuller NULL, " + + "blober BLOB" + + ")" + ); + execAsync(stmt); + stmt.finalize(); + + // Check that the table has been created + do_check_true(getOpenedDatabase().tableExists("test")); + + // Verify that it's created correctly (this will throw if it wasn't) + let checkStmt = getOpenedDatabase().createStatement( + "SELECT id, string, number, nuller, blober FROM test" + ); + checkStmt.finalize(); + run_next_test(); +} + +function test_add_data() +{ + var stmt = makeTestStatement( + "INSERT INTO test (id, string, number, nuller, blober) " + + "VALUES (?, ?, ?, ?, ?)" + ); + stmt.bindBlobByIndex(4, BLOB, BLOB.length); + stmt.bindByIndex(3, null); + stmt.bindByIndex(2, REAL); + stmt.bindByIndex(1, TEXT); + stmt.bindByIndex(0, INTEGER); + + execAsync(stmt); + stmt.finalize(); + + // Check that the result is in the table + verifyQuery("SELECT string, number, nuller, blober FROM test WHERE id = ?", + INTEGER, + [TEXT, REAL, null, BLOB]); + run_next_test(); +} + +function test_get_data() +{ + var stmt = makeTestStatement( + "SELECT string, number, nuller, blober, id FROM test WHERE id = ?" + ); + stmt.bindByIndex(0, INTEGER); + execAsync(stmt, {}, [ + function (tuple) { + do_check_neq(null, tuple); + + // Check that it's what we expect + do_check_false(tuple.getIsNull(0)); + do_check_eq(tuple.getResultByName("string"), tuple.getResultByIndex(0)); + do_check_eq(TEXT, tuple.getResultByName("string")); + do_check_eq(Ci.mozIStorageValueArray.VALUE_TYPE_TEXT, + tuple.getTypeOfIndex(0)); + + do_check_false(tuple.getIsNull(1)); + do_check_eq(tuple.getResultByName("number"), tuple.getResultByIndex(1)); + do_check_eq(REAL, tuple.getResultByName("number")); + do_check_eq(Ci.mozIStorageValueArray.VALUE_TYPE_FLOAT, + tuple.getTypeOfIndex(1)); + + do_check_true(tuple.getIsNull(2)); + do_check_eq(tuple.getResultByName("nuller"), tuple.getResultByIndex(2)); + do_check_eq(null, tuple.getResultByName("nuller")); + do_check_eq(Ci.mozIStorageValueArray.VALUE_TYPE_NULL, + tuple.getTypeOfIndex(2)); + + do_check_false(tuple.getIsNull(3)); + var blobByName = tuple.getResultByName("blober"); + do_check_eq(BLOB.length, blobByName.length); + var blobByIndex = tuple.getResultByIndex(3); + do_check_eq(BLOB.length, blobByIndex.length); + for (let i = 0; i < BLOB.length; i++) { + do_check_eq(BLOB[i], blobByName[i]); + do_check_eq(BLOB[i], blobByIndex[i]); + } + var count = { value: 0 }; + var blob = { value: null }; + tuple.getBlob(3, count, blob); + do_check_eq(BLOB.length, count.value); + for (let i = 0; i < BLOB.length; i++) + do_check_eq(BLOB[i], blob.value[i]); + do_check_eq(Ci.mozIStorageValueArray.VALUE_TYPE_BLOB, + tuple.getTypeOfIndex(3)); + + do_check_false(tuple.getIsNull(4)); + do_check_eq(tuple.getResultByName("id"), tuple.getResultByIndex(4)); + do_check_eq(INTEGER, tuple.getResultByName("id")); + do_check_eq(Ci.mozIStorageValueArray.VALUE_TYPE_INTEGER, + tuple.getTypeOfIndex(4)); + }]); + stmt.finalize(); + run_next_test(); +} + +function test_tuple_out_of_bounds() +{ + var stmt = makeTestStatement( + "SELECT string FROM test" + ); + execAsync(stmt, {}, [ + function (tuple) { + do_check_neq(null, tuple); + + // Check all out of bounds - should throw + var methods = [ + "getTypeOfIndex", + "getInt32", + "getInt64", + "getDouble", + "getUTF8String", + "getString", + "getIsNull", + ]; + for (var i in methods) { + try { + tuple[methods[i]](tuple.numEntries); + do_throw("did not throw :("); + } + catch (e) { + do_check_eq(Cr.NS_ERROR_ILLEGAL_VALUE, e.result); + } + } + + // getBlob requires more args... + try { + var blob = { value: null }; + var size = { value: 0 }; + tuple.getBlob(tuple.numEntries, blob, size); + do_throw("did not throw :("); + } + catch (e) { + do_check_eq(Cr.NS_ERROR_ILLEGAL_VALUE, e.result); + } + }]); + stmt.finalize(); + run_next_test(); +} + +function test_no_listener_works_on_success() +{ + var stmt = makeTestStatement( + "DELETE FROM test WHERE id = ?" + ); + stmt.bindByIndex(0, 0); + stmt.executeAsync(); + stmt.finalize(); + + // Run the next test. + run_next_test(); +} + +function test_no_listener_works_on_results() +{ + var stmt = makeTestStatement( + "SELECT ?" + ); + stmt.bindByIndex(0, 1); + stmt.executeAsync(); + stmt.finalize(); + + // Run the next test. + run_next_test(); +} + +function test_no_listener_works_on_error() +{ + // commit without a transaction will trigger an error + var stmt = makeTestStatement( + "COMMIT" + ); + stmt.executeAsync(); + stmt.finalize(); + + // Run the next test. + run_next_test(); +} + +function test_partial_listener_works() +{ + var stmt = makeTestStatement( + "DELETE FROM test WHERE id = ?" + ); + stmt.bindByIndex(0, 0); + stmt.executeAsync({ + handleResult(aResultSet) {} + }); + stmt.executeAsync({ + handleError(aError) {} + }); + stmt.executeAsync({ + handleCompletion(aReason) {} + }); + stmt.finalize(); + + // Run the next test. + run_next_test(); +} + +/** + * Dubious cancellation test that depends on system loading may or may not + * succeed in canceling things. It does at least test if calling cancel blows + * up. test_AsyncCancellation in test_true_async.cpp is our test that canceling + * actually works correctly. + */ +function test_immediate_cancellation() +{ + var stmt = makeTestStatement( + "DELETE FROM test WHERE id = ?" + ); + stmt.bindByIndex(0, 0); + execAsync(stmt, {cancel: true}); + stmt.finalize(); + run_next_test(); +} + +/** + * Test that calling cancel twice throws the second time. + */ +function test_double_cancellation() +{ + var stmt = makeTestStatement( + "DELETE FROM test WHERE id = ?" + ); + stmt.bindByIndex(0, 0); + let pendingStatement = execAsync(stmt, {cancel: true}); + // And cancel again - expect an exception + expectError(Cr.NS_ERROR_UNEXPECTED, + () => pendingStatement.cancel()); + + stmt.finalize(); + run_next_test(); +} + +/** + * Verify that nothing untoward happens if we try and cancel something after it + * has fully run to completion. + */ +function test_cancellation_after_execution() +{ + var stmt = makeTestStatement( + "DELETE FROM test WHERE id = ?" + ); + stmt.bindByIndex(0, 0); + let pendingStatement = execAsync(stmt, {returnPending: true}); + // (the statement has fully executed at this point) + // canceling after the statement has run to completion should not throw! + pendingStatement.cancel(); + + stmt.finalize(); + run_next_test(); +} + +/** + * Verifies that a single statement can be executed more than once. Might once + * have been intended to also ensure that callback notifications were not + * incorrectly interleaved, but that part was brittle (it's totally fine for + * handleResult to get called multiple times) and not comprehensive. + */ +function test_double_execute() +{ + var stmt = makeTestStatement( + "SELECT 1" + ); + execAsync(stmt, null, 1); + execAsync(stmt, null, 1); + stmt.finalize(); + run_next_test(); +} + +function test_finalized_statement_does_not_crash() +{ + var stmt = makeTestStatement( + "SELECT * FROM TEST" + ); + stmt.finalize(); + // we are concerned about a crash here; an error is fine. + try { + stmt.executeAsync(); + } catch (ex) { + // Do nothing. + } + + // Run the next test. + run_next_test(); +} + +/** + * Bind by mozIStorageBindingParams on the mozIStorageBaseStatement by index. + */ +function test_bind_direct_binding_params_by_index() +{ + var stmt = makeTestStatement( + "INSERT INTO test (id, string, number, nuller, blober) " + + "VALUES (?, ?, ?, ?, ?)" + ); + let insertId = nextUniqueId++; + stmt.bindByIndex(0, insertId); + stmt.bindByIndex(1, TEXT); + stmt.bindByIndex(2, REAL); + stmt.bindByIndex(3, null); + stmt.bindBlobByIndex(4, BLOB, BLOB.length); + execAsync(stmt); + stmt.finalize(); + verifyQuery("SELECT string, number, nuller, blober FROM test WHERE id = ?", + insertId, + [TEXT, REAL, null, BLOB]); + run_next_test(); +} + +/** + * Bind by mozIStorageBindingParams on the mozIStorageBaseStatement by name. + */ +function test_bind_direct_binding_params_by_name() +{ + var stmt = makeTestStatement( + "INSERT INTO test (id, string, number, nuller, blober) " + + "VALUES (:int, :text, :real, :null, :blob)" + ); + let insertId = nextUniqueId++; + stmt.bindByName("int", insertId); + stmt.bindByName("text", TEXT); + stmt.bindByName("real", REAL); + stmt.bindByName("null", null); + stmt.bindBlobByName("blob", BLOB, BLOB.length); + execAsync(stmt); + stmt.finalize(); + verifyQuery("SELECT string, number, nuller, blober FROM test WHERE id = ?", + insertId, + [TEXT, REAL, null, BLOB]); + run_next_test(); +} + +function test_bind_js_params_helper_by_index() +{ + var stmt = makeTestStatement( + "INSERT INTO test (id, string, number, nuller, blober) " + + "VALUES (?, ?, ?, ?, NULL)" + ); + let insertId = nextUniqueId++; + // we cannot bind blobs this way; no blober + stmt.params[3] = null; + stmt.params[2] = REAL; + stmt.params[1] = TEXT; + stmt.params[0] = insertId; + execAsync(stmt); + stmt.finalize(); + verifyQuery("SELECT string, number, nuller FROM test WHERE id = ?", insertId, + [TEXT, REAL, null]); + run_next_test(); +} + +function test_bind_js_params_helper_by_name() +{ + var stmt = makeTestStatement( + "INSERT INTO test (id, string, number, nuller, blober) " + + "VALUES (:int, :text, :real, :null, NULL)" + ); + let insertId = nextUniqueId++; + // we cannot bind blobs this way; no blober + stmt.params.null = null; + stmt.params.real = REAL; + stmt.params.text = TEXT; + stmt.params.int = insertId; + execAsync(stmt); + stmt.finalize(); + verifyQuery("SELECT string, number, nuller FROM test WHERE id = ?", insertId, + [TEXT, REAL, null]); + run_next_test(); +} + +function test_bind_multiple_rows_by_index() +{ + const AMOUNT_TO_ADD = 5; + var stmt = makeTestStatement( + "INSERT INTO test (id, string, number, nuller, blober) " + + "VALUES (?, ?, ?, ?, ?)" + ); + var array = stmt.newBindingParamsArray(); + for (let i = 0; i < AMOUNT_TO_ADD; i++) { + let bp = array.newBindingParams(); + bp.bindByIndex(0, INTEGER); + bp.bindByIndex(1, TEXT); + bp.bindByIndex(2, REAL); + bp.bindByIndex(3, null); + bp.bindBlobByIndex(4, BLOB, BLOB.length); + array.addParams(bp); + do_check_eq(array.length, i + 1); + } + stmt.bindParameters(array); + + let rowCount = getTableRowCount("test"); + execAsync(stmt); + do_check_eq(rowCount + AMOUNT_TO_ADD, getTableRowCount("test")); + stmt.finalize(); + run_next_test(); +} + +function test_bind_multiple_rows_by_name() +{ + const AMOUNT_TO_ADD = 5; + var stmt = makeTestStatement( + "INSERT INTO test (id, string, number, nuller, blober) " + + "VALUES (:int, :text, :real, :null, :blob)" + ); + var array = stmt.newBindingParamsArray(); + for (let i = 0; i < AMOUNT_TO_ADD; i++) { + let bp = array.newBindingParams(); + bp.bindByName("int", INTEGER); + bp.bindByName("text", TEXT); + bp.bindByName("real", REAL); + bp.bindByName("null", null); + bp.bindBlobByName("blob", BLOB, BLOB.length); + array.addParams(bp); + do_check_eq(array.length, i + 1); + } + stmt.bindParameters(array); + + let rowCount = getTableRowCount("test"); + execAsync(stmt); + do_check_eq(rowCount + AMOUNT_TO_ADD, getTableRowCount("test")); + stmt.finalize(); + run_next_test(); +} + +/** + * Verify that a mozIStorageStatement instance throws immediately when we + * try and bind to an illegal index. + */ +function test_bind_out_of_bounds_sync_immediate() +{ + let stmt = makeTestStatement( + "INSERT INTO test (id) " + + "VALUES (?)" + ); + + let array = stmt.newBindingParamsArray(); + let bp = array.newBindingParams(); + + // Check variant binding. + expectError(Cr.NS_ERROR_INVALID_ARG, + () => bp.bindByIndex(1, INTEGER)); + // Check blob binding. + expectError(Cr.NS_ERROR_INVALID_ARG, + () => bp.bindBlobByIndex(1, BLOB, BLOB.length)); + + stmt.finalize(); + run_next_test(); +} +test_bind_out_of_bounds_sync_immediate.syncOnly = true; + +/** + * Verify that a mozIStorageAsyncStatement reports an error asynchronously when + * we bind to an illegal index. + */ +function test_bind_out_of_bounds_async_deferred() +{ + let stmt = makeTestStatement( + "INSERT INTO test (id) " + + "VALUES (?)" + ); + + let array = stmt.newBindingParamsArray(); + let bp = array.newBindingParams(); + + // There is no difference between variant and blob binding for async purposes. + bp.bindByIndex(1, INTEGER); + array.addParams(bp); + stmt.bindParameters(array); + execAsync(stmt, {error: Ci.mozIStorageError.RANGE}); + + stmt.finalize(); + run_next_test(); +} +test_bind_out_of_bounds_async_deferred.asyncOnly = true; + +function test_bind_no_such_name_sync_immediate() +{ + let stmt = makeTestStatement( + "INSERT INTO test (id) " + + "VALUES (:foo)" + ); + + let array = stmt.newBindingParamsArray(); + let bp = array.newBindingParams(); + + // Check variant binding. + expectError(Cr.NS_ERROR_INVALID_ARG, + () => bp.bindByName("doesnotexist", INTEGER)); + // Check blob binding. + expectError(Cr.NS_ERROR_INVALID_ARG, + () => bp.bindBlobByName("doesnotexist", BLOB, BLOB.length)); + + stmt.finalize(); + run_next_test(); +} +test_bind_no_such_name_sync_immediate.syncOnly = true; + +function test_bind_no_such_name_async_deferred() +{ + let stmt = makeTestStatement( + "INSERT INTO test (id) " + + "VALUES (:foo)" + ); + + let array = stmt.newBindingParamsArray(); + let bp = array.newBindingParams(); + + bp.bindByName("doesnotexist", INTEGER); + array.addParams(bp); + stmt.bindParameters(array); + execAsync(stmt, {error: Ci.mozIStorageError.RANGE}); + + stmt.finalize(); + run_next_test(); +} +test_bind_no_such_name_async_deferred.asyncOnly = true; + +function test_bind_bogus_type_by_index() +{ + // We try to bind a JS Object here that should fail to bind. + let stmt = makeTestStatement( + "INSERT INTO test (blober) " + + "VALUES (?)" + ); + + let array = stmt.newBindingParamsArray(); + let bp = array.newBindingParams(); + Assert.throws(() => bp.bindByIndex(0, run_test), /NS_ERROR_UNEXPECTED/); + + stmt.finalize(); + run_next_test(); +} + +function test_bind_bogus_type_by_name() +{ + // We try to bind a JS Object here that should fail to bind. + let stmt = makeTestStatement( + "INSERT INTO test (blober) " + + "VALUES (:blob)" + ); + + let array = stmt.newBindingParamsArray(); + let bp = array.newBindingParams(); + Assert.throws(() => bp.bindByName("blob", run_test), /NS_ERROR_UNEXPECTED/); + + stmt.finalize(); + run_next_test(); +} + +function test_bind_params_already_locked() +{ + let stmt = makeTestStatement( + "INSERT INTO test (id) " + + "VALUES (:int)" + ); + + let array = stmt.newBindingParamsArray(); + let bp = array.newBindingParams(); + bp.bindByName("int", INTEGER); + array.addParams(bp); + + // We should get an error after we call addParams and try to bind again. + expectError(Cr.NS_ERROR_UNEXPECTED, + () => bp.bindByName("int", INTEGER)); + + stmt.finalize(); + run_next_test(); +} + +function test_bind_params_array_already_locked() +{ + let stmt = makeTestStatement( + "INSERT INTO test (id) " + + "VALUES (:int)" + ); + + let array = stmt.newBindingParamsArray(); + let bp1 = array.newBindingParams(); + bp1.bindByName("int", INTEGER); + array.addParams(bp1); + let bp2 = array.newBindingParams(); + stmt.bindParameters(array); + bp2.bindByName("int", INTEGER); + + // We should get an error after we have bound the array to the statement. + expectError(Cr.NS_ERROR_UNEXPECTED, + () => array.addParams(bp2)); + + stmt.finalize(); + run_next_test(); +} + +function test_no_binding_params_from_locked_array() +{ + let stmt = makeTestStatement( + "INSERT INTO test (id) " + + "VALUES (:int)" + ); + + let array = stmt.newBindingParamsArray(); + let bp = array.newBindingParams(); + bp.bindByName("int", INTEGER); + array.addParams(bp); + stmt.bindParameters(array); + + // We should not be able to get a new BindingParams object after we have bound + // to the statement. + expectError(Cr.NS_ERROR_UNEXPECTED, + () => array.newBindingParams()); + + stmt.finalize(); + run_next_test(); +} + +function test_not_right_owning_array() +{ + let stmt = makeTestStatement( + "INSERT INTO test (id) " + + "VALUES (:int)" + ); + + let array1 = stmt.newBindingParamsArray(); + let array2 = stmt.newBindingParamsArray(); + let bp = array1.newBindingParams(); + bp.bindByName("int", INTEGER); + + // We should not be able to add bp to array2 since it was created from array1. + expectError(Cr.NS_ERROR_UNEXPECTED, + () => array2.addParams(bp)); + + stmt.finalize(); + run_next_test(); +} + +function test_not_right_owning_statement() +{ + let stmt1 = makeTestStatement( + "INSERT INTO test (id) " + + "VALUES (:int)" + ); + let stmt2 = makeTestStatement( + "INSERT INTO test (id) " + + "VALUES (:int)" + ); + + let array1 = stmt1.newBindingParamsArray(); + let array2 = stmt2.newBindingParamsArray(); + let bp = array1.newBindingParams(); + bp.bindByName("int", INTEGER); + array1.addParams(bp); + + // We should not be able to bind array1 since it was created from stmt1. + expectError(Cr.NS_ERROR_UNEXPECTED, + () => stmt2.bindParameters(array1)); + + stmt1.finalize(); + stmt2.finalize(); + run_next_test(); +} + +function test_bind_empty_array() +{ + let stmt = makeTestStatement( + "INSERT INTO test (id) " + + "VALUES (:int)" + ); + + let paramsArray = stmt.newBindingParamsArray(); + + // We should not be able to bind this array to the statement because it is + // empty. + expectError(Cr.NS_ERROR_UNEXPECTED, + () => stmt.bindParameters(paramsArray)); + + stmt.finalize(); + run_next_test(); +} + +function test_multiple_results() +{ + let expectedResults = getTableRowCount("test"); + // Sanity check - we should have more than one result, but let's be sure. + do_check_true(expectedResults > 1); + + // Now check that we get back two rows of data from our async query. + let stmt = makeTestStatement("SELECT * FROM test"); + execAsync(stmt, {}, expectedResults); + + stmt.finalize(); + run_next_test(); +} + +// Test Runner + +const TEST_PASS_SYNC = 0; +const TEST_PASS_ASYNC = 1; +/** + * We run 2 passes against the test. One where makeTestStatement generates + * synchronous (mozIStorageStatement) statements and one where it generates + * asynchronous (mozIStorageAsyncStatement) statements. + * + * Because of differences in the ability to know the number of parameters before + * dispatching, some tests are sync/async specific. These functions are marked + * with 'syncOnly' or 'asyncOnly' attributes and run_next_test knows what to do. + */ +var testPass = TEST_PASS_SYNC; + +/** + * Create a statement of the type under test per testPass. + * + * @param aSQL + * The SQL string from which to build a statement. + * @return a statement of the type under test per testPass. + */ +function makeTestStatement(aSQL) { + if (testPass == TEST_PASS_SYNC) { + return getOpenedDatabase().createStatement(aSQL); + } + return getOpenedDatabase().createAsyncStatement(aSQL); +} + +var tests = [ + test_illegal_sql_async_deferred, + test_create_table, + test_add_data, + test_get_data, + test_tuple_out_of_bounds, + test_no_listener_works_on_success, + test_no_listener_works_on_results, + test_no_listener_works_on_error, + test_partial_listener_works, + test_immediate_cancellation, + test_double_cancellation, + test_cancellation_after_execution, + test_double_execute, + test_finalized_statement_does_not_crash, + test_bind_direct_binding_params_by_index, + test_bind_direct_binding_params_by_name, + test_bind_js_params_helper_by_index, + test_bind_js_params_helper_by_name, + test_bind_multiple_rows_by_index, + test_bind_multiple_rows_by_name, + test_bind_out_of_bounds_sync_immediate, + test_bind_out_of_bounds_async_deferred, + test_bind_no_such_name_sync_immediate, + test_bind_no_such_name_async_deferred, + test_bind_bogus_type_by_index, + test_bind_bogus_type_by_name, + test_bind_params_already_locked, + test_bind_params_array_already_locked, + test_bind_empty_array, + test_no_binding_params_from_locked_array, + test_not_right_owning_array, + test_not_right_owning_statement, + test_multiple_results, +]; +var index = 0; + +const STARTING_UNIQUE_ID = 2; +var nextUniqueId = STARTING_UNIQUE_ID; + +function run_next_test() +{ + function _run_next_test() { + // use a loop so we can skip tests... + while (index < tests.length) { + let test = tests[index++]; + // skip tests not appropriate to the current test pass + if ((testPass == TEST_PASS_SYNC && ("asyncOnly" in test)) || + (testPass == TEST_PASS_ASYNC && ("syncOnly" in test))) + continue; + + // Asynchronous tests means that exceptions don't kill the test. + try { + print("****** Running the next test: " + test.name); + test(); + return; + } + catch (e) { + do_throw(e); + } + } + + // if we only completed the first pass, move to the next pass + if (testPass == TEST_PASS_SYNC) { + print("********* Beginning mozIStorageAsyncStatement pass."); + testPass++; + index = 0; + // a new pass demands a new database + asyncCleanup(); + nextUniqueId = STARTING_UNIQUE_ID; + _run_next_test(); + return; + } + + // we did some async stuff; we need to clean up. + asyncCleanup(); + do_test_finished(); + } + + // Don't actually schedule another test if we're quitting. + if (!_quit) { + // For saner stacks, we execute this code RSN. + do_execute_soon(_run_next_test); + } +} + +function run_test() +{ + cleanup(); + + do_test_pending(); + run_next_test(); +} diff --git a/storage/test/unit/test_statement_wrapper_automatically.js b/storage/test/unit/test_statement_wrapper_automatically.js new file mode 100644 index 000000000..58b27dd2d --- /dev/null +++ b/storage/test/unit/test_statement_wrapper_automatically.js @@ -0,0 +1,167 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- + vim:set ts=2 sw=2 sts=2 et: + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// This file tests the functions of mozIStorageStatementWrapper + +function setup() +{ + getOpenedDatabase().createTable("test", "id INTEGER PRIMARY KEY, val NONE," + + "alt_val NONE"); +} + +/** + * A convenience wrapper for do_check_eq. Calls do_check_eq on aActualVal + * and aReturnedVal, with one caveat. + * + * Date objects are converted before parameter binding to PRTime's (microsecs + * since epoch). They are not reconverted when retrieved from the database. + * This function abstracts away this reconversion so that you can pass in, + * for example: + * + * checkVal(new Date(), aReturnedVal) // this + * checkVal(new Date().valueOf() * 1000.0, aReturnedVal) // instead of this + * + * Should any other types require conversion in the future, their conversions + * may also be abstracted away here. + * + * @param aActualVal + * the value inserted into the database + * @param aReturnedVal + * the value retrieved from the database + */ +function checkVal(aActualVal, aReturnedVal) +{ + if (aActualVal instanceof Date) aActualVal = aActualVal.valueOf() * 1000.0; + do_check_eq(aActualVal, aReturnedVal); +} + +/** + * Removes all rows from our test table. + */ +function clearTable() +{ + var stmt = createStatement("DELETE FROM test"); + stmt.execute(); + stmt.finalize(); + ensureNumRows(0); +} + +/** + * Ensures that the number of rows in our test table is equal to aNumRows. + * Calls do_check_eq on aNumRows and the value retrieved by SELECT'ing COUNT(*). + * + * @param aNumRows + * the number of rows our test table should contain + */ +function ensureNumRows(aNumRows) +{ + var stmt = createStatement("SELECT COUNT(*) AS number FROM test"); + do_check_true(stmt.step()); + do_check_eq(aNumRows, stmt.row.number); + stmt.reset(); + stmt.finalize(); +} + +/** + * Inserts aVal into our test table and checks that insertion was successful by + * retrieving the newly inserted value from the database and comparing it + * against aVal. aVal is bound to a single parameter. + * + * @param aVal + * value to insert into our test table and check + */ +function insertAndCheckSingleParam(aVal) +{ + clearTable(); + + var stmt = createStatement("INSERT INTO test (val) VALUES (:val)"); + stmt.params.val = aVal; + stmt.execute(); + stmt.finalize(); + + ensureNumRows(1); + + stmt = createStatement("SELECT val FROM test WHERE id = 1"); + do_check_true(stmt.step()); + checkVal(aVal, stmt.row.val); + stmt.reset(); + stmt.finalize(); +} + +/** + * Inserts aVal into our test table and checks that insertion was successful by + * retrieving the newly inserted value from the database and comparing it + * against aVal. aVal is bound to two separate parameters, both of which are + * checked against aVal. + * + * @param aVal + * value to insert into our test table and check + */ +function insertAndCheckMultipleParams(aVal) +{ + clearTable(); + + var stmt = createStatement("INSERT INTO test (val, alt_val) " + + "VALUES (:val, :val)"); + stmt.params.val = aVal; + stmt.execute(); + stmt.finalize(); + + ensureNumRows(1); + + stmt = createStatement("SELECT val, alt_val FROM test WHERE id = 1"); + do_check_true(stmt.step()); + checkVal(aVal, stmt.row.val); + checkVal(aVal, stmt.row.alt_val); + stmt.reset(); + stmt.finalize(); +} + +/** + * A convenience function that prints out a description of aVal using + * aVal.toString and aVal.toSource. Output is useful when the test fails. + * + * @param aVal + * a value inserted or to be inserted into our test table + */ +function printValDesc(aVal) +{ + try { + var toSource = aVal.toSource(); + } catch (ex) { + toSource = ""; + } + print("Testing value: toString=" + aVal + + (toSource ? " toSource=" + toSource : "")); +} + +function run_test() +{ + setup(); + + // function JSValStorageStatementBinder in + // storage/mozStorageStatementParams.cpp tells us that the following types + // and only the following types are valid as statement parameters: + var vals = [ + 1337, // int + 3.1337, // double + "foo", // string + true, // boolean + null, // null + new Date(), // Date object + ]; + + vals.forEach(function (val) + { + printValDesc(val); + print("Single parameter"); + insertAndCheckSingleParam(val); + print("Multiple parameters"); + insertAndCheckMultipleParams(val); + }); + + cleanup(); +} diff --git a/storage/test/unit/test_storage_aggregates.js b/storage/test/unit/test_storage_aggregates.js new file mode 100644 index 000000000..400aba836 --- /dev/null +++ b/storage/test/unit/test_storage_aggregates.js @@ -0,0 +1,116 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// This file tests the custom aggregate functions + +var testNums = [1, 2, 3, 4]; + +function setup() +{ + getOpenedDatabase().createTable("function_tests", "id INTEGER PRIMARY KEY"); + + var stmt = createStatement("INSERT INTO function_tests (id) VALUES(?1)"); + for (let i = 0; i < testNums.length; ++i) { + stmt.bindByIndex(0, testNums[i]); + stmt.execute(); + } + stmt.reset(); + stmt.finalize(); +} + +var testSquareAndSumFunction = { + calls: 0, + _sas: 0, + + reset() { + this.calls = 0; + this._sas = 0; + }, + + onStep(val) { + ++this.calls; + this._sas += val.getInt32(0) * val.getInt32(0); + }, + + onFinal() { + var retval = this._sas; + this._sas = 0; // Prepare for next group + return retval; + } +}; + +function test_aggregate_registration() +{ + var msc = getOpenedDatabase(); + msc.createAggregateFunction("test_sas_aggr", 1, testSquareAndSumFunction); +} + +function test_aggregate_no_double_registration() +{ + var msc = getOpenedDatabase(); + try { + msc.createAggregateFunction("test_sas_aggr", 2, testSquareAndSumFunction); + do_throw("We shouldn't get here!"); + } catch (e) { + do_check_eq(Cr.NS_ERROR_FAILURE, e.result); + } +} + +function test_aggregate_removal() +{ + var msc = getOpenedDatabase(); + msc.removeFunction("test_sas_aggr"); + // Should be Ok now + msc.createAggregateFunction("test_sas_aggr", 1, testSquareAndSumFunction); +} + +function test_aggregate_no_aliases() +{ + var msc = getOpenedDatabase(); + try { + msc.createAggregateFunction("test_sas_aggr2", 1, testSquareAndSumFunction); + do_throw("We shouldn't get here!"); + } catch (e) { + do_check_eq(Cr.NS_ERROR_FAILURE, e.result); + } +} + +function test_aggregate_call() +{ + var stmt = createStatement("SELECT test_sas_aggr(id) FROM function_tests"); + while (stmt.executeStep()) { + // Do nothing. + } + do_check_eq(testNums.length, testSquareAndSumFunction.calls); + testSquareAndSumFunction.reset(); + stmt.finalize(); +} + +function test_aggregate_result() +{ + var sas = 0; + for (var i = 0; i < testNums.length; ++i) { + sas += testNums[i] * testNums[i]; + } + var stmt = createStatement("SELECT test_sas_aggr(id) FROM function_tests"); + stmt.executeStep(); + do_check_eq(sas, stmt.getInt32(0)); + testSquareAndSumFunction.reset(); + stmt.finalize(); +} + +var tests = [test_aggregate_registration, test_aggregate_no_double_registration, + test_aggregate_removal, test_aggregate_no_aliases, test_aggregate_call, + test_aggregate_result]; + +function run_test() +{ + setup(); + + for (var i = 0; i < tests.length; i++) { + tests[i](); + } + + cleanup(); +} diff --git a/storage/test/unit/test_storage_connection.js b/storage/test/unit/test_storage_connection.js new file mode 100644 index 000000000..ce98d0891 --- /dev/null +++ b/storage/test/unit/test_storage_connection.js @@ -0,0 +1,763 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// This file tests the functions of mozIStorageConnection + +// Test Functions + +add_task(function* test_connectionReady_open() { + // there doesn't seem to be a way for the connection to not be ready (unless + // we close it with mozIStorageConnection::Close(), but we don't for this). + // It can only fail if GetPath fails on the database file, or if we run out + // of memory trying to use an in-memory database + + var msc = getOpenedDatabase(); + do_check_true(msc.connectionReady); +}); + +add_task(function* test_connectionReady_closed() { + // This also tests mozIStorageConnection::Close() + + var msc = getOpenedDatabase(); + msc.close(); + do_check_false(msc.connectionReady); + gDBConn = null; // this is so later tests don't start to fail. +}); + +add_task(function* test_databaseFile() { + var msc = getOpenedDatabase(); + do_check_true(getTestDB().equals(msc.databaseFile)); +}); + +add_task(function* test_tableExists_not_created() { + var msc = getOpenedDatabase(); + do_check_false(msc.tableExists("foo")); +}); + +add_task(function* test_indexExists_not_created() { + var msc = getOpenedDatabase(); + do_check_false(msc.indexExists("foo")); +}); + +add_task(function* test_temp_tableExists_and_indexExists() { + var msc = getOpenedDatabase(); + msc.executeSimpleSQL("CREATE TEMP TABLE test_temp(id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT)"); + do_check_true(msc.tableExists("test_temp")); + + msc.executeSimpleSQL("CREATE INDEX test_temp_ind ON test_temp (name)"); + do_check_true(msc.indexExists("test_temp_ind")); + + msc.executeSimpleSQL("DROP INDEX test_temp_ind"); + msc.executeSimpleSQL("DROP TABLE test_temp"); +}); + +add_task(function* test_createTable_not_created() { + var msc = getOpenedDatabase(); + msc.createTable("test", "id INTEGER PRIMARY KEY, name TEXT"); + do_check_true(msc.tableExists("test")); +}); + +add_task(function* test_indexExists_created() { + var msc = getOpenedDatabase(); + msc.executeSimpleSQL("CREATE INDEX name_ind ON test (name)"); + do_check_true(msc.indexExists("name_ind")); +}); + +add_task(function* test_createTable_already_created() { + var msc = getOpenedDatabase(); + do_check_true(msc.tableExists("test")); + Assert.throws(() => msc.createTable("test", "id INTEGER PRIMARY KEY, name TEXT"), + /NS_ERROR_FAILURE/); +}); + +add_task(function* test_attach_createTable_tableExists_indexExists() { + var msc = getOpenedDatabase(); + var file = do_get_file("storage_attach.sqlite", true); + var msc2 = getDatabase(file); + msc.executeSimpleSQL("ATTACH DATABASE '" + file.path + "' AS sample"); + + do_check_false(msc.tableExists("sample.test")); + msc.createTable("sample.test", "id INTEGER PRIMARY KEY, name TEXT"); + do_check_true(msc.tableExists("sample.test")); + Assert.throws(() => msc.createTable("sample.test", "id INTEGER PRIMARY KEY, name TEXT"), + /NS_ERROR_FAILURE/); + + do_check_false(msc.indexExists("sample.test_ind")); + msc.executeSimpleSQL("CREATE INDEX sample.test_ind ON test (name)"); + do_check_true(msc.indexExists("sample.test_ind")); + + msc.executeSimpleSQL("DETACH DATABASE sample"); + msc2.close(); + try { + file.remove(false); + } catch (e) { + // Do nothing. + } +}); + +add_task(function* test_lastInsertRowID() { + var msc = getOpenedDatabase(); + msc.executeSimpleSQL("INSERT INTO test (name) VALUES ('foo')"); + do_check_eq(1, msc.lastInsertRowID); +}); + +add_task(function* test_transactionInProgress_no() { + var msc = getOpenedDatabase(); + do_check_false(msc.transactionInProgress); +}); + +add_task(function* test_transactionInProgress_yes() { + var msc = getOpenedDatabase(); + msc.beginTransaction(); + do_check_true(msc.transactionInProgress); + msc.commitTransaction(); + do_check_false(msc.transactionInProgress); + + msc.beginTransaction(); + do_check_true(msc.transactionInProgress); + msc.rollbackTransaction(); + do_check_false(msc.transactionInProgress); +}); + +add_task(function* test_commitTransaction_no_transaction() { + var msc = getOpenedDatabase(); + do_check_false(msc.transactionInProgress); + Assert.throws(() => msc.commitTransaction(), /NS_ERROR_UNEXPECTED/); +}); + +add_task(function* test_rollbackTransaction_no_transaction() { + var msc = getOpenedDatabase(); + do_check_false(msc.transactionInProgress); + Assert.throws(() => msc.rollbackTransaction(), /NS_ERROR_UNEXPECTED/); +}); + +add_task(function* test_get_schemaVersion_not_set() { + do_check_eq(0, getOpenedDatabase().schemaVersion); +}); + +add_task(function* test_set_schemaVersion() { + var msc = getOpenedDatabase(); + const version = 1; + msc.schemaVersion = version; + do_check_eq(version, msc.schemaVersion); +}); + +add_task(function* test_set_schemaVersion_same() { + var msc = getOpenedDatabase(); + const version = 1; + msc.schemaVersion = version; // should still work ok + do_check_eq(version, msc.schemaVersion); +}); + +add_task(function* test_set_schemaVersion_negative() { + var msc = getOpenedDatabase(); + const version = -1; + msc.schemaVersion = version; + do_check_eq(version, msc.schemaVersion); +}); + +add_task(function* test_createTable() { + var temp = getTestDB().parent; + temp.append("test_db_table"); + try { + var con = getService().openDatabase(temp); + con.createTable("a", ""); + } catch (e) { + if (temp.exists()) { + try { + temp.remove(false); + } catch (e2) { + // Do nothing. + } + } + do_check_true(e.result == Cr.NS_ERROR_NOT_INITIALIZED || + e.result == Cr.NS_ERROR_FAILURE); + } finally { + if (con) { + con.close(); + } + } +}); + +add_task(function* test_defaultSynchronousAtNormal() { + var msc = getOpenedDatabase(); + var stmt = createStatement("PRAGMA synchronous;"); + try { + stmt.executeStep(); + do_check_eq(1, stmt.getInt32(0)); + } + finally { + stmt.reset(); + stmt.finalize(); + } +}); + +// must be ran before executeAsync tests +add_task(function* test_close_does_not_spin_event_loop() { + // We want to make sure that the event loop on the calling thread does not + // spin when close is called. + let event = { + ran: false, + run() { + this.ran = true; + }, + }; + + // Post the event before we call close, so it would run if the event loop was + // spun during close. + let thread = Cc["@mozilla.org/thread-manager;1"]. + getService(Ci.nsIThreadManager). + currentThread; + thread.dispatch(event, Ci.nsIThread.DISPATCH_NORMAL); + + // Sanity check, then close the database. Afterwards, we should not have ran! + do_check_false(event.ran); + getOpenedDatabase().close(); + do_check_false(event.ran); + + // Reset gDBConn so that later tests will get a new connection object. + gDBConn = null; +}); + +add_task(function* test_asyncClose_succeeds_with_finalized_async_statement() { + // XXX this test isn't perfect since we can't totally control when events will + // run. If this paticular function fails randomly, it means we have a + // real bug. + + // We want to make sure we create a cached async statement to make sure that + // when we finalize our statement, we end up finalizing the async one too so + // close will succeed. + let stmt = createStatement("SELECT * FROM test"); + stmt.executeAsync(); + stmt.finalize(); + + yield asyncClose(getOpenedDatabase()); + // Reset gDBConn so that later tests will get a new connection object. + gDBConn = null; +}); + +add_task(function* test_close_then_release_statement() { + // Testing the behavior in presence of a bad client that finalizes + // statements after the database has been closed (typically by + // letting the gc finalize the statement). + let db = getOpenedDatabase(); + let stmt = createStatement("SELECT * FROM test -- test_close_then_release_statement"); + db.close(); + stmt.finalize(); // Finalize too late - this should not crash + + // Reset gDBConn so that later tests will get a new connection object. + gDBConn = null; +}); + +add_task(function* test_asyncClose_then_release_statement() { + // Testing the behavior in presence of a bad client that finalizes + // statements after the database has been async closed (typically by + // letting the gc finalize the statement). + let db = getOpenedDatabase(); + let stmt = createStatement("SELECT * FROM test -- test_asyncClose_then_release_statement"); + yield asyncClose(db); + stmt.finalize(); // Finalize too late - this should not crash + + // Reset gDBConn so that later tests will get a new connection object. + gDBConn = null; +}); + +add_task(function* test_close_fails_with_async_statement_ran() { + let deferred = Promise.defer(); + let stmt = createStatement("SELECT * FROM test"); + stmt.executeAsync(); + stmt.finalize(); + + let db = getOpenedDatabase(); + Assert.throws(() => db.close(), /NS_ERROR_UNEXPECTED/); + + // Clean up after ourselves. + db.asyncClose(function () { + // Reset gDBConn so that later tests will get a new connection object. + gDBConn = null; + deferred.resolve(); + }); + + yield deferred.promise; +}); + +add_task(function* test_clone_optional_param() { + let db1 = getService().openUnsharedDatabase(getTestDB()); + let db2 = db1.clone(); + do_check_true(db2.connectionReady); + + // A write statement should not fail here. + let stmt = db2.createStatement("INSERT INTO test (name) VALUES (:name)"); + stmt.params.name = "dwitte"; + stmt.execute(); + stmt.finalize(); + + // And a read statement should succeed. + stmt = db2.createStatement("SELECT * FROM test"); + do_check_true(stmt.executeStep()); + stmt.finalize(); + + // Additionally check that it is a connection on the same database. + do_check_true(db1.databaseFile.equals(db2.databaseFile)); + + db1.close(); + db2.close(); +}); + +function* standardAsyncTest(promisedDB, name, shouldInit = false) { + do_print("Performing standard async test " + name); + + let adb = yield promisedDB; + do_check_true(adb instanceof Ci.mozIStorageAsyncConnection); + do_check_false(adb instanceof Ci.mozIStorageConnection); + + if (shouldInit) { + let stmt = adb.createAsyncStatement("CREATE TABLE test(name TEXT)"); + yield executeAsync(stmt); + stmt.finalize(); + } + + // Generate a name to insert and fetch back + name = "worker bee " + Math.random() + " (" + name + ")"; + + let stmt = adb.createAsyncStatement("INSERT INTO test (name) VALUES (:name)"); + stmt.params.name = name; + let result = yield executeAsync(stmt); + do_print("Request complete"); + stmt.finalize(); + do_check_true(Components.isSuccessCode(result)); + do_print("Extracting data"); + stmt = adb.createAsyncStatement("SELECT * FROM test"); + let found = false; + yield executeAsync(stmt, function (results) { + do_print("Data has been extracted"); + for (let row = results.getNextRow(); row != null; row = results.getNextRow()) { + if (row.getResultByName("name") == name) { + found = true; + break; + } + } + }); + do_check_true(found); + stmt.finalize(); + yield asyncClose(adb); + + do_print("Standard async test " + name + " complete"); +} + +add_task(function* test_open_async() { + yield standardAsyncTest(openAsyncDatabase(getTestDB(), null), "default"); + yield standardAsyncTest(openAsyncDatabase(getTestDB()), "no optional arg"); + yield standardAsyncTest(openAsyncDatabase(getTestDB(), + {shared: false, growthIncrement: 54}), "non-default options"); + yield standardAsyncTest(openAsyncDatabase("memory"), + "in-memory database", true); + yield standardAsyncTest(openAsyncDatabase("memory", + {shared: false}), + "in-memory database and options", true); + + do_print("Testing async opening with bogus options 0"); + let raised = false; + let adb = null; + + try { + adb = yield openAsyncDatabase("memory", {shared: false, growthIncrement: 54}); + } catch (ex) { + raised = true; + } finally { + if (adb) { + yield asyncClose(adb); + } + } + do_check_true(raised); + + do_print("Testing async opening with bogus options 1"); + raised = false; + adb = null; + try { + adb = yield openAsyncDatabase(getTestDB(), {shared: "forty-two"}); + } catch (ex) { + raised = true; + } finally { + if (adb) { + yield asyncClose(adb); + } + } + do_check_true(raised); + + do_print("Testing async opening with bogus options 2"); + raised = false; + adb = null; + try { + adb = yield openAsyncDatabase(getTestDB(), {growthIncrement: "forty-two"}); + } catch (ex) { + raised = true; + } finally { + if (adb) { + yield asyncClose(adb); + } + } + do_check_true(raised); +}); + + +add_task(function* test_async_open_with_shared_cache() { + do_print("Testing that opening with a shared cache doesn't break stuff"); + let adb = yield openAsyncDatabase(getTestDB(), {shared: true}); + + let stmt = adb.createAsyncStatement("INSERT INTO test (name) VALUES (:name)"); + stmt.params.name = "clockworker"; + let result = yield executeAsync(stmt); + do_print("Request complete"); + stmt.finalize(); + do_check_true(Components.isSuccessCode(result)); + do_print("Extracting data"); + stmt = adb.createAsyncStatement("SELECT * FROM test"); + let found = false; + yield executeAsync(stmt, function (results) { + do_print("Data has been extracted"); + for (let row = results.getNextRow(); row != null; row = results.getNextRow()) { + if (row.getResultByName("name") == "clockworker") { + found = true; + break; + } + } + }); + do_check_true(found); + stmt.finalize(); + yield asyncClose(adb); +}); + +add_task(function* test_clone_trivial_async() { + do_print("Open connection"); + let db = getService().openDatabase(getTestDB()); + do_check_true(db instanceof Ci.mozIStorageAsyncConnection); + do_print("AsyncClone connection"); + let clone = yield asyncClone(db, true); + do_check_true(clone instanceof Ci.mozIStorageAsyncConnection); + do_print("Close connection"); + yield asyncClose(db); + do_print("Close clone"); + yield asyncClose(clone); +}); + +add_task(function* test_clone_no_optional_param_async() { + "use strict"; + do_print("Testing async cloning"); + let adb1 = yield openAsyncDatabase(getTestDB(), null); + do_check_true(adb1 instanceof Ci.mozIStorageAsyncConnection); + + do_print("Cloning database"); + + let adb2 = yield asyncClone(adb1); + do_print("Testing that the cloned db is a mozIStorageAsyncConnection " + + "and not a mozIStorageConnection"); + do_check_true(adb2 instanceof Ci.mozIStorageAsyncConnection); + do_check_false(adb2 instanceof Ci.mozIStorageConnection); + + do_print("Inserting data into source db"); + let stmt = adb1. + createAsyncStatement("INSERT INTO test (name) VALUES (:name)"); + + stmt.params.name = "yoric"; + let result = yield executeAsync(stmt); + do_print("Request complete"); + stmt.finalize(); + do_check_true(Components.isSuccessCode(result)); + do_print("Extracting data from clone db"); + stmt = adb2.createAsyncStatement("SELECT * FROM test"); + let found = false; + yield executeAsync(stmt, function (results) { + do_print("Data has been extracted"); + for (let row = results.getNextRow(); row != null; row = results.getNextRow()) { + if (row.getResultByName("name") == "yoric") { + found = true; + break; + } + } + }); + do_check_true(found); + stmt.finalize(); + do_print("Closing databases"); + yield asyncClose(adb2); + do_print("First db closed"); + + yield asyncClose(adb1); + do_print("Second db closed"); +}); + +add_task(function* test_clone_readonly() { + let db1 = getService().openUnsharedDatabase(getTestDB()); + let db2 = db1.clone(true); + do_check_true(db2.connectionReady); + + // A write statement should fail here. + let stmt = db2.createStatement("INSERT INTO test (name) VALUES (:name)"); + stmt.params.name = "reed"; + expectError(Cr.NS_ERROR_FILE_READ_ONLY, () => stmt.execute()); + stmt.finalize(); + + // And a read statement should succeed. + stmt = db2.createStatement("SELECT * FROM test"); + do_check_true(stmt.executeStep()); + stmt.finalize(); + + db1.close(); + db2.close(); +}); + +add_task(function* test_clone_shared_readonly() { + let db1 = getService().openDatabase(getTestDB()); + let db2 = db1.clone(true); + do_check_true(db2.connectionReady); + + let stmt = db2.createStatement("INSERT INTO test (name) VALUES (:name)"); + stmt.params.name = "parker"; + // TODO currently SQLite does not actually work correctly here. The behavior + // we want is commented out, and the current behavior is being tested + // for. Our IDL comments will have to be updated when this starts to + // work again. + stmt.execute(); + // expectError(Components.results.NS_ERROR_FILE_READ_ONLY, () => stmt.execute()); + stmt.finalize(); + + // And a read statement should succeed. + stmt = db2.createStatement("SELECT * FROM test"); + do_check_true(stmt.executeStep()); + stmt.finalize(); + + db1.close(); + db2.close(); +}); + +add_task(function* test_close_clone_fails() { + let calls = [ + "openDatabase", + "openUnsharedDatabase", + ]; + calls.forEach(function (methodName) { + let db = getService()[methodName](getTestDB()); + db.close(); + expectError(Cr.NS_ERROR_NOT_INITIALIZED, () => db.clone()); + }); +}); + +add_task(function* test_memory_clone_fails() { + let db = getService().openSpecialDatabase("memory"); + db.close(); + expectError(Cr.NS_ERROR_NOT_INITIALIZED, () => db.clone()); +}); + +add_task(function* test_clone_copies_functions() { + const FUNC_NAME = "test_func"; + let calls = [ + "openDatabase", + "openUnsharedDatabase", + ]; + let functionMethods = [ + "createFunction", + "createAggregateFunction", + ]; + calls.forEach(function (methodName) { + [true, false].forEach(function (readOnly) { + functionMethods.forEach(function (functionMethod) { + let db1 = getService()[methodName](getTestDB()); + // Create a function for db1. + db1[functionMethod](FUNC_NAME, 1, { + onFunctionCall: () => 0, + onStep: () => 0, + onFinal: () => 0, + }); + + // Clone it, and make sure the function exists still. + let db2 = db1.clone(readOnly); + // Note: this would fail if the function did not exist. + let stmt = db2.createStatement("SELECT " + FUNC_NAME + "(id) FROM test"); + stmt.finalize(); + db1.close(); + db2.close(); + }); + }); + }); +}); + +add_task(function* test_clone_copies_overridden_functions() { + const FUNC_NAME = "lower"; + function test_func() { + this.called = false; + } + test_func.prototype = { + onFunctionCall() { + this.called = true; + }, + onStep() { + this.called = true; + }, + onFinal: () => 0, + }; + + let calls = [ + "openDatabase", + "openUnsharedDatabase", + ]; + let functionMethods = [ + "createFunction", + "createAggregateFunction", + ]; + calls.forEach(function (methodName) { + [true, false].forEach(function (readOnly) { + functionMethods.forEach(function (functionMethod) { + let db1 = getService()[methodName](getTestDB()); + // Create a function for db1. + let func = new test_func(); + db1[functionMethod](FUNC_NAME, 1, func); + do_check_false(func.called); + + // Clone it, and make sure the function gets called. + let db2 = db1.clone(readOnly); + let stmt = db2.createStatement("SELECT " + FUNC_NAME + "(id) FROM test"); + stmt.executeStep(); + do_check_true(func.called); + stmt.finalize(); + db1.close(); + db2.close(); + }); + }); + }); +}); + +add_task(function* test_clone_copies_pragmas() { + const PRAGMAS = [ + { name: "cache_size", value: 500, copied: true }, + { name: "temp_store", value: 2, copied: true }, + { name: "foreign_keys", value: 1, copied: true }, + { name: "journal_size_limit", value: 524288, copied: true }, + { name: "synchronous", value: 2, copied: true }, + { name: "wal_autocheckpoint", value: 16, copied: true }, + { name: "busy_timeout", value: 50, copied: true }, + { name: "ignore_check_constraints", value: 1, copied: false }, + ]; + + let db1 = getService().openUnsharedDatabase(getTestDB()); + + // Sanity check initial values are different from enforced ones. + PRAGMAS.forEach(function (pragma) { + let stmt = db1.createStatement("PRAGMA " + pragma.name); + do_check_true(stmt.executeStep()); + do_check_neq(pragma.value, stmt.getInt32(0)); + stmt.finalize(); + }); + // Execute pragmas. + PRAGMAS.forEach(function (pragma) { + db1.executeSimpleSQL("PRAGMA " + pragma.name + " = " + pragma.value); + }); + + let db2 = db1.clone(); + do_check_true(db2.connectionReady); + + // Check cloned connection inherited pragma values. + PRAGMAS.forEach(function (pragma) { + let stmt = db2.createStatement("PRAGMA " + pragma.name); + do_check_true(stmt.executeStep()); + let validate = pragma.copied ? do_check_eq : do_check_neq; + validate(pragma.value, stmt.getInt32(0)); + stmt.finalize(); + }); + + db1.close(); + db2.close(); +}); + +add_task(function* test_readonly_clone_copies_pragmas() { + const PRAGMAS = [ + { name: "cache_size", value: 500, copied: true }, + { name: "temp_store", value: 2, copied: true }, + { name: "foreign_keys", value: 1, copied: false }, + { name: "journal_size_limit", value: 524288, copied: false }, + { name: "synchronous", value: 2, copied: false }, + { name: "wal_autocheckpoint", value: 16, copied: false }, + { name: "busy_timeout", value: 50, copied: false }, + { name: "ignore_check_constraints", value: 1, copied: false }, + ]; + + let db1 = getService().openUnsharedDatabase(getTestDB()); + + // Sanity check initial values are different from enforced ones. + PRAGMAS.forEach(function (pragma) { + let stmt = db1.createStatement("PRAGMA " + pragma.name); + do_check_true(stmt.executeStep()); + do_check_neq(pragma.value, stmt.getInt32(0)); + stmt.finalize(); + }); + // Execute pragmas. + PRAGMAS.forEach(function (pragma) { + db1.executeSimpleSQL("PRAGMA " + pragma.name + " = " + pragma.value); + }); + + let db2 = db1.clone(true); + do_check_true(db2.connectionReady); + + // Check cloned connection inherited pragma values. + PRAGMAS.forEach(function (pragma) { + let stmt = db2.createStatement("PRAGMA " + pragma.name); + do_check_true(stmt.executeStep()); + let validate = pragma.copied ? do_check_eq : do_check_neq; + validate(pragma.value, stmt.getInt32(0)); + stmt.finalize(); + }); + + db1.close(); + db2.close(); +}); + +add_task(function* test_clone_attach_database() { + let db1 = getService().openUnsharedDatabase(getTestDB()); + + let c = 0; + function attachDB(conn, name) { + let file = dirSvc.get("ProfD", Ci.nsIFile); + file.append("test_storage_" + (++c) + ".sqlite"); + let db = getService().openUnsharedDatabase(file); + conn.executeSimpleSQL(`ATTACH DATABASE '${db.databaseFile.path}' AS ${name}`); + db.close(); + } + attachDB(db1, "attached_1"); + attachDB(db1, "attached_2"); + + // These should not throw. + db1.createStatement("SELECT * FROM attached_1.sqlite_master"); + db1.createStatement("SELECT * FROM attached_2.sqlite_master"); + + // R/W clone. + let db2 = db1.clone(); + do_check_true(db2.connectionReady); + + // These should not throw. + db2.createStatement("SELECT * FROM attached_1.sqlite_master"); + db2.createStatement("SELECT * FROM attached_2.sqlite_master"); + + // R/O clone. + let db3 = db1.clone(true); + do_check_true(db3.connectionReady); + + // These should not throw. + db3.createStatement("SELECT * FROM attached_1.sqlite_master"); + db3.createStatement("SELECT * FROM attached_2.sqlite_master"); + + db1.close(); + db2.close(); + db3.close(); +}); + +add_task(function* test_getInterface() { + let db = getOpenedDatabase(); + let target = db.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIEventTarget); + // Just check that target is non-null. Other tests will ensure that it has + // the correct value. + do_check_true(target != null); + + yield asyncClose(db); + gDBConn = null; +}); diff --git a/storage/test/unit/test_storage_fulltextindex.js b/storage/test/unit/test_storage_fulltextindex.js new file mode 100644 index 000000000..1f7614067 --- /dev/null +++ b/storage/test/unit/test_storage_fulltextindex.js @@ -0,0 +1,86 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// This file tests support for the fts3 (full-text index) module. + +// Example statements in these tests are taken from the Full Text Index page +// on the SQLite wiki: http://www.sqlite.org/cvstrac/wiki?p=FullTextIndex + +function test_table_creation() +{ + var msc = getOpenedUnsharedDatabase(); + + msc.executeSimpleSQL( + "CREATE VIRTUAL TABLE recipe USING fts3(name, ingredients)"); + + do_check_true(msc.tableExists("recipe")); +} + +function test_insertion() +{ + var msc = getOpenedUnsharedDatabase(); + + msc.executeSimpleSQL("INSERT INTO recipe (name, ingredients) VALUES " + + "('broccoli stew', 'broccoli peppers cheese tomatoes')"); + msc.executeSimpleSQL("INSERT INTO recipe (name, ingredients) VALUES " + + "('pumpkin stew', 'pumpkin onions garlic celery')"); + msc.executeSimpleSQL("INSERT INTO recipe (name, ingredients) VALUES " + + "('broccoli pie', 'broccoli cheese onions flour')"); + msc.executeSimpleSQL("INSERT INTO recipe (name, ingredients) VALUES " + + "('pumpkin pie', 'pumpkin sugar flour butter')"); + + var stmt = msc.createStatement("SELECT COUNT(*) FROM recipe"); + stmt.executeStep(); + + do_check_eq(stmt.getInt32(0), 4); + + stmt.reset(); + stmt.finalize(); +} + +function test_selection() +{ + var msc = getOpenedUnsharedDatabase(); + + var stmt = msc.createStatement( + "SELECT rowid, name, ingredients FROM recipe WHERE name MATCH 'pie'"); + + do_check_true(stmt.executeStep()); + do_check_eq(stmt.getInt32(0), 3); + do_check_eq(stmt.getString(1), "broccoli pie"); + do_check_eq(stmt.getString(2), "broccoli cheese onions flour"); + + do_check_true(stmt.executeStep()); + do_check_eq(stmt.getInt32(0), 4); + do_check_eq(stmt.getString(1), "pumpkin pie"); + do_check_eq(stmt.getString(2), "pumpkin sugar flour butter"); + + do_check_false(stmt.executeStep()); + + stmt.reset(); + stmt.finalize(); +} + +var tests = [test_table_creation, test_insertion, test_selection]; + +function run_test() +{ + // It's extra important to start from scratch, since these tests won't work + // with an existing shared cache connection, so we do it even though the last + // test probably did it already. + cleanup(); + + try { + for (var i = 0; i < tests.length; i++) { + tests[i](); + } + } + // It's extra important to clean up afterwards, since later tests that use + // a shared cache connection will not be able to read the database we create, + // so we do this in a finally block to ensure it happens even if some of our + // tests fail. + finally { + cleanup(); + } +} diff --git a/storage/test/unit/test_storage_function.js b/storage/test/unit/test_storage_function.js new file mode 100644 index 000000000..6532709ec --- /dev/null +++ b/storage/test/unit/test_storage_function.js @@ -0,0 +1,95 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// This file tests the custom functions + +var testNums = [1, 2, 3, 4]; + +function setup() +{ + getOpenedDatabase().createTable("function_tests", "id INTEGER PRIMARY KEY"); + + var stmt = createStatement("INSERT INTO function_tests (id) VALUES(?1)"); + for (let i = 0; i < testNums.length; ++i) { + stmt.bindByIndex(0, testNums[i]); + stmt.execute(); + } + stmt.reset(); + stmt.finalize(); +} + +var testSquareFunction = { + calls: 0, + + onFunctionCall(val) { + ++this.calls; + return val.getInt32(0) * val.getInt32(0); + } +}; + +function test_function_registration() +{ + var msc = getOpenedDatabase(); + msc.createFunction("test_square", 1, testSquareFunction); +} + +function test_function_no_double_registration() +{ + var msc = getOpenedDatabase(); + try { + msc.createFunction("test_square", 2, testSquareFunction); + do_throw("We shouldn't get here!"); + } catch (e) { + do_check_eq(Cr.NS_ERROR_FAILURE, e.result); + } +} + +function test_function_removal() +{ + var msc = getOpenedDatabase(); + msc.removeFunction("test_square"); + // Should be Ok now + msc.createFunction("test_square", 1, testSquareFunction); +} + +function test_function_aliases() +{ + var msc = getOpenedDatabase(); + msc.createFunction("test_square2", 1, testSquareFunction); +} + +function test_function_call() +{ + var stmt = createStatement("SELECT test_square(id) FROM function_tests"); + while (stmt.executeStep()) { + // Do nothing. + } + do_check_eq(testNums.length, testSquareFunction.calls); + testSquareFunction.calls = 0; + stmt.finalize(); +} + +function test_function_result() +{ + var stmt = createStatement("SELECT test_square(42) FROM function_tests"); + stmt.executeStep(); + do_check_eq(42 * 42, stmt.getInt32(0)); + testSquareFunction.calls = 0; + stmt.finalize(); +} + +var tests = [test_function_registration, test_function_no_double_registration, + test_function_removal, test_function_aliases, test_function_call, + test_function_result]; + +function run_test() +{ + setup(); + + for (var i = 0; i < tests.length; i++) { + tests[i](); + } + + cleanup(); +} diff --git a/storage/test/unit/test_storage_progresshandler.js b/storage/test/unit/test_storage_progresshandler.js new file mode 100644 index 000000000..c06a57e83 --- /dev/null +++ b/storage/test/unit/test_storage_progresshandler.js @@ -0,0 +1,111 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// This file tests the custom progress handlers + +function setup() +{ + var msc = getOpenedDatabase(); + msc.createTable("handler_tests", "id INTEGER PRIMARY KEY, num INTEGER"); + msc.beginTransaction(); + + var stmt = createStatement("INSERT INTO handler_tests (id, num) VALUES(?1, ?2)"); + for (let i = 0; i < 100; ++i) { + stmt.bindByIndex(0, i); + stmt.bindByIndex(1, Math.floor(Math.random() * 1000)); + stmt.execute(); + } + stmt.reset(); + msc.commitTransaction(); + stmt.finalize(); +} + +var testProgressHandler = { + calls: 0, + abort: false, + + onProgress(comm) { + ++this.calls; + return this.abort; + } +}; + +function test_handler_registration() +{ + var msc = getOpenedDatabase(); + msc.setProgressHandler(10, testProgressHandler); +} + +function test_handler_return() +{ + var msc = getOpenedDatabase(); + var oldH = msc.setProgressHandler(5, testProgressHandler); + do_check_true(oldH instanceof Ci.mozIStorageProgressHandler); +} + +function test_handler_removal() +{ + var msc = getOpenedDatabase(); + msc.removeProgressHandler(); + var oldH = msc.removeProgressHandler(); + do_check_eq(oldH, null); +} + +function test_handler_call() +{ + var msc = getOpenedDatabase(); + msc.setProgressHandler(50, testProgressHandler); + // Some long-executing request + var stmt = createStatement( + "SELECT SUM(t1.num * t2.num) FROM handler_tests AS t1, handler_tests AS t2"); + while (stmt.executeStep()) { + // Do nothing. + } + do_check_true(testProgressHandler.calls > 0); + stmt.finalize(); +} + +function test_handler_abort() +{ + var msc = getOpenedDatabase(); + testProgressHandler.abort = true; + msc.setProgressHandler(50, testProgressHandler); + // Some long-executing request + var stmt = createStatement( + "SELECT SUM(t1.num * t2.num) FROM handler_tests AS t1, handler_tests AS t2"); + + const SQLITE_INTERRUPT = 9; + try { + while (stmt.executeStep()) { + // Do nothing. + } + do_throw("We shouldn't get here!"); + } catch (e) { + do_check_eq(Cr.NS_ERROR_ABORT, e.result); + do_check_eq(SQLITE_INTERRUPT, msc.lastError); + } + try { + stmt.finalize(); + do_throw("We shouldn't get here!"); + } catch (e) { + // finalize should return the error code since we encountered an error + do_check_eq(Cr.NS_ERROR_ABORT, e.result); + do_check_eq(SQLITE_INTERRUPT, msc.lastError); + } +} + +var tests = [test_handler_registration, test_handler_return, + test_handler_removal, test_handler_call, + test_handler_abort]; + +function run_test() +{ + setup(); + + for (var i = 0; i < tests.length; i++) { + tests[i](); + } + + cleanup(); +} diff --git a/storage/test/unit/test_storage_service.js b/storage/test/unit/test_storage_service.js new file mode 100644 index 000000000..9cf46620e --- /dev/null +++ b/storage/test/unit/test_storage_service.js @@ -0,0 +1,142 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// This file tests the functions of mozIStorageService except for +// openUnsharedDatabase, which is tested by test_storage_service_unshared.js. + +const BACKUP_FILE_NAME = "test_storage.sqlite.backup"; + +function test_openSpecialDatabase_invalid_arg() +{ + try { + getService().openSpecialDatabase("abcd"); + do_throw("We should not get here!"); + } catch (e) { + print(e); + print("e.result is " + e.result); + do_check_eq(Cr.NS_ERROR_INVALID_ARG, e.result); + } +} + +function test_openDatabase_null_file() +{ + try { + getService().openDatabase(null); + do_throw("We should not get here!"); + } catch (e) { + print(e); + print("e.result is " + e.result); + do_check_eq(Cr.NS_ERROR_INVALID_ARG, e.result); + } +} + +function test_openUnsharedDatabase_null_file() +{ + try { + getService().openUnsharedDatabase(null); + do_throw("We should not get here!"); + } catch (e) { + print(e); + print("e.result is " + e.result); + do_check_eq(Cr.NS_ERROR_INVALID_ARG, e.result); + } +} + +function test_openDatabase_file_DNE() +{ + // the file should be created after calling + var db = getTestDB(); + do_check_false(db.exists()); + getService().openDatabase(db); + do_check_true(db.exists()); +} + +function test_openDatabase_file_exists() +{ + // it should already exist from our last test + var db = getTestDB(); + do_check_true(db.exists()); + getService().openDatabase(db); + do_check_true(db.exists()); +} + +function test_corrupt_db_throws_with_openDatabase() +{ + try { + getDatabase(getCorruptDB()); + do_throw("should not be here"); + } + catch (e) { + do_check_eq(Cr.NS_ERROR_FILE_CORRUPTED, e.result); + } +} + +function test_fake_db_throws_with_openDatabase() +{ + try { + getDatabase(getFakeDB()); + do_throw("should not be here"); + } + catch (e) { + do_check_eq(Cr.NS_ERROR_FILE_CORRUPTED, e.result); + } +} + +function test_backup_not_new_filename() +{ + const fname = getTestDB().leafName; + + var backup = getService().backupDatabaseFile(getTestDB(), fname); + do_check_neq(fname, backup.leafName); + + backup.remove(false); +} + +function test_backup_new_filename() +{ + var backup = getService().backupDatabaseFile(getTestDB(), BACKUP_FILE_NAME); + do_check_eq(BACKUP_FILE_NAME, backup.leafName); + + backup.remove(false); +} + +function test_backup_new_folder() +{ + var parentDir = getTestDB().parent; + parentDir.append("test_storage_temp"); + if (parentDir.exists()) + parentDir.remove(true); + parentDir.create(Ci.nsIFile.DIRECTORY_TYPE, 0o755); + do_check_true(parentDir.exists()); + + var backup = getService().backupDatabaseFile(getTestDB(), BACKUP_FILE_NAME, + parentDir); + do_check_eq(BACKUP_FILE_NAME, backup.leafName); + do_check_true(parentDir.equals(backup.parent)); + + parentDir.remove(true); +} + +var tests = [ + test_openSpecialDatabase_invalid_arg, + test_openDatabase_null_file, + test_openUnsharedDatabase_null_file, + test_openDatabase_file_DNE, + test_openDatabase_file_exists, + test_corrupt_db_throws_with_openDatabase, + test_fake_db_throws_with_openDatabase, + test_backup_not_new_filename, + test_backup_new_filename, + test_backup_new_folder, +]; + +function run_test() +{ + for (var i = 0; i < tests.length; i++) { + tests[i](); + } + + cleanup(); +} + diff --git a/storage/test/unit/test_storage_service_unshared.js b/storage/test/unit/test_storage_service_unshared.js new file mode 100644 index 000000000..70efb2a43 --- /dev/null +++ b/storage/test/unit/test_storage_service_unshared.js @@ -0,0 +1,35 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// This file tests the openUnsharedDatabase function of mozIStorageService. + +function test_openUnsharedDatabase_file_DNE() +{ + // the file should be created after calling + var db = getTestDB(); + do_check_false(db.exists()); + getService().openUnsharedDatabase(db); + do_check_true(db.exists()); +} + +function test_openUnsharedDatabase_file_exists() +{ + // it should already exist from our last test + var db = getTestDB(); + do_check_true(db.exists()); + getService().openUnsharedDatabase(db); + do_check_true(db.exists()); +} + +var tests = [test_openUnsharedDatabase_file_DNE, + test_openUnsharedDatabase_file_exists]; + +function run_test() +{ + for (var i = 0; i < tests.length; i++) + tests[i](); + + cleanup(); +} + diff --git a/storage/test/unit/test_storage_statement.js b/storage/test/unit/test_storage_statement.js new file mode 100644 index 000000000..026e271ac --- /dev/null +++ b/storage/test/unit/test_storage_statement.js @@ -0,0 +1,184 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// This file tests the functions of mozIStorageStatement + +function setup() +{ + getOpenedDatabase().createTable("test", "id INTEGER PRIMARY KEY, name TEXT"); +} + +function test_parameterCount_none() +{ + var stmt = createStatement("SELECT * FROM test"); + do_check_eq(0, stmt.parameterCount); + stmt.reset(); + stmt.finalize(); +} + +function test_parameterCount_one() +{ + var stmt = createStatement("SELECT * FROM test WHERE id = ?1"); + do_check_eq(1, stmt.parameterCount); + stmt.reset(); + stmt.finalize(); +} + +function test_getParameterName() +{ + var stmt = createStatement("SELECT * FROM test WHERE id = :id"); + do_check_eq(":id", stmt.getParameterName(0)); + stmt.reset(); + stmt.finalize(); +} + +function test_getParameterIndex_different() +{ + var stmt = createStatement("SELECT * FROM test WHERE id = :id OR name = :name"); + do_check_eq(0, stmt.getParameterIndex("id")); + do_check_eq(1, stmt.getParameterIndex("name")); + stmt.reset(); + stmt.finalize(); +} + +function test_getParameterIndex_same() +{ + var stmt = createStatement("SELECT * FROM test WHERE id = :test OR name = :test"); + do_check_eq(0, stmt.getParameterIndex("test")); + stmt.reset(); + stmt.finalize(); +} + +function test_columnCount() +{ + var stmt = createStatement("SELECT * FROM test WHERE id = ?1 OR name = ?2"); + do_check_eq(2, stmt.columnCount); + stmt.reset(); + stmt.finalize(); +} + +function test_getColumnName() +{ + var stmt = createStatement("SELECT name, id FROM test"); + do_check_eq("id", stmt.getColumnName(1)); + do_check_eq("name", stmt.getColumnName(0)); + stmt.reset(); + stmt.finalize(); +} + +function test_getColumnIndex_same_case() +{ + var stmt = createStatement("SELECT name, id FROM test"); + do_check_eq(0, stmt.getColumnIndex("name")); + do_check_eq(1, stmt.getColumnIndex("id")); + stmt.reset(); + stmt.finalize(); +} + +function test_getColumnIndex_different_case() +{ + var stmt = createStatement("SELECT name, id FROM test"); + try { + do_check_eq(0, stmt.getColumnIndex("NaMe")); + do_throw("should not get here"); + } catch (e) { + do_check_eq(Cr.NS_ERROR_INVALID_ARG, e.result); + } + try { + do_check_eq(1, stmt.getColumnIndex("Id")); + do_throw("should not get here"); + } catch (e) { + do_check_eq(Cr.NS_ERROR_INVALID_ARG, e.result); + } + stmt.reset(); + stmt.finalize(); +} + +function test_state_ready() +{ + var stmt = createStatement("SELECT name, id FROM test"); + do_check_eq(Ci.mozIStorageStatement.MOZ_STORAGE_STATEMENT_READY, stmt.state); + stmt.reset(); + stmt.finalize(); +} + +function test_state_executing() +{ + var stmt = createStatement("INSERT INTO test (name) VALUES ('foo')"); + stmt.execute(); + stmt.execute(); + stmt.finalize(); + + stmt = createStatement("SELECT name, id FROM test"); + stmt.executeStep(); + do_check_eq(Ci.mozIStorageStatement.MOZ_STORAGE_STATEMENT_EXECUTING, + stmt.state); + stmt.executeStep(); + do_check_eq(Ci.mozIStorageStatement.MOZ_STORAGE_STATEMENT_EXECUTING, + stmt.state); + stmt.reset(); + do_check_eq(Ci.mozIStorageStatement.MOZ_STORAGE_STATEMENT_READY, stmt.state); + stmt.finalize(); +} + +function test_state_after_finalize() +{ + var stmt = createStatement("SELECT name, id FROM test"); + stmt.executeStep(); + stmt.finalize(); + do_check_eq(Ci.mozIStorageStatement.MOZ_STORAGE_STATEMENT_INVALID, stmt.state); +} + +function test_failed_execute() +{ + var stmt = createStatement("INSERT INTO test (name) VALUES ('foo')"); + stmt.execute(); + stmt.finalize(); + var id = getOpenedDatabase().lastInsertRowID; + stmt = createStatement("INSERT INTO test(id, name) VALUES(:id, 'bar')"); + stmt.params.id = id; + try { + // Should throw a constraint error + stmt.execute(); + do_throw("Should have seen a constraint error"); + } + catch (e) { + do_check_eq(getOpenedDatabase().lastError, Ci.mozIStorageError.CONSTRAINT); + } + do_check_eq(Ci.mozIStorageStatement.MOZ_STORAGE_STATEMENT_READY, stmt.state); + // Should succeed without needing to reset the statement manually + stmt.finalize(); +} + +function test_bind_undefined() +{ + var stmt = createStatement("INSERT INTO test (name) VALUES ('foo')"); + + expectError(Cr.NS_ERROR_ILLEGAL_VALUE, + () => stmt.bindParameters(undefined)); + + stmt.finalize(); +} + +var tests = [test_parameterCount_none, test_parameterCount_one, + test_getParameterName, test_getParameterIndex_different, + test_getParameterIndex_same, test_columnCount, + test_getColumnName, test_getColumnIndex_same_case, + test_getColumnIndex_different_case, test_state_ready, + test_state_executing, test_state_after_finalize, + test_failed_execute, + test_bind_undefined, +]; + +function run_test() +{ + setup(); + + for (var i = 0; i < tests.length; i++) { + tests[i](); + } + + cleanup(); +} + diff --git a/storage/test/unit/test_storage_value_array.js b/storage/test/unit/test_storage_value_array.js new file mode 100644 index 000000000..27bd23992 --- /dev/null +++ b/storage/test/unit/test_storage_value_array.js @@ -0,0 +1,182 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// This file tests the functions of mozIStorageValueArray + +add_task(function* setup() { + getOpenedDatabase().createTable("test", "id INTEGER PRIMARY KEY, name TEXT," + + "number REAL, nuller NULL, blobber BLOB"); + + var stmt = createStatement("INSERT INTO test (name, number, blobber) " + + "VALUES (?1, ?2, ?3)"); + stmt.bindByIndex(0, "foo"); + stmt.bindByIndex(1, 2.34); + stmt.bindBlobByIndex(2, [], 0); + stmt.execute(); + + stmt.bindByIndex(0, ""); + stmt.bindByIndex(1, 1.23); + stmt.bindBlobByIndex(2, [1, 2], 2); + stmt.execute(); + + stmt.reset(); + stmt.finalize(); + + do_register_cleanup(cleanup); +}); + +add_task(function* test_getIsNull_for_null() { + var stmt = createStatement("SELECT nuller, blobber FROM test WHERE id = ?1"); + stmt.bindByIndex(0, 1); + do_check_true(stmt.executeStep()); + + do_check_true(stmt.getIsNull(0)); // null field + do_check_true(stmt.getIsNull(1)); // data is null if size is 0 + stmt.reset(); + stmt.finalize(); +}); + +add_task(function* test_getIsNull_for_non_null() { + var stmt = createStatement("SELECT name, blobber FROM test WHERE id = ?1"); + stmt.bindByIndex(0, 2); + do_check_true(stmt.executeStep()); + + do_check_false(stmt.getIsNull(0)); + do_check_false(stmt.getIsNull(1)); + stmt.reset(); + stmt.finalize(); +}); + +add_task(function* test_value_type_null() { + var stmt = createStatement("SELECT nuller FROM test WHERE id = ?1"); + stmt.bindByIndex(0, 1); + do_check_true(stmt.executeStep()); + + do_check_eq(Ci.mozIStorageValueArray.VALUE_TYPE_NULL, + stmt.getTypeOfIndex(0)); + stmt.reset(); + stmt.finalize(); +}); + +add_task(function* test_value_type_integer() { + var stmt = createStatement("SELECT id FROM test WHERE id = ?1"); + stmt.bindByIndex(0, 1); + do_check_true(stmt.executeStep()); + + do_check_eq(Ci.mozIStorageValueArray.VALUE_TYPE_INTEGER, + stmt.getTypeOfIndex(0)); + stmt.reset(); + stmt.finalize(); +}); + +add_task(function* test_value_type_float() { + var stmt = createStatement("SELECT number FROM test WHERE id = ?1"); + stmt.bindByIndex(0, 1); + do_check_true(stmt.executeStep()); + + do_check_eq(Ci.mozIStorageValueArray.VALUE_TYPE_FLOAT, + stmt.getTypeOfIndex(0)); + stmt.reset(); + stmt.finalize(); +}); + +add_task(function* test_value_type_text() { + var stmt = createStatement("SELECT name FROM test WHERE id = ?1"); + stmt.bindByIndex(0, 1); + do_check_true(stmt.executeStep()); + + do_check_eq(Ci.mozIStorageValueArray.VALUE_TYPE_TEXT, + stmt.getTypeOfIndex(0)); + stmt.reset(); + stmt.finalize(); +}); + +add_task(function* test_value_type_blob() { + var stmt = createStatement("SELECT blobber FROM test WHERE id = ?1"); + stmt.bindByIndex(0, 2); + do_check_true(stmt.executeStep()); + + do_check_eq(Ci.mozIStorageValueArray.VALUE_TYPE_BLOB, + stmt.getTypeOfIndex(0)); + stmt.reset(); + stmt.finalize(); +}); + +add_task(function* test_numEntries_one() { + var stmt = createStatement("SELECT blobber FROM test WHERE id = ?1"); + stmt.bindByIndex(0, 2); + do_check_true(stmt.executeStep()); + + do_check_eq(1, stmt.numEntries); + stmt.reset(); + stmt.finalize(); +}); + +add_task(function* test_numEntries_all() { + var stmt = createStatement("SELECT * FROM test WHERE id = ?1"); + stmt.bindByIndex(0, 2); + do_check_true(stmt.executeStep()); + + do_check_eq(5, stmt.numEntries); + stmt.reset(); + stmt.finalize(); +}); + +add_task(function* test_getInt() { + var stmt = createStatement("SELECT id FROM test WHERE id = ?1"); + stmt.bindByIndex(0, 2); + do_check_true(stmt.executeStep()); + + do_check_eq(2, stmt.getInt32(0)); + do_check_eq(2, stmt.getInt64(0)); + stmt.reset(); + stmt.finalize(); +}); + +add_task(function* test_getDouble() { + var stmt = createStatement("SELECT number FROM test WHERE id = ?1"); + stmt.bindByIndex(0, 2); + do_check_true(stmt.executeStep()); + + do_check_eq(1.23, stmt.getDouble(0)); + stmt.reset(); + stmt.finalize(); +}); + +add_task(function* test_getUTF8String() { + var stmt = createStatement("SELECT name FROM test WHERE id = ?1"); + stmt.bindByIndex(0, 1); + do_check_true(stmt.executeStep()); + + do_check_eq("foo", stmt.getUTF8String(0)); + stmt.reset(); + stmt.finalize(); +}); + +add_task(function* test_getString() { + var stmt = createStatement("SELECT name FROM test WHERE id = ?1"); + stmt.bindByIndex(0, 2); + do_check_true(stmt.executeStep()); + + do_check_eq("", stmt.getString(0)); + stmt.reset(); + stmt.finalize(); +}); + +add_task(function* test_getBlob() { + var stmt = createStatement("SELECT blobber FROM test WHERE id = ?1"); + stmt.bindByIndex(0, 2); + do_check_true(stmt.executeStep()); + + var count = { value: 0 }; + var arr = { value: null }; + stmt.getBlob(0, count, arr); + do_check_eq(2, count.value); + do_check_eq(1, arr.value[0]); + do_check_eq(2, arr.value[1]); + stmt.reset(); + stmt.finalize(); +}); + + diff --git a/storage/test/unit/test_telemetry_vfs.js b/storage/test/unit/test_telemetry_vfs.js new file mode 100644 index 000000000..0822fe3e7 --- /dev/null +++ b/storage/test/unit/test_telemetry_vfs.js @@ -0,0 +1,30 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// Make sure that there are telemetry entries created by sqlite io + +function run_sql(d, sql) { + var stmt = d.createStatement(sql); + stmt.execute(); + stmt.finalize(); +} + +function new_file(name) +{ + var file = dirSvc.get("ProfD", Ci.nsIFile); + file.append(name); + return file; +} +function run_test() +{ + const Telemetry = Cc["@mozilla.org/base/telemetry;1"].getService(Ci.nsITelemetry); + let read_hgram = Telemetry.getHistogramById("MOZ_SQLITE_OTHER_READ_B"); + let old_sum = read_hgram.snapshot().sum; + const file = new_file("telemetry.sqlite"); + var d = getDatabase(file); + run_sql(d, "CREATE TABLE bloat(data varchar)"); + run_sql(d, "DROP TABLE bloat"); + do_check_true(read_hgram.snapshot().sum > old_sum); +} + diff --git a/storage/test/unit/test_unicode.js b/storage/test/unit/test_unicode.js new file mode 100644 index 000000000..7753bbfdb --- /dev/null +++ b/storage/test/unit/test_unicode.js @@ -0,0 +1,83 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// This file tests the unicode functions that we have added + +const LATIN1_AE = "\xc6"; // "Æ" +const LATIN1_ae = "\xe6"; // "æ" + +add_task(function* setup() { + getOpenedDatabase().createTable("test", "id INTEGER PRIMARY KEY, name TEXT"); + + var stmt = createStatement("INSERT INTO test (name, id) VALUES (?1, ?2)"); + stmt.bindByIndex(0, LATIN1_AE); + stmt.bindByIndex(1, 1); + stmt.execute(); + stmt.bindByIndex(0, "A"); + stmt.bindByIndex(1, 2); + stmt.execute(); + stmt.bindByIndex(0, "b"); + stmt.bindByIndex(1, 3); + stmt.execute(); + stmt.bindByIndex(0, LATIN1_ae); + stmt.bindByIndex(1, 4); + stmt.execute(); + stmt.finalize(); + + do_register_cleanup(cleanup); +}); + +add_task(function* test_upper_ascii() { + var stmt = createStatement("SELECT name, id FROM test WHERE name = upper('a')"); + do_check_true(stmt.executeStep()); + do_check_eq("A", stmt.getString(0)); + do_check_eq(2, stmt.getInt32(1)); + stmt.reset(); + stmt.finalize(); +}); + +add_task(function* test_upper_non_ascii() { + var stmt = createStatement("SELECT name, id FROM test WHERE name = upper(?1)"); + stmt.bindByIndex(0, LATIN1_ae); + do_check_true(stmt.executeStep()); + do_check_eq(LATIN1_AE, stmt.getString(0)); + do_check_eq(1, stmt.getInt32(1)); + stmt.reset(); + stmt.finalize(); +}); + +add_task(function* test_lower_ascii() { + var stmt = createStatement("SELECT name, id FROM test WHERE name = lower('B')"); + do_check_true(stmt.executeStep()); + do_check_eq("b", stmt.getString(0)); + do_check_eq(3, stmt.getInt32(1)); + stmt.reset(); + stmt.finalize(); +}); + +add_task(function* test_lower_non_ascii() { + var stmt = createStatement("SELECT name, id FROM test WHERE name = lower(?1)"); + stmt.bindByIndex(0, LATIN1_AE); + do_check_true(stmt.executeStep()); + do_check_eq(LATIN1_ae, stmt.getString(0)); + do_check_eq(4, stmt.getInt32(1)); + stmt.reset(); + stmt.finalize(); +}); + +add_task(function* test_like_search_different() { + var stmt = createStatement("SELECT COUNT(*) FROM test WHERE name LIKE ?1"); + stmt.bindByIndex(0, LATIN1_AE); + do_check_true(stmt.executeStep()); + do_check_eq(2, stmt.getInt32(0)); + stmt.finalize(); +}); + +add_task(function* test_like_search_same() { + var stmt = createStatement("SELECT COUNT(*) FROM test WHERE name LIKE ?1"); + stmt.bindByIndex(0, LATIN1_ae); + do_check_true(stmt.executeStep()); + do_check_eq(2, stmt.getInt32(0)); + stmt.finalize(); +}); diff --git a/storage/test/unit/test_vacuum.js b/storage/test/unit/test_vacuum.js new file mode 100644 index 000000000..e284f78c7 --- /dev/null +++ b/storage/test/unit/test_vacuum.js @@ -0,0 +1,335 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// This file tests the Vacuum Manager. + +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); +Components.utils.import("resource://gre/modules/Services.jsm"); + +/** + * Loads a test component that will register as a vacuum-participant. + * If other participants are found they will be unregistered, to avoid conflicts + * with the test itself. + */ +function load_test_vacuum_component() +{ + const CATEGORY_NAME = "vacuum-participant"; + + do_load_manifest("vacuumParticipant.manifest"); + + // This is a lazy check, there could be more participants than just this test + // we just mind that the test exists though. + const EXPECTED_ENTRIES = ["vacuumParticipant"]; + let catMan = Cc["@mozilla.org/categorymanager;1"]. + getService(Ci.nsICategoryManager); + let found = false; + let entries = catMan.enumerateCategory(CATEGORY_NAME); + while (entries.hasMoreElements()) { + let entry = entries.getNext().QueryInterface(Ci.nsISupportsCString).data; + print("Check if the found category entry (" + entry + ") is expected."); + if (EXPECTED_ENTRIES.indexOf(entry) != -1) { + print("Check that only one test entry exists."); + do_check_false(found); + found = true; + } + else { + // Temporary unregister other participants for this test. + catMan.deleteCategoryEntry("vacuum-participant", entry, false); + } + } + print("Check the test entry exists."); + do_check_true(found); +} + +/** + * Sends a fake idle-daily notification to the VACUUM Manager. + */ +function synthesize_idle_daily() +{ + let vm = Cc["@mozilla.org/storage/vacuum;1"].getService(Ci.nsIObserver); + vm.observe(null, "idle-daily", null); +} + +/** + * Returns a new nsIFile reference for a profile database. + * @param filename for the database, excluded the .sqlite extension. + */ +function new_db_file(name) +{ + let file = Services.dirsvc.get("ProfD", Ci.nsIFile); + file.append(name + ".sqlite"); + return file; +} + +function run_test() +{ + do_test_pending(); + + // Change initial page size. Do it immediately since it would require an + // additional vacuum op to do it later. As a bonus this makes the page size + // change test really fast since it only has to check results. + let conn = getDatabase(new_db_file("testVacuum")); + conn.executeSimpleSQL("PRAGMA page_size = 1024"); + print("Check current page size."); + let stmt = conn.createStatement("PRAGMA page_size"); + try { + while (stmt.executeStep()) { + do_check_eq(stmt.row.page_size, 1024); + } + } + finally { + stmt.finalize(); + } + + load_test_vacuum_component(); + + run_next_test(); +} + +const TESTS = [ + + function test_common_vacuum() + { + print("\n*** Test that a VACUUM correctly happens and all notifications are fired."); + // Wait for VACUUM begin. + let beginVacuumReceived = false; + Services.obs.addObserver(function onVacuum(aSubject, aTopic, aData) { + Services.obs.removeObserver(onVacuum, aTopic); + beginVacuumReceived = true; + }, "test-begin-vacuum", false); + + // Wait for heavy IO notifications. + let heavyIOTaskBeginReceived = false; + let heavyIOTaskEndReceived = false; + Services.obs.addObserver(function onVacuum(aSubject, aTopic, aData) { + if (heavyIOTaskBeginReceived && heavyIOTaskEndReceived) { + Services.obs.removeObserver(onVacuum, aTopic); + } + + if (aData == "vacuum-begin") { + heavyIOTaskBeginReceived = true; + } + else if (aData == "vacuum-end") { + heavyIOTaskEndReceived = true; + } + }, "heavy-io-task", false); + + // Wait for VACUUM end. + Services.obs.addObserver(function onVacuum(aSubject, aTopic, aData) { + Services.obs.removeObserver(onVacuum, aTopic); + print("Check we received onBeginVacuum"); + do_check_true(beginVacuumReceived); + print("Check we received heavy-io-task notifications"); + do_check_true(heavyIOTaskBeginReceived); + do_check_true(heavyIOTaskEndReceived); + print("Received onEndVacuum"); + run_next_test(); + }, "test-end-vacuum", false); + + synthesize_idle_daily(); + }, + + function test_skipped_if_recent_vacuum() + { + print("\n*** Test that a VACUUM is skipped if it was run recently."); + Services.prefs.setIntPref("storage.vacuum.last.testVacuum.sqlite", + parseInt(Date.now() / 1000)); + + // Wait for VACUUM begin. + let vacuumObserver = { + gotNotification: false, + observe: function VO_observe(aSubject, aTopic, aData) { + this.gotNotification = true; + }, + QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]) + }; + Services.obs.addObserver(vacuumObserver, "test-begin-vacuum", false); + + // Check after a couple seconds that no VACUUM has been run. + do_timeout(2000, function () { + print("Check VACUUM did not run."); + do_check_false(vacuumObserver.gotNotification); + Services.obs.removeObserver(vacuumObserver, "test-begin-vacuum"); + run_next_test(); + }); + + synthesize_idle_daily(); + }, + + function test_page_size_change() + { + print("\n*** Test that a VACUUM changes page_size"); + + // We did setup the database with a small page size, the previous vacuum + // should have updated it. + print("Check that page size was updated."); + let conn = getDatabase(new_db_file("testVacuum")); + let stmt = conn.createStatement("PRAGMA page_size"); + try { + while (stmt.executeStep()) { + do_check_eq(stmt.row.page_size, conn.defaultPageSize); + } + } + finally { + stmt.finalize(); + } + + run_next_test(); + }, + + function test_skipped_optout_vacuum() + { + print("\n*** Test that a VACUUM is skipped if the participant wants to opt-out."); + Services.obs.notifyObservers(null, "test-options", "opt-out"); + + // Wait for VACUUM begin. + let vacuumObserver = { + gotNotification: false, + observe: function VO_observe(aSubject, aTopic, aData) { + this.gotNotification = true; + }, + QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]) + }; + Services.obs.addObserver(vacuumObserver, "test-begin-vacuum", false); + + // Check after a couple seconds that no VACUUM has been run. + do_timeout(2000, function () { + print("Check VACUUM did not run."); + do_check_false(vacuumObserver.gotNotification); + Services.obs.removeObserver(vacuumObserver, "test-begin-vacuum"); + run_next_test(); + }); + + synthesize_idle_daily(); + }, + + /* Changing page size on WAL is not supported till Bug 634374 is properly fixed. + function test_page_size_change_with_wal() + { + print("\n*** Test that a VACUUM changes page_size with WAL mode"); + Services.obs.notifyObservers(null, "test-options", "wal"); + + // Set a small page size. + let conn = getDatabase(new_db_file("testVacuum2")); + conn.executeSimpleSQL("PRAGMA page_size = 1024"); + let stmt = conn.createStatement("PRAGMA page_size"); + try { + while (stmt.executeStep()) { + do_check_eq(stmt.row.page_size, 1024); + } + } + finally { + stmt.finalize(); + } + + // Use WAL journal mode. + conn.executeSimpleSQL("PRAGMA journal_mode = WAL"); + stmt = conn.createStatement("PRAGMA journal_mode"); + try { + while (stmt.executeStep()) { + do_check_eq(stmt.row.journal_mode, "wal"); + } + } + finally { + stmt.finalize(); + } + + // Wait for VACUUM end. + let vacuumObserver = { + observe: function VO_observe(aSubject, aTopic, aData) { + Services.obs.removeObserver(this, aTopic); + print("Check page size has been updated."); + let stmt = conn.createStatement("PRAGMA page_size"); + try { + while (stmt.executeStep()) { + do_check_eq(stmt.row.page_size, Ci.mozIStorageConnection.DEFAULT_PAGE_SIZE); + } + } + finally { + stmt.finalize(); + } + + print("Check journal mode has been restored."); + stmt = conn.createStatement("PRAGMA journal_mode"); + try { + while (stmt.executeStep()) { + do_check_eq(stmt.row.journal_mode, "wal"); + } + } + finally { + stmt.finalize(); + } + + run_next_test(); + }, + QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]) + } + Services.obs.addObserver(vacuumObserver, "test-end-vacuum", false); + + synthesize_idle_daily(); + }, + */ + + function test_memory_database_crash() + { + print("\n*** Test that we don't crash trying to vacuum a memory database"); + Services.obs.notifyObservers(null, "test-options", "memory"); + + // Wait for VACUUM begin. + let vacuumObserver = { + gotNotification: false, + observe: function VO_observe(aSubject, aTopic, aData) { + this.gotNotification = true; + }, + QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]) + }; + Services.obs.addObserver(vacuumObserver, "test-begin-vacuum", false); + + // Check after a couple seconds that no VACUUM has been run. + do_timeout(2000, function () { + print("Check VACUUM did not run."); + do_check_false(vacuumObserver.gotNotification); + Services.obs.removeObserver(vacuumObserver, "test-begin-vacuum"); + run_next_test(); + }); + + synthesize_idle_daily(); + }, + + /* Changing page size on WAL is not supported till Bug 634374 is properly fixed. + function test_wal_restore_fail() + { + print("\n*** Test that a failing WAL restoration notifies failure"); + Services.obs.notifyObservers(null, "test-options", "wal-fail"); + + // Wait for VACUUM end. + let vacuumObserver = { + observe: function VO_observe(aSubject, aTopic, aData) { + Services.obs.removeObserver(vacuumObserver, "test-end-vacuum"); + print("Check WAL restoration failed."); + do_check_false(aData); + run_next_test(); + }, + QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]) + } + Services.obs.addObserver(vacuumObserver, "test-end-vacuum", false); + + synthesize_idle_daily(); + }, + */ +]; + +function run_next_test() +{ + if (TESTS.length == 0) { + Services.obs.notifyObservers(null, "test-options", "dispose"); + do_test_finished(); + } + else { + // Set last VACUUM to a date in the past. + Services.prefs.setIntPref("storage.vacuum.last.testVacuum.sqlite", + parseInt(Date.now() / 1000 - 31 * 86400)); + do_execute_soon(TESTS.shift()); + } +} diff --git a/storage/test/unit/vacuumParticipant.js b/storage/test/unit/vacuumParticipant.js new file mode 100644 index 000000000..01b980178 --- /dev/null +++ b/storage/test/unit/vacuumParticipant.js @@ -0,0 +1,125 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// This testing component is used in test_vacuum* tests. + +const Cc = Components.classes; +const Ci = Components.interfaces; + +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); +Components.utils.import("resource://gre/modules/Services.jsm"); + +/** + * Returns a new nsIFile reference for a profile database. + * @param filename for the database, excluded the .sqlite extension. + */ +function new_db_file(name) +{ + let file = Services.dirsvc.get("ProfD", Ci.nsIFile); + file.append(name + ".sqlite"); + return file; +} + +/** + * Opens and returns a connection to the provided database file. + * @param nsIFile interface to the database file. + */ +function getDatabase(aFile) +{ + return Cc["@mozilla.org/storage/service;1"].getService(Ci.mozIStorageService) + .openDatabase(aFile); +} + +function vacuumParticipant() +{ + this._dbConn = getDatabase(new_db_file("testVacuum")); + Services.obs.addObserver(this, "test-options", false); +} + +vacuumParticipant.prototype = +{ + classDescription: "vacuumParticipant", + classID: Components.ID("{52aa0b22-b82f-4e38-992a-c3675a3355d2}"), + contractID: "@unit.test.com/test-vacuum-participant;1", + + get expectedDatabasePageSize() { + return this._dbConn.defaultPageSize; + }, + get databaseConnection() { + return this._dbConn; + }, + + _grant: true, + onBeginVacuum: function TVP_onBeginVacuum() + { + if (!this._grant) { + this._grant = true; + return false; + } + Services.obs.notifyObservers(null, "test-begin-vacuum", null); + return true; + }, + onEndVacuum: function TVP_EndVacuum(aSucceeded) + { + if (this._stmt) { + this._stmt.finalize(); + } + Services.obs.notifyObservers(null, "test-end-vacuum", aSucceeded); + }, + + observe: function TVP_observe(aSubject, aTopic, aData) + { + if (aData == "opt-out") { + this._grant = false; + } + else if (aData == "wal") { + try { + this._dbConn.close(); + } catch (e) { + // Do nothing. + } + this._dbConn = getDatabase(new_db_file("testVacuum2")); + } + else if (aData == "wal-fail") { + try { + this._dbConn.close(); + } catch (e) { + // Do nothing. + } + this._dbConn = getDatabase(new_db_file("testVacuum3")); + // Use WAL journal mode. + this._dbConn.executeSimpleSQL("PRAGMA journal_mode = WAL"); + // Create a not finalized statement. + this._stmt = this._dbConn.createStatement("SELECT :test"); + this._stmt.params.test = 1; + this._stmt.executeStep(); + } + else if (aData == "memory") { + try { + this._dbConn.asyncClose(); + } catch (e) { + // Do nothing. + } + this._dbConn = Cc["@mozilla.org/storage/service;1"]. + getService(Ci.mozIStorageService). + openSpecialDatabase("memory"); + } + else if (aData == "dispose") { + Services.obs.removeObserver(this, "test-options"); + try { + this._dbConn.asyncClose(); + } catch (e) { + // Do nothing. + } + } + }, + + QueryInterface: XPCOMUtils.generateQI([ + Ci.mozIStorageVacuumParticipant, + Ci.nsIObserver, + ]) +}; + +var gComponentsArray = [vacuumParticipant]; +this.NSGetFactory = XPCOMUtils.generateNSGetFactory(gComponentsArray); diff --git a/storage/test/unit/vacuumParticipant.manifest b/storage/test/unit/vacuumParticipant.manifest new file mode 100644 index 000000000..cf359a80f --- /dev/null +++ b/storage/test/unit/vacuumParticipant.manifest @@ -0,0 +1,3 @@ +component {52aa0b22-b82f-4e38-992a-c3675a3355d2} vacuumParticipant.js +contract @unit.test.com/test-vacuum-participant;1 {52aa0b22-b82f-4e38-992a-c3675a3355d2} +category vacuum-participant vacuumParticipant @unit.test.com/test-vacuum-participant;1 diff --git a/storage/test/unit/xpcshell.ini b/storage/test/unit/xpcshell.ini new file mode 100644 index 000000000..e93c7d5b9 --- /dev/null +++ b/storage/test/unit/xpcshell.ini @@ -0,0 +1,46 @@ +[DEFAULT] +head = head_storage.js +tail = +support-files = + corruptDB.sqlite + fakeDB.sqlite + locale_collation.txt + vacuumParticipant.js + vacuumParticipant.manifest + +[test_bug-365166.js] +[test_bug-393952.js] +[test_bug-429521.js] +[test_bug-444233.js] +[test_cache_size.js] +[test_chunk_growth.js] +# Bug 676981: test fails consistently on Android +fail-if = os == "android" +[test_connection_asyncClose.js] +[test_connection_executeAsync.js] +[test_connection_executeSimpleSQLAsync.js] +[test_js_helpers.js] +[test_levenshtein.js] +[test_like.js] +[test_like_escape.js] +[test_locale_collation.js] +[test_page_size_is_32k.js] +[test_sqlite_secure_delete.js] +[test_statement_executeAsync.js] +[test_statement_wrapper_automatically.js] +[test_storage_aggregates.js] +[test_storage_connection.js] +# Bug 676981: test fails consistently on Android +fail-if = os == "android" +[test_storage_fulltextindex.js] +[test_storage_function.js] +[test_storage_progresshandler.js] +[test_storage_service.js] +[test_storage_service_unshared.js] +[test_storage_statement.js] +[test_storage_value_array.js] +[test_unicode.js] +[test_vacuum.js] +[test_telemetry_vfs.js] +# Bug 676981: test fails consistently on Android +# fail-if = os == "android" diff --git a/storage/variantToSQLiteT_impl.h b/storage/variantToSQLiteT_impl.h new file mode 100644 index 000000000..5e55a261f --- /dev/null +++ b/storage/variantToSQLiteT_impl.h @@ -0,0 +1,127 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// Note: we are already in the namepace mozilla::storage + +// Note 2: whoever #includes this file must provide implementations of +// sqlite3_T_* prior. + +//////////////////////////////////////////////////////////////////////////////// +//// variantToSQLiteT Implementation + +template <typename T> +int +variantToSQLiteT(T aObj, + nsIVariant *aValue) +{ + // Allow to return nullptr not wrapped to nsIVariant for speed. + if (!aValue) + return sqlite3_T_null(aObj); + + uint16_t valueType; + aValue->GetDataType(&valueType); + switch (valueType) { + case nsIDataType::VTYPE_INT8: + case nsIDataType::VTYPE_INT16: + case nsIDataType::VTYPE_INT32: + case nsIDataType::VTYPE_UINT8: + case nsIDataType::VTYPE_UINT16: + { + int32_t value; + nsresult rv = aValue->GetAsInt32(&value); + NS_ENSURE_SUCCESS(rv, SQLITE_MISMATCH); + return sqlite3_T_int(aObj, value); + } + case nsIDataType::VTYPE_UINT32: // Try to preserve full range + case nsIDataType::VTYPE_INT64: + // Data loss possible, but there is no unsigned types in SQLite + case nsIDataType::VTYPE_UINT64: + { + int64_t value; + nsresult rv = aValue->GetAsInt64(&value); + NS_ENSURE_SUCCESS(rv, SQLITE_MISMATCH); + return sqlite3_T_int64(aObj, value); + } + case nsIDataType::VTYPE_FLOAT: + case nsIDataType::VTYPE_DOUBLE: + { + double value; + nsresult rv = aValue->GetAsDouble(&value); + NS_ENSURE_SUCCESS(rv, SQLITE_MISMATCH); + return sqlite3_T_double(aObj, value); + } + case nsIDataType::VTYPE_BOOL: + { + bool value; + nsresult rv = aValue->GetAsBool(&value); + NS_ENSURE_SUCCESS(rv, SQLITE_MISMATCH); + return sqlite3_T_int(aObj, value ? 1 : 0); + } + case nsIDataType::VTYPE_CHAR: + case nsIDataType::VTYPE_CHAR_STR: + case nsIDataType::VTYPE_STRING_SIZE_IS: + case nsIDataType::VTYPE_UTF8STRING: + case nsIDataType::VTYPE_CSTRING: + { + nsAutoCString value; + // GetAsAUTF8String should never perform conversion when coming from + // 8-bit string types, and thus can accept strings with arbitrary encoding + // (including UTF8 and ASCII). + nsresult rv = aValue->GetAsAUTF8String(value); + NS_ENSURE_SUCCESS(rv, SQLITE_MISMATCH); + return sqlite3_T_text(aObj, value); + } + case nsIDataType::VTYPE_WCHAR: + case nsIDataType::VTYPE_DOMSTRING: + case nsIDataType::VTYPE_WCHAR_STR: + case nsIDataType::VTYPE_WSTRING_SIZE_IS: + case nsIDataType::VTYPE_ASTRING: + { + nsAutoString value; + // GetAsAString does proper conversion to UCS2 from all string-like types. + // It can be used universally without problems (unless someone implements + // their own variant, but that's their problem). + nsresult rv = aValue->GetAsAString(value); + NS_ENSURE_SUCCESS(rv, SQLITE_MISMATCH); + return sqlite3_T_text16(aObj, value); + } + case nsIDataType::VTYPE_VOID: + case nsIDataType::VTYPE_EMPTY: + case nsIDataType::VTYPE_EMPTY_ARRAY: + return sqlite3_T_null(aObj); + case nsIDataType::VTYPE_ARRAY: + { + uint16_t arrayType; + nsIID iid; + uint32_t count; + void *data; + nsresult rv = aValue->GetAsArray(&arrayType, &iid, &count, &data); + NS_ENSURE_SUCCESS(rv, SQLITE_MISMATCH); + + // Check to make sure it's a supported type. + NS_ASSERTION(arrayType == nsIDataType::VTYPE_UINT8, + "Invalid type passed! You may leak!"); + if (arrayType != nsIDataType::VTYPE_UINT8) { + // Technically this could leak with certain data types, but somebody was + // being stupid passing us this anyway. + free(data); + return SQLITE_MISMATCH; + } + + // Finally do our thing. The function should free the array accordingly! + int rc = sqlite3_T_blob(aObj, data, count); + return rc; + } + // Maybe, it'll be possible to convert these + // in future too. + case nsIDataType::VTYPE_ID: + case nsIDataType::VTYPE_INTERFACE: + case nsIDataType::VTYPE_INTERFACE_IS: + default: + return SQLITE_MISMATCH; + } + return SQLITE_OK; +} |