зеркало из https://github.com/mozilla/gecko-dev.git
Bug 692614: Support all spec'ed key-types, including Arrays. Patch by Jan Varga and me. r=janv/bent/me
This commit is contained in:
Родитель
15504d8795
Коммит
09d6e71f40
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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<PRUint64>(mKey.ToInteger()));
|
||||
pun.d = SwapBytes(static_cast<PRUint64>(autoIncrementNum));
|
||||
|
||||
JSAutoStructuredCloneBuffer& buffer = mCloneWriteInfo.mCloneBuffer;
|
||||
PRUint64 offsetToKeyProp = mCloneWriteInfo.mOffsetToKeyProp;
|
||||
|
|
|
@ -56,6 +56,7 @@
|
|||
#include "nsXPCOM.h"
|
||||
#include "nsXPCOMPrivate.h"
|
||||
#include "test_quota.h"
|
||||
#include "xpcprivate.h"
|
||||
|
||||
#include "AsyncConnectionHelper.h"
|
||||
#include "CheckQuotaHelper.h"
|
||||
|
|
|
@ -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 <bent.mozilla@gmail.com>
|
||||
*
|
||||
* 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<jsdouble>(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<PRUint64*>(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;
|
||||
}
|
|
@ -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<int64>(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<jsdouble>(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<const PRUint8*>(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<char*>(const_cast<PRUint8*>(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<const unsigned char*>(mBuffer.BeginReading());
|
||||
}
|
||||
|
||||
const unsigned char* BufferEnd() const
|
||||
{
|
||||
return reinterpret_cast<const unsigned char*>(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
|
||||
|
|
|
@ -71,6 +71,7 @@ CPPSRCS = \
|
|||
LazyIdleThread.cpp \
|
||||
OpenDatabaseHelper.cpp \
|
||||
TransactionThreadPool.cpp \
|
||||
Key.cpp \
|
||||
$(NULL)
|
||||
|
||||
EXPORTS_mozilla/dom/indexedDB = \
|
||||
|
|
|
@ -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<const void *, int> data(static_cast<const void*>(buffer.get()),
|
||||
int(buffer.Length()));
|
||||
|
||||
nsCOMPtr<nsIVariant> 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<mozIStorageFunction> 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!");
|
||||
|
|
|
@ -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 \
|
||||
|
|
|
@ -12,7 +12,8 @@
|
|||
<script type="text/javascript;version=1.7">
|
||||
function genCheck(key, value, test, options) {
|
||||
return function(event) {
|
||||
is(event.target.result, key, "correct returned key in " + test);
|
||||
is(JSON.stringify(event.target.result), JSON.stringify(key),
|
||||
"correct returned key in " + test);
|
||||
if (options && options.store) {
|
||||
is(event.target.source, options.store, "correct store in " + test);
|
||||
}
|
||||
|
@ -120,6 +121,14 @@
|
|||
genCheck(c1++, { explicit: 8 }, "eighth" + test);
|
||||
yield;
|
||||
|
||||
trans = db.transaction("store1", RW);
|
||||
trans.objectStore("store1").add({ explicit: 7 }, [100000]).onsuccess =
|
||||
genCheck([100000], { explicit: 7 }, "seventh" + test);
|
||||
yield;
|
||||
trans.objectStore("store1").add({ explicit: 8 }).onsuccess =
|
||||
genCheck(c1++, { explicit: 8 }, "eighth" + test);
|
||||
yield;
|
||||
|
||||
trans = db.transaction("store1", RW);
|
||||
trans.objectStore("store1").add({ explicit: 9 }, -100000).onsuccess =
|
||||
genCheck(-100000, { explicit: 9 }, "ninth" + test);
|
||||
|
@ -166,6 +175,15 @@
|
|||
c2++;
|
||||
yield;
|
||||
|
||||
trans = db.transaction("store2", RW);
|
||||
trans.objectStore("store2").add({ explicit: 7, id: [100000] }).onsuccess =
|
||||
genCheck([100000], { explicit: 7, id: [100000] }, "seventh store2" + test);
|
||||
yield;
|
||||
trans.objectStore("store2").add({ explicit: 8 }).onsuccess =
|
||||
genCheck(c2, { explicit: 8, id: c2 }, "eighth store2" + test);
|
||||
c2++;
|
||||
yield;
|
||||
|
||||
trans = db.transaction("store2", RW);
|
||||
trans.objectStore("store2").add({ explicit: 9, id: -100000 }).onsuccess =
|
||||
genCheck(-100000, { explicit: 9, id: -100000 }, "ninth store2" + test);
|
||||
|
|
|
@ -1,85 +0,0 @@
|
|||
<!--
|
||||
Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/
|
||||
-->
|
||||
<html>
|
||||
<head>
|
||||
<title>Indexed Database Property Test</title>
|
||||
|
||||
<script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
|
||||
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
|
||||
|
||||
<script type="text/javascript;version=1.7">
|
||||
function testSteps()
|
||||
{
|
||||
function compare(key1, key2, expected, exception) {
|
||||
function maybeQuote(key) {
|
||||
return typeof(key) == "string" ? "\"" + key + "\"" : key;
|
||||
}
|
||||
|
||||
var cmp = "cmp(" + maybeQuote(key1) + ", " + maybeQuote(key2) + ")";
|
||||
|
||||
if (exception) {
|
||||
var caught;
|
||||
try {
|
||||
var result = mozIndexedDB.cmp(key1, key2);
|
||||
}
|
||||
catch(e) {
|
||||
caught = e;
|
||||
}
|
||||
ok(caught, "Got an exception for " + cmp);
|
||||
is(caught instanceof IDBDatabaseException, true,
|
||||
"Got IDBDatabaseException for " + cmp);
|
||||
is(caught.code, IDBDatabaseException.DATA_ERR,
|
||||
"Got correct exception code for " + cmp);
|
||||
}
|
||||
else {
|
||||
is(mozIndexedDB.cmp(key1, key2), expected,
|
||||
"Correct result for " + cmp);
|
||||
}
|
||||
}
|
||||
|
||||
compare(NaN, 0, 0, true);
|
||||
compare(0, NaN, 0, true);
|
||||
compare(undefined, 0, 0, true);
|
||||
compare(0, undefined, 0, true);
|
||||
compare(null, 0, 0, true);
|
||||
compare(0, null, 0, true);
|
||||
|
||||
compare(0, 0, 0);
|
||||
compare(1, 0, 1);
|
||||
compare(0, 1, -1);
|
||||
compare(1, 1, 0);
|
||||
compare(2, 1, 1);
|
||||
compare(1, 2, -1);
|
||||
compare(-1, -1, 0);
|
||||
compare(0, -1, 1);
|
||||
compare(-1, 0, -1);
|
||||
|
||||
compare("", "", 0);
|
||||
compare("a", "", 1);
|
||||
compare("", "a", -1);
|
||||
compare("a", "a", 0);
|
||||
compare("a", "b", -1);
|
||||
compare("b", "a", 1);
|
||||
compare("a", "aa", -1);
|
||||
compare("aa", "a", 1);
|
||||
|
||||
compare(0, "", -1);
|
||||
compare("", 0, 1);
|
||||
compare(0, "a", -1);
|
||||
compare("a", 0, 1);
|
||||
compare(99999, "", -1);
|
||||
compare("", 99999, 1);
|
||||
|
||||
finishTest();
|
||||
yield;
|
||||
}
|
||||
</script>
|
||||
<script type="text/javascript;version=1.7" src="helpers.js"></script>
|
||||
|
||||
</head>
|
||||
|
||||
<body onload="runTest();"></body>
|
||||
|
||||
</html>
|
|
@ -0,0 +1,305 @@
|
|||
<!--
|
||||
Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/
|
||||
-->
|
||||
<html>
|
||||
<head>
|
||||
<title>Indexed Database Property Test</title>
|
||||
|
||||
<script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
|
||||
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
|
||||
|
||||
<script type="text/javascript;version=1.7">
|
||||
|
||||
function compareKeys(k1, k2) {
|
||||
let t = typeof k1;
|
||||
if (t != typeof k2)
|
||||
return false;
|
||||
|
||||
if (t !== "object")
|
||||
return k1 === k2;
|
||||
|
||||
if (k1 instanceof Date) {
|
||||
return (k2 instanceof Date) &&
|
||||
k1.getTime() === k2.getTime();
|
||||
}
|
||||
|
||||
if (k1 instanceof Array) {
|
||||
if (!(k2 instanceof Array) ||
|
||||
k1.length != k2.length)
|
||||
return false;
|
||||
|
||||
for (let i = 0; i < k1.length; ++i) {
|
||||
if (!compareKeys(k1[i], k2[i]))
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function testSteps()
|
||||
{
|
||||
const dbname = window.location.pathname;
|
||||
const RW = IDBTransaction.READ_WRITE
|
||||
let c1 = 1;
|
||||
let c2 = 1;
|
||||
|
||||
let openRequest = mozIndexedDB.open(dbname, 1);
|
||||
openRequest.onerror = errorHandler;
|
||||
openRequest.onupgradeneeded = grabEventAndContinueHandler;
|
||||
openRequest.onsuccess = unexpectedSuccessHandler;
|
||||
let event = yield;
|
||||
let db = event.target.result;
|
||||
let trans = event.target.transaction;
|
||||
|
||||
// Create test stores
|
||||
let store = db.createObjectStore("store");
|
||||
|
||||
// Test simple inserts
|
||||
var keys = [
|
||||
-1/0,
|
||||
-1.7e308,
|
||||
-10000,
|
||||
-2,
|
||||
-1.5,
|
||||
-1,
|
||||
-1.00001e-200,
|
||||
-1e-200,
|
||||
0,
|
||||
1e-200,
|
||||
1.00001e-200,
|
||||
1,
|
||||
2,
|
||||
10000,
|
||||
1.7e308,
|
||||
1/0,
|
||||
new Date("1750-01-02"),
|
||||
new Date("1800-12-31T12:34:56.001"),
|
||||
new Date(-1000),
|
||||
new Date(-10),
|
||||
new Date(-1),
|
||||
new Date(0),
|
||||
new Date(1),
|
||||
new Date(2),
|
||||
new Date(1000),
|
||||
new Date("1971-01-01"),
|
||||
new Date("1971-01-01T01:01:01"),
|
||||
new Date("1971-01-01T01:01:01.001"),
|
||||
new Date("1971-01-01T01:01:01.01"),
|
||||
new Date("1971-01-01T01:01:01.1"),
|
||||
new Date("1980-02-02"),
|
||||
new Date("3333-03-19T03:33:33.333"),
|
||||
"",
|
||||
"\x00",
|
||||
"\x00\x00",
|
||||
"\x00\x01",
|
||||
"\x01",
|
||||
"\x02",
|
||||
"\x03",
|
||||
"\x04",
|
||||
"\x07",
|
||||
"\x08",
|
||||
"\x0F",
|
||||
"\x10",
|
||||
"\x1F",
|
||||
"\x20",
|
||||
"01234",
|
||||
"\x3F",
|
||||
"\x40",
|
||||
"A",
|
||||
"A\x00",
|
||||
"A1",
|
||||
"ZZZZ",
|
||||
"a",
|
||||
"a\x00",
|
||||
"aa",
|
||||
"azz",
|
||||
"}",
|
||||
"\x7E",
|
||||
"\x7F",
|
||||
"\x80",
|
||||
"\xFF",
|
||||
"\u0100",
|
||||
"\u01FF",
|
||||
"\u0200",
|
||||
"\u03FF",
|
||||
"\u0400",
|
||||
"\u07FF",
|
||||
"\u0800",
|
||||
"\u0FFF",
|
||||
"\u1000",
|
||||
"\u1FFF",
|
||||
"\u2000",
|
||||
"\u3FFF",
|
||||
"\u4000",
|
||||
"\u7FFF",
|
||||
"\u8000",
|
||||
"\uD800",
|
||||
"\uD800a",
|
||||
"\uD800\uDC01",
|
||||
"\uDBFF",
|
||||
"\uDC00",
|
||||
"\uDFFF\uD800",
|
||||
"\uFFFE",
|
||||
"\uFFFF",
|
||||
"\uFFFF\x00",
|
||||
"\uFFFFZZZ",
|
||||
[],
|
||||
[-1/0],
|
||||
[-1],
|
||||
[0],
|
||||
[1],
|
||||
[1, "a"],
|
||||
[1, []],
|
||||
[1, [""]],
|
||||
[2, 3],
|
||||
[2, 3.0000000000001],
|
||||
[12, [[]]],
|
||||
[12, [[[]]]],
|
||||
[12, [[[""]]]],
|
||||
[12, [[["foo"]]]],
|
||||
[12, [[[[[3]]]]]],
|
||||
[12, [[[[[[3]]]]]]],
|
||||
[new Date(-1)],
|
||||
[new Date(1)],
|
||||
[""],
|
||||
["", [[]]],
|
||||
["", [[[]]]],
|
||||
["abc"],
|
||||
["abc", "def"],
|
||||
["abc\x00"],
|
||||
["abc\x00", "\x00\x01"],
|
||||
["abc\x00", "\x00def"],
|
||||
["abc\x00\x00def"],
|
||||
["x", [[]]],
|
||||
["x", [[[]]]],
|
||||
[[]],
|
||||
[[],"foo"],
|
||||
[[],[]],
|
||||
[[[]]],
|
||||
[[[]], []],
|
||||
[[[]], [[]]],
|
||||
[[[]], [[1]]],
|
||||
[[[]], [[[]]]],
|
||||
[[[1]]],
|
||||
[[[[]], []]],
|
||||
];
|
||||
|
||||
for (var i = 0; i < keys.length; ++i) {
|
||||
let keyI = keys[i];
|
||||
is(mozIndexedDB.cmp(keyI, keyI), 0, i + " compared to self");
|
||||
|
||||
function doCompare(keyI) {
|
||||
for (var j = i-1; j >= i-10 && j >= 0; --j) {
|
||||
is(mozIndexedDB.cmp(keyI, keys[j]), 1, i + " compared to " + j);
|
||||
is(mozIndexedDB.cmp(keys[j], keyI), -1, j + " compared to " + i);
|
||||
}
|
||||
}
|
||||
|
||||
doCompare(keyI);
|
||||
store.add(i, keyI).onsuccess = function(e) {
|
||||
is(mozIndexedDB.cmp(e.target.result, keyI), 0,
|
||||
"Returned key should cmp as equal");
|
||||
ok(compareKeys(e.target.result, keyI),
|
||||
"Returned key should actually be equal");
|
||||
};
|
||||
|
||||
// Test that -0 compares the same as 0
|
||||
if (keyI === 0) {
|
||||
doCompare(-0);
|
||||
let req = store.add(i, -0);
|
||||
req.onerror = new ExpectError(IDBDatabaseException.CONSTRAINT_ERR);
|
||||
req.onsuccess = unexpectedSuccessHandler;
|
||||
yield;
|
||||
}
|
||||
else if (Array.isArray(keyI) && keyI.length === 1 && keyI[0] === 0) {
|
||||
doCompare([-0]);
|
||||
let req = store.add(i, [-0]);
|
||||
req.onerror = new ExpectError(IDBDatabaseException.CONSTRAINT_ERR);
|
||||
req.onsuccess = unexpectedSuccessHandler;
|
||||
yield;
|
||||
}
|
||||
}
|
||||
|
||||
store.openCursor().onsuccess = grabEventAndContinueHandler;
|
||||
for (i = 0; i < keys.length; ++i) {
|
||||
event = yield;
|
||||
let cursor = event.target.result;
|
||||
is(mozIndexedDB.cmp(cursor.key, keys[i]), 0,
|
||||
"Read back key should cmp as equal");
|
||||
ok(compareKeys(cursor.key, keys[i]),
|
||||
"Read back key should actually be equal");
|
||||
is(cursor.value, i, "Stored with right value");
|
||||
|
||||
cursor.continue();
|
||||
}
|
||||
event = yield;
|
||||
is(event.target.result, undefined, "no more results expected");
|
||||
|
||||
var nan = 0/0;
|
||||
var invalidKeys = [
|
||||
nan,
|
||||
undefined,
|
||||
null,
|
||||
/x/,
|
||||
{},
|
||||
[nan],
|
||||
[undefined],
|
||||
[null],
|
||||
[/x/],
|
||||
[{}],
|
||||
[1, nan],
|
||||
[1, undefined],
|
||||
[1, null],
|
||||
[1, /x/],
|
||||
[1, {}],
|
||||
[1, [nan]],
|
||||
[1, [undefined]],
|
||||
[1, [null]],
|
||||
[1, [/x/]],
|
||||
[1, [{}]],
|
||||
];
|
||||
|
||||
for (i = 0; i < invalidKeys.length; ++i) {
|
||||
try {
|
||||
mozIndexedDB.cmp(invalidKeys[i], 1);
|
||||
ok(false, "didn't throw");
|
||||
}
|
||||
catch(ex) {
|
||||
ok(ex instanceof IDBDatabaseException, "Threw IDBDatabaseException");
|
||||
is(ex.code, IDBDatabaseException.DATA_ERR, "Threw right IDBDatabaseException");
|
||||
}
|
||||
try {
|
||||
mozIndexedDB.cmp(1, invalidKeys[i]);
|
||||
ok(false, "didn't throw2");
|
||||
}
|
||||
catch(ex) {
|
||||
ok(ex instanceof IDBDatabaseException, "Threw IDBDatabaseException2");
|
||||
is(ex.code, IDBDatabaseException.DATA_ERR, "Threw right IDBDatabaseException2");
|
||||
}
|
||||
try {
|
||||
store.put(1, invalidKeys[i]);
|
||||
ok(false, "didn't throw3");
|
||||
}
|
||||
catch(ex) {
|
||||
ok(ex instanceof IDBDatabaseException, "Threw IDBDatabaseException3");
|
||||
is(ex.code, IDBDatabaseException.DATA_ERR, "Threw right IDBDatabaseException3");
|
||||
}
|
||||
}
|
||||
|
||||
openRequest.onsuccess = grabEventAndContinueHandler;
|
||||
yield;
|
||||
|
||||
finishTest();
|
||||
yield;
|
||||
}
|
||||
</script>
|
||||
<script type="text/javascript;version=1.7" src="helpers.js"></script>
|
||||
</head>
|
||||
|
||||
<body onload="runTest();"></body>
|
||||
|
||||
</html>
|
Загрузка…
Ссылка в новой задаче