diff --git a/dom/indexedDB/IDBFactory.cpp b/dom/indexedDB/IDBFactory.cpp index 780646a57183..442ea8974c58 100644 --- a/dom/indexedDB/IDBFactory.cpp +++ b/dom/indexedDB/IDBFactory.cpp @@ -457,6 +457,6 @@ IDBFactory::Cmp(const jsval& aFirst, return NS_ERROR_DOM_INDEXEDDB_DATA_ERR; } - *_retval = first == second ? 0 : first < second ? -1 : 1; + *_retval = Key::CompareKeys(first, second); return NS_OK; } diff --git a/dom/indexedDB/IDBKeyRange.cpp b/dom/indexedDB/IDBKeyRange.cpp index 111567199270..f28c84d20df4 100644 --- a/dom/indexedDB/IDBKeyRange.cpp +++ b/dom/indexedDB/IDBKeyRange.cpp @@ -272,7 +272,9 @@ IDBKeyRange::FromJSVal(JSContext* aCx, if (JSVAL_IS_VOID(aVal) || JSVAL_IS_NULL(aVal)) { // undefined and null returns no IDBKeyRange. } - else if (JSVAL_IS_PRIMITIVE(aVal)) { + else if (JSVAL_IS_PRIMITIVE(aVal) || + JS_IsArrayObject(aCx, JSVAL_TO_OBJECT(aVal)) || + JS_ObjectIsDate(aCx, JSVAL_TO_OBJECT(aVal))) { // A valid key returns an 'only' IDBKeyRange. keyRange = new IDBKeyRange(false, false, true); diff --git a/dom/indexedDB/IDBObjectStore.cpp b/dom/indexedDB/IDBObjectStore.cpp index 6cff1d9f865d..d34a9356dbba 100644 --- a/dom/indexedDB/IDBObjectStore.cpp +++ b/dom/indexedDB/IDBObjectStore.cpp @@ -55,6 +55,8 @@ #include "nsThreadUtils.h" #include "snappy/snappy.h" #include "test_quota.h" +#include "xpcprivate.h" +#include "XPCQuickStubs.h" #include "AsyncConnectionHelper.h" #include "IDBCursor.h" @@ -1908,10 +1910,10 @@ AddHelper::DoDatabaseWork(mozIStorageConnection* aConnection) } mKey.SetFromInteger(autoIncrementNum); } - else if (mKey.IsInteger() && - mKey.ToInteger() >= mObjectStore->Info()->nextAutoIncrementId) { + else if (mKey.IsFloat() && + mKey.ToFloat() >= mObjectStore->Info()->nextAutoIncrementId) { // XXX Once we support floats, we should use floor(mKey.ToFloat()) here - autoIncrementNum = mKey.ToInteger(); + autoIncrementNum = floor(mKey.ToFloat()); } if (keyUnset && !keyPath.IsEmpty()) { @@ -1925,7 +1927,7 @@ AddHelper::DoDatabaseWork(mozIStorageConnection* aConnection) PRUint64 u; } pun; - pun.d = SwapBytes(static_cast(mKey.ToInteger())); + pun.d = SwapBytes(static_cast(autoIncrementNum)); JSAutoStructuredCloneBuffer& buffer = mCloneWriteInfo.mCloneBuffer; PRUint64 offsetToKeyProp = mCloneWriteInfo.mOffsetToKeyProp; diff --git a/dom/indexedDB/IndexedDatabaseManager.cpp b/dom/indexedDB/IndexedDatabaseManager.cpp index a46c8f9873e6..b8f1f05e0f75 100644 --- a/dom/indexedDB/IndexedDatabaseManager.cpp +++ b/dom/indexedDB/IndexedDatabaseManager.cpp @@ -56,6 +56,7 @@ #include "nsXPCOM.h" #include "nsXPCOMPrivate.h" #include "test_quota.h" +#include "xpcprivate.h" #include "AsyncConnectionHelper.h" #include "CheckQuotaHelper.h" diff --git a/dom/indexedDB/Key.cpp b/dom/indexedDB/Key.cpp new file mode 100644 index 000000000000..34d356e13082 --- /dev/null +++ b/dom/indexedDB/Key.cpp @@ -0,0 +1,443 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Indexed Database. + * + * The Initial Developer of the Original Code is The Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Ben Turner + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +#include "Key.h" +#include "nsIStreamBufferAccess.h" +#include "jsdate.h" +#include "nsContentUtils.h" +#include "nsJSUtils.h" +#include "xpcprivate.h" +#include "XPCQuickStubs.h" + +USING_INDEXEDDB_NAMESPACE + +/* + Here's how we encode keys: + + Basic strategy is the following + + Numbers: 1 n n n n n n n n ("n"s are encoded 64bit float) + Dates: 2 n n n n n n n n ("n"s are encoded 64bit float) + Strings: 3 s s s ... 0 ("s"s are encoded unicode bytes) + Arrays: 4 i i i ... 0 ("i"s are encoded array items) + + + When encoding floats, 64bit IEEE 754 are almost sortable, except that + positive sort lower than negative, and negative sort descending. So we use + the following encoding: + + value < 0 ? + (-to64bitInt(value)) : + (to64bitInt(value) | 0x8000000000000000) + + + When encoding strings, we use variable-size encoding per the following table + + Chars 0 - 7E are encoded as 0xxxxxxx with 1 added + Chars 7F - (3FFF+7F) are encoded as 10xxxxxx xxxxxxxx with 7F subtracted + Chars (3FFF+80) - FFFF are encoded as 11xxxxxx xxxxxxxx xx000000 + + This ensures that the first byte is never encoded as 0, which means that the + string terminator (per basic-stategy table) sorts before any character. + The reason that (3FFF+80) - FFFF is encoded "shifted up" 6 bits is to maximize + the chance that the last character is 0. See below for why. + + + When encoding Arrays, we use an additional trick. Rather than adding a byte + containing the value '4' to indicate type, we instead add 4 to the next byte. + This is usually the byte containing the type of the first item in the array. + So simple examples are + + ["foo"] 7 s s s 0 0 // 7 is 3 + 4 + [1, 2] 5 n n n n n n n n 1 n n n n n n n n 0 // 5 is 1 + 4 + + Whe do this iteratively if the first item in the array is also an array + + [["foo"]] 11 s s s 0 0 0 + + However, to avoid overflow in the byte, we only do this 3 times. If the first + item in an array is an array, and that array also has an array as first item, + we simply write out the total value accumulated so far and then follow the + "normal" rules. + + [[["foo"]]] 12 3 s s s 0 0 0 0 + + There is another edge case that can happen though, which is that the array + doesn't have a first item to which we can add 4 to the type. Instead the + next byte would normally be the array terminator (per basic-strategy table) + so we simply add the 4 there. + + [[]] 8 0 // 8 is 4 + 4 + 0 + [] 4 // 4 is 4 + 0 + [[], "foo"] 8 3 s s s 0 0 // 8 is 4 + 4 + 0 + + Note that the max-3-times rule kicks in before we get a chance to add to the + array terminator + + [[[]]] 12 0 0 0 // 12 is 4 + 4 + 4 + + We could use a much higher number than 3 at no complexity or performance cost, + however it seems unlikely that it'll make a practical difference, and the low + limit makes testing eaiser. + + + As a final optimization we do a post-encoding step which drops all 0s at the + end of the encoded buffer. + + "foo" // 3 s s s + 1 // 1 bf f0 + ["a", "b"] // 7 s 3 s + [1, 2] // 5 bf f0 0 0 0 0 0 0 1 c0 + [[]] // 8 +*/ + +const int MaxArrayCollapse = 3; + +nsresult +Key::EncodeJSVal(JSContext* aCx, const jsval aVal, PRUint8 aTypeOffset) +{ + PR_STATIC_ASSERT(eMaxType * MaxArrayCollapse < 256); + + if (JSVAL_IS_STRING(aVal)) { + nsDependentJSString str; + if (!str.init(aCx, aVal)) { + return NS_ERROR_OUT_OF_MEMORY; + } + EncodeString(str, aTypeOffset); + return NS_OK; + } + + if (JSVAL_IS_INT(aVal)) { + EncodeNumber((double)JSVAL_TO_INT(aVal), eFloat + aTypeOffset); + return NS_OK; + } + + if (JSVAL_IS_DOUBLE(aVal)) { + double d = JSVAL_TO_DOUBLE(aVal); + if (DOUBLE_IS_NaN(d)) { + return NS_ERROR_DOM_INDEXEDDB_DATA_ERR; + } + EncodeNumber(d, eFloat + aTypeOffset); + return NS_OK; + } + + if (!JSVAL_IS_PRIMITIVE(aVal)) { + JSObject* obj = JSVAL_TO_OBJECT(aVal); + if (JS_IsArrayObject(aCx, obj)) { + aTypeOffset += eMaxType; + + if (aTypeOffset == eMaxType * MaxArrayCollapse) { + mBuffer.Append(aTypeOffset); + aTypeOffset = 0; + } + NS_ASSERTION((aTypeOffset % eMaxType) == 0 && + aTypeOffset < (eMaxType * MaxArrayCollapse), + "Wrong typeoffset"); + + jsuint length; + if (!JS_GetArrayLength(aCx, obj, &length)) { + return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; + } + + for (jsuint index = 0; index < length; index++) { + jsval val; + if (!JS_GetElement(aCx, obj, index, &val)) { + return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; + } + + nsresult rv = EncodeJSVal(aCx, val, aTypeOffset); + NS_ENSURE_SUCCESS(rv, rv); + + aTypeOffset = 0; + } + + mBuffer.Append(eTerminator + aTypeOffset); + + return NS_OK; + } + + if (JS_ObjectIsDate(aCx, obj)) { + EncodeNumber(js_DateGetMsecSinceEpoch(aCx, obj), eDate + aTypeOffset); + return NS_OK; + } + } + + return NS_ERROR_DOM_INDEXEDDB_DATA_ERR; +} + +// static +nsresult +Key::DecodeJSVal(const unsigned char*& aPos, const unsigned char* aEnd, + JSContext* aCx, PRUint8 aTypeOffset, jsval* aVal) +{ + if (*aPos - aTypeOffset >= eArray) { + JSObject* array = JS_NewArrayObject(aCx, 0, nsnull); + if (!array) { + NS_WARNING("Failed to make array!"); + return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; + } + + aTypeOffset += eMaxType; + + if (aTypeOffset == eMaxType * MaxArrayCollapse) { + ++aPos; + aTypeOffset = 0; + } + + jsuint index = 0; + while (aPos < aEnd && *aPos - aTypeOffset != eTerminator) { + jsval val; + nsresult rv = DecodeJSVal(aPos, aEnd, aCx, aTypeOffset, &val); + NS_ENSURE_SUCCESS(rv, rv); + + aTypeOffset = 0; + + if (!JS_SetElement(aCx, array, index++, &val)) { + NS_WARNING("Failed to set array element!"); + return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; + } + } + + NS_ASSERTION(aPos >= aEnd || (*aPos % eMaxType) == eTerminator, + "Should have found end-of-array marker"); + ++aPos; + + *aVal = OBJECT_TO_JSVAL(array); + } + else if (*aPos - aTypeOffset == eString) { + nsString key; + DecodeString(aPos, aEnd, key); + if (!xpc_qsStringToJsval(aCx, key, aVal)) { + return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; + } + } + else if (*aPos - aTypeOffset == eDate) { + jsdouble msec = static_cast(DecodeNumber(aPos, aEnd)); + JSObject* date = JS_NewDateObjectMsec(aCx, msec); + if (!date) { + NS_WARNING("Failed to make date!"); + return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; + } + + *aVal = OBJECT_TO_JSVAL(date); + } + else if (*aPos - aTypeOffset == eFloat) { + *aVal = DOUBLE_TO_JSVAL(DecodeNumber(aPos, aEnd)); + } + else { + NS_NOTREACHED("Unknown key type!"); + } + + return NS_OK; +} + + +#define ONE_BYTE_LIMIT 0x7E +#define TWO_BYTE_LIMIT (0x3FFF+0x7F) + +#define ONE_BYTE_ADJUST 1 +#define TWO_BYTE_ADJUST (-0x7F) +#define THREE_BYTE_SHIFT 6 + +void +Key::EncodeString(const nsAString& aString, PRUint8 aTypeOffset) +{ + // First measure how long the encoded string will be. + + // The +2 is for initial 3 and trailing 0. We'll compensate for multi-byte + // chars below. + PRUint32 size = aString.Length() + 2; + + const PRUnichar* start = aString.BeginReading(); + const PRUnichar* end = aString.EndReading(); + for (const PRUnichar* iter = start; iter < end; ++iter) { + if (*iter > ONE_BYTE_LIMIT) { + size += *iter > TWO_BYTE_LIMIT ? 2 : 1; + } + } + + // Allocate memory for the new size + PRUint32 oldLen = mBuffer.Length(); + char* buffer; + if (!mBuffer.GetMutableData(&buffer, oldLen + size)) { + return; + } + buffer += oldLen; + + // Write type marker + *(buffer++) = eString + aTypeOffset; + + // Encode string + for (const PRUnichar* iter = start; iter < end; ++iter) { + if (*iter <= ONE_BYTE_LIMIT) { + *(buffer++) = *iter + ONE_BYTE_ADJUST; + } + else if (*iter <= TWO_BYTE_LIMIT) { + PRUnichar c = PRUnichar(*iter) + TWO_BYTE_ADJUST + 0x8000; + *(buffer++) = (char)(c >> 8); + *(buffer++) = (char)(c & 0xFF); + } + else { + PRUint32 c = (PRUint32(*iter) << THREE_BYTE_SHIFT) | 0x00C00000; + *(buffer++) = (char)(c >> 16); + *(buffer++) = (char)(c >> 8); + *(buffer++) = (char)c; + } + } + + // Write end marker + *(buffer++) = eTerminator; + + NS_ASSERTION(buffer == mBuffer.EndReading(), "Wrote wrong number of bytes"); +} + +// static +void +Key::DecodeString(const unsigned char*& aPos, const unsigned char* aEnd, + nsString& aString) +{ + NS_ASSERTION(*aPos % eMaxType == eString, "Don't call me!"); + + const unsigned char* buffer = aPos + 1; + + // First measure how big the decoded string will be. + PRUint32 size = 0; + const unsigned char* iter; + for (iter = buffer; iter < aEnd && *iter != eTerminator; ++iter) { + if (*iter & 0x80) { + iter += (*iter & 0x40) ? 2 : 1; + } + ++size; + } + + // Set end so that we don't have to check for null termination in the loop + // below + if (iter < aEnd) { + aEnd = iter; + } + + PRUnichar* out; + if (size && !aString.GetMutableData(&out, size)) { + return; + } + + for (iter = buffer; iter < aEnd;) { + if (!(*iter & 0x80)) { + *out = *(iter++) - ONE_BYTE_ADJUST; + } + else if (!(*iter & 0x40)) { + PRUnichar c = (PRUnichar(*(iter++)) << 8); + if (iter < aEnd) { + c |= *(iter++); + } + *out = c - TWO_BYTE_ADJUST - 0x8000; + } + else { + PRUint32 c = PRUint32(*(iter++)) << (16 - THREE_BYTE_SHIFT); + if (iter < aEnd) { + c |= PRUint32(*(iter++)) << (8 - THREE_BYTE_SHIFT); + } + if (iter < aEnd) { + c |= *(iter++) >> THREE_BYTE_SHIFT; + } + *out = (PRUnichar)c; + } + + ++out; + } + + NS_ASSERTION(!size || out == aString.EndReading(), + "Should have written the whole string"); + + aPos = iter + 1; +} + +union Float64Union { + double d; + PRUint64 u; +}; + +void +Key::EncodeNumber(double aFloat, PRUint8 aType) +{ + // Allocate memory for the new size + PRUint32 oldLen = mBuffer.Length(); + char* buffer; + if (!mBuffer.GetMutableData(&buffer, oldLen + 1 + sizeof(double))) { + return; + } + buffer += oldLen; + + *(buffer++) = aType; + + Float64Union pun; + pun.d = aFloat; + PRUint64 number = pun.u & PR_UINT64(0x8000000000000000) ? + -pun.u : + (pun.u | PR_UINT64(0x8000000000000000)); + + *reinterpret_cast(buffer) = NS_SWAP64(number); +} + +// static +double +Key::DecodeNumber(const unsigned char*& aPos, const unsigned char* aEnd) +{ + NS_ASSERTION(*aPos % eMaxType == eFloat || + *aPos % eMaxType == eDate, "Don't call me!"); + + ++aPos; + PRUint64 number = 0; + for (PRInt32 n = 7; n >= 0; --n) { + number <<= 8; + if (aPos < aEnd) { + number |= *(aPos++); + } + else { + number <<= 8 * n; + break; + } + } + + Float64Union pun; + pun.u = number & PR_UINT64(0x8000000000000000) ? + (number & ~PR_UINT64(0x8000000000000000)) : + -number; + + return pun.d; +} diff --git a/dom/indexedDB/Key.h b/dom/indexedDB/Key.h index 614f67d700ed..40756d0b14d0 100644 --- a/dom/indexedDB/Key.h +++ b/dom/indexedDB/Key.h @@ -43,10 +43,6 @@ #include "mozilla/dom/indexedDB/IndexedDatabase.h" #include "mozIStorageStatement.h" -#include "nsJSUtils.h" - -#include "xpcprivate.h" -#include "XPCQuickStubs.h" BEGIN_INDEXEDDB_NAMESPACE @@ -72,179 +68,139 @@ public: bool operator==(const Key& aOther) const { - NS_ASSERTION(mType != KEYTYPE_VOID && aOther.mType != KEYTYPE_VOID, + NS_ASSERTION(!mBuffer.IsVoid() && !aOther.mBuffer.IsVoid(), "Don't compare unset keys!"); - if (mType == aOther.mType) { - switch (mType) { - case KEYTYPE_STRING: - return ToString() == aOther.ToString(); - - case KEYTYPE_INTEGER: - return ToInteger() == aOther.ToInteger(); - - default: - NS_NOTREACHED("Unknown type!"); - } - } - return false; + return mBuffer.Equals(aOther.mBuffer); } bool operator!=(const Key& aOther) const { - return !(*this == aOther); + NS_ASSERTION(!mBuffer.IsVoid() && !aOther.mBuffer.IsVoid(), + "Don't compare unset keys!"); + + return !mBuffer.Equals(aOther.mBuffer); } bool operator<(const Key& aOther) const { - NS_ASSERTION(mType != KEYTYPE_VOID && aOther.mType != KEYTYPE_VOID, + NS_ASSERTION(!mBuffer.IsVoid() && !aOther.mBuffer.IsVoid(), "Don't compare unset keys!"); - switch (mType) { - case KEYTYPE_STRING: { - if (aOther.mType == KEYTYPE_INTEGER) { - return false; - } - NS_ASSERTION(aOther.mType == KEYTYPE_STRING, "Unknown type!"); - return ToString() < aOther.ToString(); - } - - case KEYTYPE_INTEGER: - if (aOther.mType == KEYTYPE_STRING) { - return true; - } - NS_ASSERTION(aOther.mType == KEYTYPE_INTEGER, "Unknown type!"); - return ToInteger() < aOther.ToInteger(); - - default: - NS_NOTREACHED("Unknown type!"); - } - return false; + return Compare(mBuffer, aOther.mBuffer) < 0; } bool operator>(const Key& aOther) const { - return !(*this == aOther || *this < aOther); + NS_ASSERTION(!mBuffer.IsVoid() && !aOther.mBuffer.IsVoid(), + "Don't compare unset keys!"); + + return Compare(mBuffer, aOther.mBuffer) > 0; } bool operator<=(const Key& aOther) const { - return (*this == aOther || *this < aOther); + NS_ASSERTION(!mBuffer.IsVoid() && !aOther.mBuffer.IsVoid(), + "Don't compare unset keys!"); + + return Compare(mBuffer, aOther.mBuffer) <= 0; } bool operator>=(const Key& aOther) const { - return (*this == aOther || !(*this < aOther)); + NS_ASSERTION(!mBuffer.IsVoid() && !aOther.mBuffer.IsVoid(), + "Don't compare unset keys!"); + + return Compare(mBuffer, aOther.mBuffer) >= 0; } void Unset() { - mType = KEYTYPE_VOID; - mStringKey.SetIsVoid(true); - mIntKey = 0; + mBuffer.SetIsVoid(true); } - bool IsUnset() const { return mType == KEYTYPE_VOID; } - bool IsString() const { return mType == KEYTYPE_STRING; } - bool IsInteger() const { return mType == KEYTYPE_INTEGER; } - - nsresult SetFromString(const nsAString& aString) + bool IsUnset() const { - mType = KEYTYPE_STRING; - mStringKey = aString; - mIntKey = 0; - return NS_OK; + return mBuffer.IsVoid(); } - nsresult SetFromInteger(PRInt64 aInt) + bool IsFloat() const { - mType = KEYTYPE_INTEGER; - mStringKey.SetIsVoid(true); - mIntKey = aInt; - return NS_OK; + return !mBuffer.IsVoid() && mBuffer.First() == eFloat; + } + + double ToFloat() const + { + NS_ASSERTION(IsFloat(), "Why'd you call this?"); + const unsigned char* pos = BufferStart(); + double res = DecodeNumber(pos, BufferEnd()); + NS_ASSERTION(pos >= BufferEnd(), "Should consume whole buffer"); + return res; + } + + void SetFromString(const nsAString& aString) + { + mBuffer.Truncate(); + EncodeString(aString, 0); + TrimBuffer(); + } + + void SetFromInteger(PRInt64 aInt) + { + mBuffer.Truncate(); + EncodeNumber(double(aInt), eFloat); + TrimBuffer(); } nsresult SetFromJSVal(JSContext* aCx, - jsval aVal) + const jsval aVal) { - if (JSVAL_IS_STRING(aVal)) { - nsDependentJSString str; - if (!str.init(aCx, aVal)) { - return NS_ERROR_OUT_OF_MEMORY; - } - return SetFromString(str); - } - - if (JSVAL_IS_INT(aVal)) { - SetFromInteger(JSVAL_TO_INT(aVal)); - return NS_OK; - } - - if (JSVAL_IS_DOUBLE(aVal)) { - jsdouble doubleActual = JSVAL_TO_DOUBLE(aVal); - int64 doubleAsInt = static_cast(doubleActual); - if (doubleActual == doubleAsInt) { - SetFromInteger(doubleAsInt); - return NS_OK; - } - } + mBuffer.Truncate(); if (JSVAL_IS_NULL(aVal) || JSVAL_IS_VOID(aVal)) { Unset(); return NS_OK; } - return NS_ERROR_DOM_INDEXEDDB_DATA_ERR; + nsresult rv = EncodeJSVal(aCx, aVal, 0); + if (NS_FAILED(rv)) { + Unset(); + return rv; + } + TrimBuffer(); + + return NS_OK; } nsresult ToJSVal(JSContext* aCx, jsval* aVal) const { - if (IsString()) { - nsString key = ToString(); - if (!xpc_qsStringToJsval(aCx, key, aVal)) { - return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; - } - } - else if (IsInteger()) { - if (!JS_NewNumberValue(aCx, static_cast(ToInteger()), aVal)) { - return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; - } - } - else if (IsUnset()) { + if (IsUnset()) { *aVal = JSVAL_VOID; + return NS_OK; } - else { - NS_NOTREACHED("Unknown key type!"); - } + + const unsigned char* pos = BufferStart(); + nsresult rv = DecodeJSVal(pos, BufferEnd(), aCx, 0, aVal); + NS_ENSURE_SUCCESS(rv, rv); + + NS_ASSERTION(pos >= BufferEnd(), + "Didn't consume whole buffer"); + return NS_OK; } - PRInt64 ToInteger() const + const nsCString& GetBuffer() const { - NS_ASSERTION(IsInteger(), "Don't call me!"); - return mIntKey; - } - - const nsString& ToString() const - { - NS_ASSERTION(IsString(), "Don't call me!"); - return mStringKey; + return mBuffer; } nsresult BindToStatement(mozIStorageStatement* aStatement, const nsACString& aParamName) const { - nsresult rv; - - if (IsString()) { - rv = aStatement->BindStringByName(aParamName, ToString()); - } - else { - NS_ASSERTION(IsInteger(), "Bad key!"); - rv = aStatement->BindInt64ByName(aParamName, ToInteger()); - } + nsresult rv = aStatement->BindBlobByName(aParamName, + reinterpret_cast(mBuffer.get()), mBuffer.Length()); return NS_SUCCEEDED(rv) ? NS_OK : NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; } @@ -252,60 +208,83 @@ public: nsresult SetFromStatement(mozIStorageStatement* aStatement, PRUint32 aIndex) { - PRInt32 columnType; - nsresult rv = aStatement->GetTypeOfIndex(aIndex, &columnType); + PRUint8* data; + PRUint32 dataLength = 0; + + nsresult rv = aStatement->GetBlob(aIndex, &dataLength, &data); NS_ENSURE_SUCCESS(rv, NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR); - NS_ASSERTION(columnType == mozIStorageStatement::VALUE_TYPE_INTEGER || - columnType == mozIStorageStatement::VALUE_TYPE_TEXT, - "Unsupported column type!"); + mBuffer.Adopt( + reinterpret_cast(const_cast(data)), dataLength); - return SetFromStatement(aStatement, aIndex, columnType); - } - - nsresult SetFromStatement(mozIStorageStatement* aStatement, - PRUint32 aIndex, - PRInt32 aColumnType) - { - if (aColumnType == mozIStorageStatement::VALUE_TYPE_INTEGER) { - return SetFromInteger(aStatement->AsInt64(aIndex)); - } - - if (aColumnType == mozIStorageStatement::VALUE_TYPE_TEXT) { - nsString keyString; - nsresult rv = aStatement->GetString(aIndex, keyString); - NS_ENSURE_SUCCESS(rv, NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR); - - return SetFromString(keyString); - } - - NS_NOTREACHED("Unsupported column type!"); - return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; + return NS_OK; } static - bool CanBeConstructedFromJSVal(jsval aVal) + PRInt16 CompareKeys(Key& aFirst, Key& aSecond) { - return JSVAL_IS_INT(aVal) || JSVAL_IS_DOUBLE(aVal) || JSVAL_IS_STRING(aVal); + PRInt32 result = Compare(aFirst.mBuffer, aSecond.mBuffer); + + if (result < 0) { + return -1; + } + + if (result > 0) { + return 1; + } + + return 0; } private: - // Wish we could use JSType here but we will end up supporting types like Date - // which JSType can't really identify. Rolling our own for now. - enum Type { - KEYTYPE_VOID, - KEYTYPE_STRING, - KEYTYPE_INTEGER + const unsigned char* BufferStart() const + { + return reinterpret_cast(mBuffer.BeginReading()); + } + + const unsigned char* BufferEnd() const + { + return reinterpret_cast(mBuffer.EndReading()); + } + + enum { + eTerminator = 0, + eFloat = 1, + eDate = 2, + eString = 3, + eArray = 4, + eMaxType = eArray }; - // Type of value in mJSVal. - Type mType; + // Encoding helper. Trims trailing zeros off of mBuffer as a post-processing + // step. + void TrimBuffer() + { + const char* end = mBuffer.EndReading() - 1; + while (!*end) { + --end; + } - // The string if mType is KEYTYPE_STRING, otherwise a void string. - nsString mStringKey; + mBuffer.Truncate(end + 1 - mBuffer.BeginReading()); + } - // The integer value if mType is KEYTYPE_INTEGER, otherwise 0. - int64 mIntKey; + // Encoding functions. These append the encoded value to the end of mBuffer + nsresult EncodeJSVal(JSContext* aCx, const jsval aVal, PRUint8 aTypeOffset); + void EncodeString(const nsAString& aString, PRUint8 aTypeOffset); + void EncodeNumber(double aFloat, PRUint8 aType); + + // Decoding functions. aPos points into mBuffer and is adjusted to point + // past the consumed value. + static nsresult DecodeJSVal(const unsigned char*& aPos, + const unsigned char* aEnd, JSContext* aCx, + PRUint8 aTypeOffset, jsval* aVal); + static void DecodeString(const unsigned char*& aPos, + const unsigned char* aEnd, + nsString& aString); + static double DecodeNumber(const unsigned char*& aPos, + const unsigned char* aEnd); + + nsCString mBuffer; }; END_INDEXEDDB_NAMESPACE diff --git a/dom/indexedDB/Makefile.in b/dom/indexedDB/Makefile.in index 5744598364c4..027d65980091 100644 --- a/dom/indexedDB/Makefile.in +++ b/dom/indexedDB/Makefile.in @@ -71,6 +71,7 @@ CPPSRCS = \ LazyIdleThread.cpp \ OpenDatabaseHelper.cpp \ TransactionThreadPool.cpp \ + Key.cpp \ $(NULL) EXPORTS_mozilla/dom/indexedDB = \ diff --git a/dom/indexedDB/OpenDatabaseHelper.cpp b/dom/indexedDB/OpenDatabaseHelper.cpp index 0f17c583609e..6be6786b80e4 100644 --- a/dom/indexedDB/OpenDatabaseHelper.cpp +++ b/dom/indexedDB/OpenDatabaseHelper.cpp @@ -60,7 +60,7 @@ namespace { PR_STATIC_ASSERT(JS_STRUCTURED_CLONE_VERSION == 1); // Major schema version. Bump for almost everything. -const PRUint32 kMajorSchemaVersion = 11; +const PRUint32 kMajorSchemaVersion = 12; // Minor schema version. Should almost always be 0 (maybe bump on release // branches if we have to). @@ -215,7 +215,7 @@ CreateTables(mozIStorageConnection* aDBConn) "CREATE TABLE object_data (" "id INTEGER PRIMARY KEY, " "object_store_id INTEGER NOT NULL, " - "key_value DEFAULT NULL, " + "key_value BLOB DEFAULT NULL, " "data BLOB NOT NULL, " "file_ids TEXT, " "UNIQUE (object_store_id, key_value), " @@ -245,8 +245,8 @@ CreateTables(mozIStorageConnection* aDBConn) rv = aDBConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "CREATE TABLE index_data (" "index_id INTEGER NOT NULL, " - "value NOT NULL, " - "object_data_key NOT NULL, " + "value BLOB NOT NULL, " + "object_data_key BLOB NOT NULL, " "object_data_id INTEGER NOT NULL, " "PRIMARY KEY (index_id, value, object_data_key), " "FOREIGN KEY (index_id) REFERENCES object_store_index(id) ON DELETE " @@ -268,8 +268,8 @@ CreateTables(mozIStorageConnection* aDBConn) rv = aDBConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "CREATE TABLE unique_index_data (" "index_id INTEGER NOT NULL, " - "value NOT NULL, " - "object_data_key NOT NULL, " // NONE affinity + "value BLOB NOT NULL, " + "object_data_key BLOB NOT NULL, " "object_data_id INTEGER NOT NULL, " "PRIMARY KEY (index_id, value, object_data_key), " "UNIQUE (index_id, value), " @@ -1078,6 +1078,269 @@ UpgradeSchemaFrom10_0To11_0(mozIStorageConnection* aConnection) return NS_OK; } +class EncodeKeysFunction : public mozIStorageFunction +{ +public: + NS_DECL_ISUPPORTS + + NS_IMETHOD + OnFunctionCall(mozIStorageValueArray* aArguments, + nsIVariant** aResult) + { + PRUint32 argc; + nsresult rv = aArguments->GetNumEntries(&argc); + NS_ENSURE_SUCCESS(rv, rv); + + if (argc != 1) { + NS_WARNING("Don't call me with the wrong number of arguments!"); + return NS_ERROR_UNEXPECTED; + } + + PRInt32 type; + rv = aArguments->GetTypeOfIndex(0, &type); + NS_ENSURE_SUCCESS(rv, rv); + + Key key; + if (type == mozIStorageStatement::VALUE_TYPE_INTEGER) { + PRInt64 intKey; + aArguments->GetInt64(0, &intKey); + key.SetFromInteger(intKey); + } + else if (type == mozIStorageStatement::VALUE_TYPE_TEXT) { + nsString stringKey; + aArguments->GetString(0, stringKey); + key.SetFromString(stringKey); + } + else { + NS_WARNING("Don't call me with the wrong type of arguments!"); + return NS_ERROR_UNEXPECTED; + } + + const nsCString& buffer = key.GetBuffer(); + + std::pair data(static_cast(buffer.get()), + int(buffer.Length())); + + nsCOMPtr result = new mozilla::storage::BlobVariant(data); + + result.forget(aResult); + return NS_OK; + } +}; + +NS_IMPL_ISUPPORTS1(EncodeKeysFunction, mozIStorageFunction) + +nsresult +UpgradeSchemaFrom11_0To12_0(mozIStorageConnection* aConnection) +{ + NS_NAMED_LITERAL_CSTRING(encoderName, "encode"); + + nsCOMPtr encoder = new EncodeKeysFunction(); + + nsresult rv = aConnection->CreateFunction(encoderName, 1, encoder); + NS_ENSURE_SUCCESS(rv, rv); + + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "CREATE TEMPORARY TABLE temp_upgrade (" + "id INTEGER PRIMARY KEY, " + "object_store_id, " + "key_value, " + "data, " + "file_ids " + ");" + )); + NS_ENSURE_SUCCESS(rv, rv); + + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "INSERT INTO temp_upgrade " + "SELECT id, object_store_id, encode(key_value), data, file_ids " + "FROM object_data;" + )); + NS_ENSURE_SUCCESS(rv, rv); + + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "DROP TABLE object_data;" + )); + NS_ENSURE_SUCCESS(rv, rv); + + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "CREATE TABLE object_data (" + "id INTEGER PRIMARY KEY, " + "object_store_id INTEGER NOT NULL, " + "key_value BLOB DEFAULT NULL, " + "data BLOB NOT NULL, " + "file_ids TEXT, " + "UNIQUE (object_store_id, key_value), " + "FOREIGN KEY (object_store_id) REFERENCES object_store(id) ON DELETE " + "CASCADE" + ");" + )); + NS_ENSURE_SUCCESS(rv, rv); + + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "INSERT INTO object_data " + "SELECT id, object_store_id, key_value, data, file_ids " + "FROM temp_upgrade;" + )); + NS_ENSURE_SUCCESS(rv, rv); + + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "DROP TABLE temp_upgrade;" + )); + NS_ENSURE_SUCCESS(rv, rv); + + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "CREATE TRIGGER object_data_insert_trigger " + "AFTER INSERT ON object_data " + "FOR EACH ROW " + "WHEN NEW.file_ids IS NOT NULL " + "BEGIN " + "SELECT update_refcount(NULL, NEW.file_ids); " + "END;" + )); + NS_ENSURE_SUCCESS(rv, rv); + + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "CREATE TRIGGER object_data_update_trigger " + "AFTER UPDATE OF file_ids ON object_data " + "FOR EACH ROW " + "WHEN OLD.file_ids IS NOT NULL OR NEW.file_ids IS NOT NULL " + "BEGIN " + "SELECT update_refcount(OLD.file_ids, NEW.file_ids); " + "END;" + )); + NS_ENSURE_SUCCESS(rv, rv); + + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "CREATE TRIGGER object_data_delete_trigger " + "AFTER DELETE ON object_data " + "FOR EACH ROW WHEN OLD.file_ids IS NOT NULL " + "BEGIN " + "SELECT update_refcount(OLD.file_ids, NULL); " + "END;" + )); + NS_ENSURE_SUCCESS(rv, rv); + + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "CREATE TEMPORARY TABLE temp_upgrade (" + "index_id, " + "value, " + "object_data_key, " + "object_data_id " + ");" + )); + NS_ENSURE_SUCCESS(rv, rv); + + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "INSERT INTO temp_upgrade " + "SELECT index_id, encode(value), encode(object_data_key), object_data_id " + "FROM index_data;" + )); + NS_ENSURE_SUCCESS(rv, rv); + + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "DROP TABLE index_data;" + )); + NS_ENSURE_SUCCESS(rv, rv); + + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "CREATE TABLE index_data (" + "index_id INTEGER NOT NULL, " + "value BLOB NOT NULL, " + "object_data_key BLOB NOT NULL, " + "object_data_id INTEGER NOT NULL, " + "PRIMARY KEY (index_id, value, object_data_key), " + "FOREIGN KEY (index_id) REFERENCES object_store_index(id) ON DELETE " + "CASCADE, " + "FOREIGN KEY (object_data_id) REFERENCES object_data(id) ON DELETE " + "CASCADE" + ");" + )); + NS_ENSURE_SUCCESS(rv, rv); + + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "INSERT OR IGNORE INTO index_data " + "SELECT index_id, value, object_data_key, object_data_id " + "FROM temp_upgrade;" + )); + NS_ENSURE_SUCCESS(rv, rv); + + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "DROP TABLE temp_upgrade;" + )); + NS_ENSURE_SUCCESS(rv, rv); + + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "CREATE INDEX index_data_object_data_id_index " + "ON index_data (object_data_id);" + )); + NS_ENSURE_SUCCESS(rv, rv); + + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "CREATE TEMPORARY TABLE temp_upgrade (" + "index_id, " + "value, " + "object_data_key, " + "object_data_id " + ");" + )); + NS_ENSURE_SUCCESS(rv, rv); + + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "INSERT INTO temp_upgrade " + "SELECT index_id, encode(value), encode(object_data_key), object_data_id " + "FROM unique_index_data;" + )); + NS_ENSURE_SUCCESS(rv, rv); + + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "DROP TABLE unique_index_data;" + )); + NS_ENSURE_SUCCESS(rv, rv); + + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "CREATE TABLE unique_index_data (" + "index_id INTEGER NOT NULL, " + "value BLOB NOT NULL, " + "object_data_key BLOB NOT NULL, " + "object_data_id INTEGER NOT NULL, " + "PRIMARY KEY (index_id, value, object_data_key), " + "UNIQUE (index_id, value), " + "FOREIGN KEY (index_id) REFERENCES object_store_index(id) ON DELETE " + "CASCADE " + "FOREIGN KEY (object_data_id) REFERENCES object_data(id) ON DELETE " + "CASCADE" + ");" + )); + NS_ENSURE_SUCCESS(rv, rv); + + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "INSERT INTO unique_index_data " + "SELECT index_id, value, object_data_key, object_data_id " + "FROM temp_upgrade;" + )); + NS_ENSURE_SUCCESS(rv, rv); + + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "DROP TABLE temp_upgrade;" + )); + NS_ENSURE_SUCCESS(rv, rv); + + rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "CREATE INDEX unique_index_data_object_data_id_index " + "ON unique_index_data (object_data_id);" + )); + NS_ENSURE_SUCCESS(rv, rv); + + rv = aConnection->RemoveFunction(encoderName); + NS_ENSURE_SUCCESS(rv, rv); + + rv = aConnection->SetSchemaVersion(MakeSchemaVersion(12, 0)); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + class VersionChangeEventsRunnable; class SetVersionHelper : public AsyncConnectionHelper, @@ -1529,7 +1792,7 @@ OpenDatabaseHelper::CreateDatabaseConnection( } else { // This logic needs to change next time we change the schema! - PR_STATIC_ASSERT(kSQLiteSchemaVersion == PRInt32((11 << 4) + 0)); + PR_STATIC_ASSERT(kSQLiteSchemaVersion == PRInt32((12 << 4) + 0)); while (schemaVersion != kSQLiteSchemaVersion) { if (schemaVersion == 4) { @@ -1554,6 +1817,9 @@ OpenDatabaseHelper::CreateDatabaseConnection( else if (schemaVersion == MakeSchemaVersion(10, 0)) { rv = UpgradeSchemaFrom10_0To11_0(connection); } + else if (schemaVersion == MakeSchemaVersion(11, 0)) { + rv = UpgradeSchemaFrom11_0To12_0(connection); + } else { NS_WARNING("Unable to open IndexedDB database, no upgrade path is " "available!"); diff --git a/dom/indexedDB/test/Makefile.in b/dom/indexedDB/test/Makefile.in index c7e85250d28d..d7af6b77deab 100644 --- a/dom/indexedDB/test/Makefile.in +++ b/dom/indexedDB/test/Makefile.in @@ -60,7 +60,6 @@ TEST_FILES = \ test_autoIncrement.html \ test_bfcache.html \ test_clear.html \ - test_cmp.html \ test_complex_keyPaths.html \ test_count.html \ test_create_index.html \ @@ -94,6 +93,7 @@ TEST_FILES = \ test_indexes.html \ test_indexes_bad_values.html \ test_key_requirements.html \ + test_keys.html \ test_leaving_page.html \ test_multientry.html \ test_objectCursors.html \ diff --git a/dom/indexedDB/test/test_autoIncrement.html b/dom/indexedDB/test/test_autoIncrement.html index ea2c890c1bec..5dd04286fff8 100644 --- a/dom/indexedDB/test/test_autoIncrement.html +++ b/dom/indexedDB/test/test_autoIncrement.html @@ -12,7 +12,8 @@ - - - - - - - - - - diff --git a/dom/indexedDB/test/test_keys.html b/dom/indexedDB/test/test_keys.html new file mode 100644 index 000000000000..4c359c2e1031 --- /dev/null +++ b/dom/indexedDB/test/test_keys.html @@ -0,0 +1,305 @@ + + + + Indexed Database Property Test + + + + + + + + + + +