зеркало из https://github.com/mozilla/gecko-dev.git
Bug 692630: Support multi-entry indexes. r=bent
This commit is contained in:
Родитель
f8d6fe9362
Коммит
9a4a46521c
|
@ -1641,6 +1641,7 @@ jsid nsDOMClassInfo::sURL_id = JSID_VOID;
|
|||
jsid nsDOMClassInfo::sKeyPath_id = JSID_VOID;
|
||||
jsid nsDOMClassInfo::sAutoIncrement_id = JSID_VOID;
|
||||
jsid nsDOMClassInfo::sUnique_id = JSID_VOID;
|
||||
jsid nsDOMClassInfo::sMultiEntry_id = JSID_VOID;
|
||||
jsid nsDOMClassInfo::sOnload_id = JSID_VOID;
|
||||
jsid nsDOMClassInfo::sOnerror_id = JSID_VOID;
|
||||
|
||||
|
@ -1904,6 +1905,7 @@ nsDOMClassInfo::DefineStaticJSVals(JSContext *cx)
|
|||
SET_JSID_TO_STRING(sKeyPath_id, cx, "keyPath");
|
||||
SET_JSID_TO_STRING(sAutoIncrement_id, cx, "autoIncrement");
|
||||
SET_JSID_TO_STRING(sUnique_id, cx, "unique");
|
||||
SET_JSID_TO_STRING(sMultiEntry_id, cx, "multiEntry");
|
||||
SET_JSID_TO_STRING(sOnload_id, cx, "onload");
|
||||
SET_JSID_TO_STRING(sOnerror_id, cx, "onerror");
|
||||
|
||||
|
@ -4902,6 +4904,7 @@ nsDOMClassInfo::ShutDown()
|
|||
sKeyPath_id = JSID_VOID;
|
||||
sAutoIncrement_id = JSID_VOID;
|
||||
sUnique_id = JSID_VOID;
|
||||
sMultiEntry_id = JSID_VOID;
|
||||
sOnload_id = JSID_VOID;
|
||||
sOnerror_id = JSID_VOID;
|
||||
|
||||
|
|
|
@ -295,6 +295,7 @@ public:
|
|||
static jsid sKeyPath_id;
|
||||
static jsid sAutoIncrement_id;
|
||||
static jsid sUnique_id;
|
||||
static jsid sMultiEntry_id;
|
||||
static jsid sOnload_id;
|
||||
static jsid sOnerror_id;
|
||||
|
||||
|
|
|
@ -97,7 +97,8 @@ DatabaseInfo::~DatabaseInfo()
|
|||
IndexInfo::IndexInfo()
|
||||
: id(LL_MININT),
|
||||
unique(false),
|
||||
autoIncrement(false)
|
||||
autoIncrement(false),
|
||||
multiEntry(false)
|
||||
{
|
||||
MOZ_COUNT_CTOR(IndexInfo);
|
||||
}
|
||||
|
@ -107,7 +108,8 @@ IndexInfo::IndexInfo(const IndexInfo& aOther)
|
|||
name(aOther.name),
|
||||
keyPath(aOther.keyPath),
|
||||
unique(aOther.unique),
|
||||
autoIncrement(aOther.autoIncrement)
|
||||
autoIncrement(aOther.autoIncrement),
|
||||
multiEntry(aOther.multiEntry)
|
||||
{
|
||||
MOZ_COUNT_CTOR(IndexInfo);
|
||||
}
|
||||
|
|
|
@ -121,6 +121,7 @@ struct IndexInfo
|
|||
nsString keyPath;
|
||||
bool unique;
|
||||
bool autoIncrement;
|
||||
bool multiEntry;
|
||||
};
|
||||
|
||||
struct ObjectStoreInfo
|
||||
|
@ -149,7 +150,8 @@ struct IndexUpdateInfo
|
|||
~IndexUpdateInfo();
|
||||
#endif
|
||||
|
||||
IndexInfo info;
|
||||
PRInt64 indexId;
|
||||
bool indexUnique;
|
||||
Key value;
|
||||
};
|
||||
|
||||
|
|
|
@ -273,7 +273,7 @@ IDBFactory::LoadDatabaseInformation(mozIStorageConnection* aConnection,
|
|||
|
||||
// Load index information
|
||||
rv = aConnection->CreateStatement(NS_LITERAL_CSTRING(
|
||||
"SELECT object_store_id, id, name, key_path, unique_index, "
|
||||
"SELECT object_store_id, id, name, key_path, unique_index, multientry, "
|
||||
"object_store_autoincrement "
|
||||
"FROM object_store_index"
|
||||
), getter_AddRefs(stmt));
|
||||
|
@ -307,7 +307,8 @@ IDBFactory::LoadDatabaseInformation(mozIStorageConnection* aConnection,
|
|||
NS_ENSURE_SUCCESS(rv, rv);
|
||||
|
||||
indexInfo->unique = !!stmt->AsInt32(4);
|
||||
indexInfo->autoIncrement = !!stmt->AsInt32(5);
|
||||
indexInfo->multiEntry = !!stmt->AsInt32(5);
|
||||
indexInfo->autoIncrement = !!stmt->AsInt32(6);
|
||||
}
|
||||
NS_ENSURE_SUCCESS(rv, rv);
|
||||
|
||||
|
|
|
@ -318,6 +318,7 @@ IDBIndex::Create(IDBObjectStore* aObjectStore,
|
|||
index->mName = aIndexInfo->name;
|
||||
index->mKeyPath = aIndexInfo->keyPath;
|
||||
index->mUnique = aIndexInfo->unique;
|
||||
index->mMultiEntry = aIndexInfo->multiEntry;
|
||||
index->mAutoIncrement = aIndexInfo->autoIncrement;
|
||||
|
||||
return index.forget();
|
||||
|
@ -396,6 +397,15 @@ IDBIndex::GetUnique(bool* aUnique)
|
|||
return NS_OK;
|
||||
}
|
||||
|
||||
NS_IMETHODIMP
|
||||
IDBIndex::GetMultiEntry(bool* aMultiEntry)
|
||||
{
|
||||
NS_ASSERTION(NS_IsMainThread(), "Wrong thread!");
|
||||
|
||||
*aMultiEntry = mMultiEntry;
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
NS_IMETHODIMP
|
||||
IDBIndex::GetObjectStore(nsIIDBObjectStore** aObjectStore)
|
||||
{
|
||||
|
|
|
@ -87,6 +87,11 @@ public:
|
|||
return mUnique;
|
||||
}
|
||||
|
||||
bool IsMultiEntry() const
|
||||
{
|
||||
return mMultiEntry;
|
||||
}
|
||||
|
||||
bool IsAutoIncrement() const
|
||||
{
|
||||
return mAutoIncrement;
|
||||
|
@ -110,6 +115,7 @@ private:
|
|||
nsString mName;
|
||||
nsString mKeyPath;
|
||||
bool mUnique;
|
||||
bool mMultiEntry;
|
||||
bool mAutoIncrement;
|
||||
};
|
||||
|
||||
|
|
|
@ -97,9 +97,6 @@ public:
|
|||
AsyncConnectionHelper::ReleaseMainThreadObjects();
|
||||
}
|
||||
|
||||
nsresult UpdateIndexes(mozIStorageConnection* aConnection,
|
||||
PRInt64 aObjectDataId);
|
||||
|
||||
private:
|
||||
// In-params.
|
||||
nsRefPtr<IDBObjectStore> mObjectStore;
|
||||
|
@ -422,10 +419,10 @@ typedef nsCharSeparatedTokenizerTemplate<IgnoreWhitespace> KeyPathTokenizer;
|
|||
|
||||
inline
|
||||
nsresult
|
||||
GetKeyFromValue(JSContext* aCx,
|
||||
GetJSValFromKeyPath(JSContext* aCx,
|
||||
jsval aVal,
|
||||
const nsAString& aKeyPath,
|
||||
Key& aKey)
|
||||
jsval& aKey)
|
||||
{
|
||||
NS_ASSERTION(aCx, "Null pointer!");
|
||||
// aVal can be primitive iff the key path is empty.
|
||||
|
@ -453,7 +450,22 @@ GetKeyFromValue(JSContext* aCx,
|
|||
NS_ENSURE_TRUE(ok, NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR);
|
||||
}
|
||||
|
||||
if (NS_FAILED(aKey.SetFromJSVal(aCx, intermediate))) {
|
||||
aKey = intermediate;
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
inline
|
||||
nsresult
|
||||
GetKeyFromValue(JSContext* aCx,
|
||||
jsval aVal,
|
||||
const nsAString& aKeyPath,
|
||||
Key& aKey)
|
||||
{
|
||||
jsval key;
|
||||
nsresult rv = GetJSValFromKeyPath(aCx, aVal, aKeyPath, key);
|
||||
NS_ENSURE_SUCCESS(rv, rv);
|
||||
|
||||
if (NS_FAILED(aKey.SetFromJSVal(aCx, key))) {
|
||||
aKey.Unset();
|
||||
}
|
||||
|
||||
|
@ -571,78 +583,63 @@ IDBObjectStore::IsValidKeyPath(JSContext* aCx,
|
|||
|
||||
// static
|
||||
nsresult
|
||||
IDBObjectStore::GetKeyPathValueFromStructuredData(const PRUint8* aData,
|
||||
PRUint32 aDataLength,
|
||||
IDBObjectStore::AppendIndexUpdateInfo(PRInt64 aIndexID,
|
||||
const nsAString& aKeyPath,
|
||||
JSContext* aCx,
|
||||
Key& aValue)
|
||||
{
|
||||
NS_ASSERTION(aData, "Null pointer!");
|
||||
NS_ASSERTION(aDataLength, "Empty data!");
|
||||
NS_ASSERTION(aCx, "Null pointer!");
|
||||
|
||||
JSAutoRequest ar(aCx);
|
||||
|
||||
jsval clone;
|
||||
if (!JS_ReadStructuredClone(aCx, reinterpret_cast<const uint64*>(aData),
|
||||
aDataLength, JS_STRUCTURED_CLONE_VERSION,
|
||||
&clone, NULL, NULL)) {
|
||||
return NS_ERROR_DOM_DATA_CLONE_ERR;
|
||||
}
|
||||
|
||||
if (JSVAL_IS_PRIMITIVE(clone) && !aKeyPath.IsEmpty()) {
|
||||
// This isn't an object, so just leave the key unset.
|
||||
aValue.Unset();
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
nsresult rv = GetKeyFromValue(aCx, clone, aKeyPath, aValue);
|
||||
NS_ENSURE_SUCCESS(rv, rv);
|
||||
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
/* static */
|
||||
nsresult
|
||||
IDBObjectStore::GetIndexUpdateInfo(ObjectStoreInfo* aObjectStoreInfo,
|
||||
bool aUnique,
|
||||
bool aMultiEntry,
|
||||
JSContext* aCx,
|
||||
jsval aObject,
|
||||
nsTArray<IndexUpdateInfo>& aUpdateInfoArray)
|
||||
{
|
||||
JSObject* cloneObj = nsnull;
|
||||
|
||||
PRUint32 count = aObjectStoreInfo->indexes.Length();
|
||||
if (count) {
|
||||
if (!aUpdateInfoArray.SetCapacity(count)) {
|
||||
NS_ERROR("Out of memory!");
|
||||
return NS_ERROR_OUT_OF_MEMORY;
|
||||
}
|
||||
|
||||
for (PRUint32 indexesIndex = 0; indexesIndex < count; indexesIndex++) {
|
||||
const IndexInfo& indexInfo = aObjectStoreInfo->indexes[indexesIndex];
|
||||
|
||||
Key value;
|
||||
nsresult rv = GetKeyFromValue(aCx, aObject, indexInfo.keyPath, value);
|
||||
jsval key;
|
||||
nsresult rv = GetJSValFromKeyPath(aCx, aObject, aKeyPath, key);
|
||||
NS_ENSURE_SUCCESS(rv, rv);
|
||||
|
||||
if (value.IsUnset()) {
|
||||
if (aMultiEntry && !JSVAL_IS_PRIMITIVE(key) &&
|
||||
JS_IsArrayObject(aCx, JSVAL_TO_OBJECT(key))) {
|
||||
JSObject* array = JSVAL_TO_OBJECT(key);
|
||||
jsuint arrayLength;
|
||||
if (!JS_GetArrayLength(aCx, array, &arrayLength)) {
|
||||
return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR;
|
||||
}
|
||||
|
||||
for (jsuint arrayIndex = 0; arrayIndex < arrayLength; arrayIndex++) {
|
||||
jsval arrayItem;
|
||||
if (!JS_GetElement(aCx, array, arrayIndex, &arrayItem)) {
|
||||
return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR;
|
||||
}
|
||||
|
||||
Key value;
|
||||
if (NS_FAILED(value.SetFromJSVal(aCx, arrayItem)) ||
|
||||
value.IsUnset()) {
|
||||
// Not a value we can do anything with, ignore it.
|
||||
continue;
|
||||
}
|
||||
|
||||
IndexUpdateInfo* updateInfo = aUpdateInfoArray.AppendElement();
|
||||
updateInfo->info = indexInfo;
|
||||
updateInfo->indexId = aIndexID;
|
||||
updateInfo->indexUnique = aUnique;
|
||||
updateInfo->value = value;
|
||||
}
|
||||
}
|
||||
else {
|
||||
aUpdateInfoArray.Clear();
|
||||
Key value;
|
||||
if (NS_FAILED(value.SetFromJSVal(aCx, key)) ||
|
||||
value.IsUnset()) {
|
||||
// Not a value we can do anything with, ignore it.
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
IndexUpdateInfo* updateInfo = aUpdateInfoArray.AppendElement();
|
||||
updateInfo->indexId = aIndexID;
|
||||
updateInfo->indexUnique = aUnique;
|
||||
updateInfo->value = value;
|
||||
}
|
||||
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
/* static */
|
||||
// static
|
||||
nsresult
|
||||
IDBObjectStore::UpdateIndexes(IDBTransaction* aTransaction,
|
||||
PRInt64 aObjectStoreId,
|
||||
|
@ -652,21 +649,13 @@ IDBObjectStore::UpdateIndexes(IDBTransaction* aTransaction,
|
|||
PRInt64 aObjectDataId,
|
||||
const nsTArray<IndexUpdateInfo>& aUpdateInfoArray)
|
||||
{
|
||||
#ifdef DEBUG
|
||||
if (aAutoIncrement) {
|
||||
NS_ASSERTION(aObjectDataId != LL_MININT, "Bad objectData id!");
|
||||
}
|
||||
else {
|
||||
NS_ASSERTION(aObjectDataId == LL_MININT, "Bad objectData id!");
|
||||
}
|
||||
#endif
|
||||
|
||||
PRUint32 indexCount = aUpdateInfoArray.Length();
|
||||
NS_ASSERTION(!aAutoIncrement || aObjectDataId != LL_MININT,
|
||||
"Bad objectData id!");
|
||||
|
||||
nsCOMPtr<mozIStorageStatement> stmt;
|
||||
nsresult rv;
|
||||
|
||||
if (!aAutoIncrement) {
|
||||
if (aObjectDataId == LL_MININT) {
|
||||
stmt = aTransaction->GetCachedStatement(
|
||||
"SELECT id "
|
||||
"FROM object_data "
|
||||
|
@ -728,25 +717,24 @@ IDBObjectStore::UpdateIndexes(IDBTransaction* aTransaction,
|
|||
NS_ENSURE_SUCCESS(rv, rv);
|
||||
}
|
||||
|
||||
for (PRUint32 indexIndex = 0; indexIndex < indexCount; indexIndex++) {
|
||||
const IndexUpdateInfo& updateInfo = aUpdateInfoArray[indexIndex];
|
||||
|
||||
NS_ASSERTION(updateInfo.info.autoIncrement == aAutoIncrement, "Huh?!");
|
||||
PRUint32 infoCount = aUpdateInfoArray.Length();
|
||||
for (PRUint32 i = 0; i < infoCount; i++) {
|
||||
const IndexUpdateInfo& updateInfo = aUpdateInfoArray[i];
|
||||
|
||||
// Insert new values.
|
||||
stmt = aTransaction->IndexDataInsertStatement(aAutoIncrement,
|
||||
updateInfo.info.unique);
|
||||
updateInfo.indexUnique);
|
||||
NS_ENSURE_TRUE(stmt, NS_ERROR_FAILURE);
|
||||
|
||||
mozStorageStatementScoper scoper4(stmt);
|
||||
|
||||
rv = stmt->BindInt64ByName(indexId, updateInfo.info.id);
|
||||
rv = stmt->BindInt64ByName(indexId, updateInfo.indexId);
|
||||
NS_ENSURE_SUCCESS(rv, rv);
|
||||
|
||||
rv = stmt->BindInt64ByName(objectDataId, aObjectDataId);
|
||||
NS_ENSURE_SUCCESS(rv, rv);
|
||||
|
||||
if (!updateInfo.info.autoIncrement) {
|
||||
if (!aAutoIncrement) {
|
||||
rv = aObjectStoreKey.BindToStatement(stmt, objectDataKey);
|
||||
NS_ENSURE_SUCCESS(rv, rv);
|
||||
}
|
||||
|
@ -755,6 +743,23 @@ IDBObjectStore::UpdateIndexes(IDBTransaction* aTransaction,
|
|||
NS_ENSURE_SUCCESS(rv, rv);
|
||||
|
||||
rv = stmt->Execute();
|
||||
if (rv == NS_ERROR_STORAGE_CONSTRAINT && updateInfo.indexUnique) {
|
||||
// If we're inserting multiple entries for the same unique index, then
|
||||
// we might have failed to insert due to colliding with another entry for
|
||||
// the same index in which case we should ignore it.
|
||||
|
||||
for (PRInt32 j = (PRInt32)i - 1;
|
||||
j >= 0 && aUpdateInfoArray[j].indexId == updateInfo.indexId;
|
||||
--j) {
|
||||
if (updateInfo.value == aUpdateInfoArray[j].value) {
|
||||
// We found a key with the same value for the same index. So we
|
||||
// must have had a collision with a value we just inserted.
|
||||
rv = NS_OK;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (NS_FAILED(rv)) {
|
||||
return rv;
|
||||
}
|
||||
|
@ -929,8 +934,17 @@ IDBObjectStore::GetAddInfo(JSContext* aCx,
|
|||
NS_ERROR("This should never fail!");
|
||||
}
|
||||
|
||||
rv = GetIndexUpdateInfo(info, aCx, aValue, aUpdateInfoArray);
|
||||
NS_ENSURE_SUCCESS(rv, NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR);
|
||||
PRUint32 count = info->indexes.Length();
|
||||
aUpdateInfoArray.SetCapacity(count); // Pretty good estimate
|
||||
for (PRUint32 indexesIndex = 0; indexesIndex < count; indexesIndex++) {
|
||||
const IndexInfo& indexInfo = info->indexes[indexesIndex];
|
||||
|
||||
rv = AppendIndexUpdateInfo(indexInfo.id, indexInfo.keyPath,
|
||||
indexInfo.unique, indexInfo.multiEntry,
|
||||
aCx, aValue, aUpdateInfoArray);
|
||||
NS_ENSURE_SUCCESS(rv, rv);
|
||||
|
||||
}
|
||||
|
||||
const jschar* keyPathChars =
|
||||
reinterpret_cast<const jschar*>(mKeyPath.get());
|
||||
|
@ -1365,6 +1379,7 @@ IDBObjectStore::CreateIndex(const nsAString& aName,
|
|||
NS_ASSERTION(mTransaction->IsOpen(), "Impossible!");
|
||||
|
||||
bool unique = false;
|
||||
bool multiEntry = false;
|
||||
|
||||
// Get optional arguments.
|
||||
if (!JSVAL_IS_VOID(aOptions) && !JSVAL_IS_NULL(aOptions)) {
|
||||
|
@ -1388,6 +1403,17 @@ IDBObjectStore::CreateIndex(const nsAString& aName,
|
|||
return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR;
|
||||
}
|
||||
unique = !!boolVal;
|
||||
|
||||
if (!JS_GetPropertyById(aCx, options, nsDOMClassInfo::sMultiEntry_id, &val)) {
|
||||
NS_WARNING("JS_GetPropertyById failed!");
|
||||
return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR;
|
||||
}
|
||||
|
||||
if (!JS_ValueToBoolean(aCx, val, &boolVal)) {
|
||||
NS_WARNING("JS_ValueToBoolean failed!");
|
||||
return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR;
|
||||
}
|
||||
multiEntry = !!boolVal;
|
||||
}
|
||||
|
||||
DatabaseInfo* databaseInfo = mTransaction->Database()->Info();
|
||||
|
@ -1402,6 +1428,7 @@ IDBObjectStore::CreateIndex(const nsAString& aName,
|
|||
indexInfo->name = aName;
|
||||
indexInfo->keyPath = aKeyPath;
|
||||
indexInfo->unique = unique;
|
||||
indexInfo->multiEntry = multiEntry;
|
||||
indexInfo->autoIncrement = mAutoIncrement;
|
||||
|
||||
// Don't leave this in the list if we fail below!
|
||||
|
@ -2153,8 +2180,9 @@ CreateIndexHelper::DoDatabaseWork(mozIStorageConnection* aConnection)
|
|||
nsCOMPtr<mozIStorageStatement> stmt =
|
||||
mTransaction->GetCachedStatement(
|
||||
"INSERT INTO object_store_index (id, name, key_path, unique_index, "
|
||||
"object_store_id, object_store_autoincrement) "
|
||||
"VALUES (:id, :name, :key_path, :unique, :osid, :os_auto_increment)"
|
||||
"multientry, object_store_id, object_store_autoincrement) "
|
||||
"VALUES (:id, :name, :key_path, :unique, :multientry, :osid, "
|
||||
":os_auto_increment)"
|
||||
);
|
||||
NS_ENSURE_TRUE(stmt, NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR);
|
||||
|
||||
|
@ -2175,6 +2203,10 @@ CreateIndexHelper::DoDatabaseWork(mozIStorageConnection* aConnection)
|
|||
mIndex->IsUnique() ? 1 : 0);
|
||||
NS_ENSURE_SUCCESS(rv, NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR);
|
||||
|
||||
rv = stmt->BindInt32ByName(NS_LITERAL_CSTRING("multientry"),
|
||||
mIndex->IsMultiEntry() ? 1 : 0);
|
||||
NS_ENSURE_SUCCESS(rv, NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR);
|
||||
|
||||
rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("osid"),
|
||||
mIndex->ObjectStore()->Id());
|
||||
NS_ENSURE_SUCCESS(rv, NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR);
|
||||
|
@ -2231,42 +2263,16 @@ CreateIndexHelper::InsertDataFromObjectStore(mozIStorageConnection* aConnection)
|
|||
mIndex->ObjectStore()->Id());
|
||||
NS_ENSURE_SUCCESS(rv, NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR);
|
||||
|
||||
bool hasResult;
|
||||
while (NS_SUCCEEDED(stmt->ExecuteStep(&hasResult)) && hasResult) {
|
||||
nsCOMPtr<mozIStorageStatement> insertStmt =
|
||||
mTransaction->IndexDataInsertStatement(mIndex->IsAutoIncrement(),
|
||||
mIndex->IsUnique());
|
||||
NS_ENSURE_TRUE(insertStmt, NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR);
|
||||
|
||||
mozStorageStatementScoper scoper2(insertStmt);
|
||||
|
||||
rv = insertStmt->BindInt64ByName(NS_LITERAL_CSTRING("index_id"),
|
||||
mIndex->Id());
|
||||
NS_ENSURE_SUCCESS(rv, NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR);
|
||||
|
||||
rv = insertStmt->BindInt64ByName(NS_LITERAL_CSTRING("object_data_id"),
|
||||
stmt->AsInt64(0));
|
||||
NS_ENSURE_SUCCESS(rv, NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR);
|
||||
|
||||
if (!mIndex->IsAutoIncrement()) {
|
||||
NS_NAMED_LITERAL_CSTRING(objectDataKey, "object_data_key");
|
||||
|
||||
Key key;
|
||||
rv = key.SetFromStatement(stmt, 2);
|
||||
NS_ENSURE_SUCCESS(rv, rv);
|
||||
|
||||
rv =
|
||||
key.BindToStatement(insertStmt, NS_LITERAL_CSTRING("object_data_key"));
|
||||
NS_ENSURE_SUCCESS(rv, rv);
|
||||
}
|
||||
|
||||
const PRUint8* data;
|
||||
PRUint32 dataLength;
|
||||
rv = stmt->GetSharedBlob(1, &dataLength, &data);
|
||||
NS_ENSURE_SUCCESS(rv, NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR);
|
||||
|
||||
NS_ENSURE_TRUE(sTLSIndex != BAD_TLS_INDEX, NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR);
|
||||
|
||||
bool hasResult;
|
||||
rv = stmt->ExecuteStep(&hasResult);
|
||||
NS_ENSURE_SUCCESS(rv, NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR);
|
||||
if (!hasResult) {
|
||||
// Bail early if we have no data to avoid creating the below runtime
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
ThreadLocalJSRuntime* tlsEntry =
|
||||
reinterpret_cast<ThreadLocalJSRuntime*>(PR_GetThreadPrivate(sTLSIndex));
|
||||
|
||||
|
@ -2277,24 +2283,50 @@ CreateIndexHelper::InsertDataFromObjectStore(mozIStorageConnection* aConnection)
|
|||
PR_SetThreadPrivate(sTLSIndex, tlsEntry);
|
||||
}
|
||||
|
||||
Key key;
|
||||
rv = IDBObjectStore::GetKeyPathValueFromStructuredData(data, dataLength,
|
||||
mIndex->KeyPath(),
|
||||
tlsEntry->Context(),
|
||||
key);
|
||||
NS_ENSURE_SUCCESS(rv, rv);
|
||||
JSContext* cx = tlsEntry->Context();
|
||||
JSAutoRequest ar(cx);
|
||||
|
||||
if (key.IsUnset()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
rv = key.BindToStatement(insertStmt, NS_LITERAL_CSTRING("value"));
|
||||
NS_ENSURE_SUCCESS(rv, rv);
|
||||
|
||||
rv = insertStmt->Execute();
|
||||
do {
|
||||
const PRUint8* data;
|
||||
PRUint32 dataLength;
|
||||
rv = stmt->GetSharedBlob(1, &dataLength, &data);
|
||||
NS_ENSURE_SUCCESS(rv, NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR);
|
||||
|
||||
jsval clone;
|
||||
if (!JS_ReadStructuredClone(cx, reinterpret_cast<const uint64*>(data),
|
||||
dataLength, JS_STRUCTURED_CLONE_VERSION,
|
||||
&clone, NULL, NULL)) {
|
||||
return NS_ERROR_DOM_DATA_CLONE_ERR;
|
||||
}
|
||||
|
||||
nsTArray<IndexUpdateInfo> updateInfo;
|
||||
rv = IDBObjectStore::AppendIndexUpdateInfo(mIndex->Id(),
|
||||
mIndex->KeyPath(),
|
||||
mIndex->IsUnique(),
|
||||
mIndex->IsMultiEntry(),
|
||||
tlsEntry->Context(),
|
||||
clone, updateInfo);
|
||||
NS_ENSURE_SUCCESS(rv, rv);
|
||||
|
||||
PRInt64 objectDataID = stmt->AsInt64(0);
|
||||
|
||||
Key key;
|
||||
if (!mIndex->IsAutoIncrement()) {
|
||||
rv = key.SetFromStatement(stmt, 2);
|
||||
NS_ENSURE_SUCCESS(rv, rv);
|
||||
}
|
||||
else {
|
||||
key.SetFromInteger(objectDataID);
|
||||
}
|
||||
|
||||
rv = IDBObjectStore::UpdateIndexes(mTransaction, mIndex->Id(),
|
||||
key, mIndex->IsAutoIncrement(),
|
||||
false, objectDataID, updateInfo);
|
||||
NS_ENSURE_SUCCESS(rv, rv);
|
||||
|
||||
} while (NS_SUCCEEDED(rv = stmt->ExecuteStep(&hasResult)) && hasResult);
|
||||
NS_ENSURE_SUCCESS(rv, NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR);
|
||||
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
|
|
|
@ -76,14 +76,10 @@ public:
|
|||
IsValidKeyPath(JSContext* aCx, const nsAString& aKeyPath);
|
||||
|
||||
static nsresult
|
||||
GetKeyPathValueFromStructuredData(const PRUint8* aData,
|
||||
PRUint32 aDataLength,
|
||||
AppendIndexUpdateInfo(PRInt64 aIndexID,
|
||||
const nsAString& aKeyPath,
|
||||
JSContext* aCx,
|
||||
Key& aValue);
|
||||
|
||||
static nsresult
|
||||
GetIndexUpdateInfo(ObjectStoreInfo* aObjectStoreInfo,
|
||||
bool aUnique,
|
||||
bool aMultiEntry,
|
||||
JSContext* aCx,
|
||||
jsval aObject,
|
||||
nsTArray<IndexUpdateInfo>& aUpdateInfoArray);
|
||||
|
|
|
@ -406,7 +406,7 @@ IDBTransaction::IndexDataInsertStatement(bool aAutoIncrement,
|
|||
);
|
||||
}
|
||||
return GetCachedStatement(
|
||||
"INSERT INTO ai_index_data "
|
||||
"INSERT OR IGNORE INTO ai_index_data "
|
||||
"(index_id, ai_object_data_id, value) "
|
||||
"VALUES (:index_id, :object_data_id, :value)"
|
||||
);
|
||||
|
@ -419,7 +419,7 @@ IDBTransaction::IndexDataInsertStatement(bool aAutoIncrement,
|
|||
);
|
||||
}
|
||||
return GetCachedStatement(
|
||||
"INSERT INTO index_data ("
|
||||
"INSERT OR IGNORE INTO index_data ("
|
||||
"index_id, object_data_id, object_data_key, value) "
|
||||
"VALUES (:index_id, :object_data_id, :object_data_key, :value)"
|
||||
);
|
||||
|
|
|
@ -51,7 +51,7 @@
|
|||
#include "nsStringGlue.h"
|
||||
#include "nsTArray.h"
|
||||
|
||||
#define DB_SCHEMA_VERSION 7
|
||||
#define DB_SCHEMA_VERSION 8
|
||||
|
||||
#define BEGIN_INDEXEDDB_NAMESPACE \
|
||||
namespace mozilla { namespace dom { namespace indexedDB {
|
||||
|
|
|
@ -165,6 +165,7 @@ CreateTables(mozIStorageConnection* aDBConn)
|
|||
"name TEXT NOT NULL, "
|
||||
"key_path TEXT NOT NULL, "
|
||||
"unique_index INTEGER NOT NULL, "
|
||||
"multientry INTEGER NOT NULL DEFAULT 0, "
|
||||
"object_store_autoincrement INTERGER NOT NULL, "
|
||||
"PRIMARY KEY (id), "
|
||||
"UNIQUE (object_store_id, name), "
|
||||
|
@ -819,6 +820,83 @@ UpgradeSchemaFrom6To7(mozIStorageConnection* aConnection)
|
|||
return NS_OK;
|
||||
}
|
||||
|
||||
|
||||
nsresult
|
||||
UpgradeSchemaFrom7To8(mozIStorageConnection* aConnection)
|
||||
{
|
||||
mozStorageTransaction transaction(aConnection, false,
|
||||
mozIStorageConnection::TRANSACTION_IMMEDIATE);
|
||||
|
||||
// Turn off foreign key constraints before we do anything here.
|
||||
nsresult rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
|
||||
"PRAGMA foreign_keys = OFF;"
|
||||
));
|
||||
NS_ENSURE_SUCCESS(rv, rv);
|
||||
|
||||
rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
|
||||
"CREATE TEMPORARY TABLE temp_upgrade ("
|
||||
"id, "
|
||||
"object_store_id, "
|
||||
"name, "
|
||||
"key_path, "
|
||||
"unique_index, "
|
||||
"object_store_autoincrement, "
|
||||
");"
|
||||
));
|
||||
NS_ENSURE_SUCCESS(rv, rv);
|
||||
|
||||
rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
|
||||
"INSERT INTO temp_upgrade "
|
||||
"SELECT id, object_store_id, name, key_path, "
|
||||
"unique_index, object_store_autoincrement, "
|
||||
"FROM object_store_index;"
|
||||
));
|
||||
NS_ENSURE_SUCCESS(rv, rv);
|
||||
|
||||
rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
|
||||
"DROP TABLE object_store_index;"
|
||||
));
|
||||
NS_ENSURE_SUCCESS(rv, rv);
|
||||
|
||||
rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
|
||||
"CREATE TABLE object_store_index ("
|
||||
"id INTEGER, "
|
||||
"object_store_id INTEGER NOT NULL, "
|
||||
"name TEXT NOT NULL, "
|
||||
"key_path TEXT NOT NULL, "
|
||||
"unique_index INTEGER NOT NULL, "
|
||||
"multientry INTEGER NOT NULL, "
|
||||
"object_store_autoincrement INTERGER NOT NULL, "
|
||||
"PRIMARY KEY (id), "
|
||||
"UNIQUE (object_store_id, name), "
|
||||
"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_store_index "
|
||||
"SELECT id, object_store_id, name, key_path, "
|
||||
"unique_index, 0, object_store_autoincrement, "
|
||||
"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->SetSchemaVersion(8);
|
||||
NS_ENSURE_SUCCESS(rv, rv);
|
||||
|
||||
rv = transaction.Commit();
|
||||
NS_ENSURE_SUCCESS(rv, rv);
|
||||
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
nsresult
|
||||
CreateDatabaseConnection(const nsAString& aName,
|
||||
nsIFile* aDBFile,
|
||||
|
@ -870,7 +948,7 @@ CreateDatabaseConnection(const nsAString& aName,
|
|||
}
|
||||
else if (schemaVersion != DB_SCHEMA_VERSION) {
|
||||
// This logic needs to change next time we change the schema!
|
||||
PR_STATIC_ASSERT(DB_SCHEMA_VERSION == 7);
|
||||
PR_STATIC_ASSERT(DB_SCHEMA_VERSION == 8);
|
||||
|
||||
#define UPGRADE_SCHEMA_CASE(_from, _to) \
|
||||
if (schemaVersion == _from) { \
|
||||
|
@ -886,6 +964,7 @@ CreateDatabaseConnection(const nsAString& aName,
|
|||
UPGRADE_SCHEMA_CASE(4, 5)
|
||||
UPGRADE_SCHEMA_CASE(5, 6)
|
||||
UPGRADE_SCHEMA_CASE(6, 7)
|
||||
UPGRADE_SCHEMA_CASE(7, 8)
|
||||
|
||||
#undef UPGRADE_SCHEMA_CASE
|
||||
|
||||
|
|
|
@ -47,7 +47,7 @@ interface nsIIDBRequest;
|
|||
* http://dev.w3.org/2006/webapi/WebSimpleDB/#idl-def-IDBIndex for more
|
||||
* information.
|
||||
*/
|
||||
[scriptable, builtinclass, uuid(1da60889-3db4-4f66-9fd7-b78c1e7969b7)]
|
||||
[scriptable, builtinclass, uuid(fcb9a158-833e-4aa9-ab19-ab90cbb50afc)]
|
||||
interface nsIIDBIndex : nsISupports
|
||||
{
|
||||
readonly attribute DOMString name;
|
||||
|
@ -58,6 +58,8 @@ interface nsIIDBIndex : nsISupports
|
|||
|
||||
readonly attribute boolean unique;
|
||||
|
||||
readonly attribute boolean multiEntry;
|
||||
|
||||
readonly attribute nsIIDBObjectStore objectStore;
|
||||
|
||||
[implicit_jscontext]
|
||||
|
|
|
@ -84,6 +84,7 @@ TEST_FILES = \
|
|||
test_indexes_bad_values.html \
|
||||
test_key_requirements.html \
|
||||
test_leaving_page.html \
|
||||
test_multientry.html \
|
||||
test_objectCursors.html \
|
||||
test_objectStore_inline_autoincrement_key_added_on_put.html \
|
||||
test_objectStore_remove_values.html \
|
||||
|
|
|
@ -0,0 +1,230 @@
|
|||
<!--
|
||||
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()
|
||||
{
|
||||
// Test object stores
|
||||
|
||||
let openRequest = mozIndexedDB.open(window.location.pathname, 1);
|
||||
openRequest.onerror = errorHandler;
|
||||
openRequest.onupgradeneeded = grabEventAndContinueHandler;
|
||||
openRequest.onsuccess = unexpectedSuccessHandler;
|
||||
let event = yield;
|
||||
let db = event.target.result;
|
||||
db.onerror = errorHandler;
|
||||
let tests =
|
||||
[{ add: { x: 1, id: 1 },
|
||||
indexes:[{ v: 1, k: 1 }] },
|
||||
{ add: { x: [2, 3], id: 2 },
|
||||
indexes:[{ v: 1, k: 1 },
|
||||
{ v: 2, k: 2 },
|
||||
{ v: 3, k: 2 }] },
|
||||
{ put: { x: [2, 4], id: 1 },
|
||||
indexes:[{ v: 2, k: 1 },
|
||||
{ v: 2, k: 2 },
|
||||
{ v: 3, k: 2 },
|
||||
{ v: 4, k: 1 }] },
|
||||
{ add: { x: [5, 6, 5, -2, 3], id: 3 },
|
||||
indexes:[{ v:-2, k: 3 },
|
||||
{ v: 2, k: 1 },
|
||||
{ v: 2, k: 2 },
|
||||
{ v: 3, k: 2 },
|
||||
{ v: 3, k: 3 },
|
||||
{ v: 4, k: 1 },
|
||||
{ v: 5, k: 3 },
|
||||
{ v: 6, k: 3 }] },
|
||||
{ delete: IDBKeyRange.bound(1, 3),
|
||||
indexes:[] },
|
||||
{ put: { x: ["food", {}, false, undefined, /x/, [73, false]], id: 2 },
|
||||
indexes:[{ v: "food", k: 2 }] },
|
||||
{ add: { x: [{}, /x/, -12, "food", null, [false], undefined], id: 3 },
|
||||
indexes:[{ v: -12, k: 3 },
|
||||
{ v: "food", k: 2 },
|
||||
{ v: "food", k: 3 }] },
|
||||
{ put: { x: [], id: 2 },
|
||||
indexes:[{ v: -12, k: 3 },
|
||||
{ v: "food", k: 3 }] },
|
||||
{ put: { x: { y: 3 }, id: 3 },
|
||||
indexes:[] },
|
||||
{ add: { x: false, id: 7 },
|
||||
indexes:[] },
|
||||
{ delete: IDBKeyRange.lowerBound(0),
|
||||
indexes:[] },
|
||||
];
|
||||
|
||||
let store = db.createObjectStore("mystore", { keyPath: "id" });
|
||||
let index = store.createIndex("myindex", "x", { multiEntry: true });
|
||||
is(index.multiEntry, true, "index created with multiEntry");
|
||||
|
||||
let i;
|
||||
for (i = 0; i < tests.length; ++i) {
|
||||
let test = tests[i];
|
||||
let testName = " for " + JSON.stringify(test);
|
||||
let req;
|
||||
if (test.add) {
|
||||
req = store.add(test.add);
|
||||
}
|
||||
else if (test.put) {
|
||||
req = store.put(test.put);
|
||||
}
|
||||
else if (test.delete) {
|
||||
req = store.delete(test.delete);
|
||||
}
|
||||
else {
|
||||
ok(false, "borked test");
|
||||
}
|
||||
req.onsuccess = grabEventAndContinueHandler;
|
||||
let e = yield;
|
||||
|
||||
req = index.openKeyCursor();
|
||||
req.onsuccess = grabEventAndContinueHandler;
|
||||
for (let j = 0; j < test.indexes.length; ++j) {
|
||||
e = yield;
|
||||
is(req.result.key, test.indexes[j].v, "found expected index key at index " + j + testName);
|
||||
is(req.result.primaryKey, test.indexes[j].k, "found expected index primary key at index " + j + testName);
|
||||
req.result.continue();
|
||||
}
|
||||
e = yield;
|
||||
is(req.result, undefined, "exhausted indexes");
|
||||
|
||||
let tempIndex = store.createIndex("temp index", "x", { multiEntry: true });
|
||||
req = tempIndex.openKeyCursor();
|
||||
req.onsuccess = grabEventAndContinueHandler;
|
||||
for (let j = 0; j < test.indexes.length; ++j) {
|
||||
e = yield;
|
||||
is(req.result.key, test.indexes[j].v, "found expected temp index key at index " + j + testName);
|
||||
is(req.result.primaryKey, test.indexes[j].k, "found expected temp index primary key at index " + j + testName);
|
||||
req.result.continue();
|
||||
}
|
||||
e = yield;
|
||||
is(req.result, undefined, "exhausted temp index");
|
||||
store.deleteIndex("temp index");
|
||||
}
|
||||
|
||||
// Unique indexes
|
||||
tests =
|
||||
[{ add: { x: 1, id: 1 },
|
||||
indexes:[{ v: 1, k: 1 }] },
|
||||
{ add: { x: [2, 3], id: 2 },
|
||||
indexes:[{ v: 1, k: 1 },
|
||||
{ v: 2, k: 2 },
|
||||
{ v: 3, k: 2 }] },
|
||||
{ put: { x: [2, 4], id: 3 },
|
||||
fail: true },
|
||||
{ put: { x: [1, 4], id: 1 },
|
||||
indexes:[{ v: 1, k: 1 },
|
||||
{ v: 2, k: 2 },
|
||||
{ v: 3, k: 2 },
|
||||
{ v: 4, k: 1 }] },
|
||||
{ add: { x: [5, 0, 5, 5, 5], id: 3 },
|
||||
indexes:[{ v: 0, k: 3 },
|
||||
{ v: 1, k: 1 },
|
||||
{ v: 2, k: 2 },
|
||||
{ v: 3, k: 2 },
|
||||
{ v: 4, k: 1 },
|
||||
{ v: 5, k: 3 }] },
|
||||
{ delete: IDBKeyRange.bound(1, 2),
|
||||
indexes:[{ v: 0, k: 3 },
|
||||
{ v: 5, k: 3 }] },
|
||||
{ add: { x: [0, 6], id: 8 },
|
||||
fail: true },
|
||||
{ add: { x: 5, id: 8 },
|
||||
fail: true },
|
||||
{ put: { x: 0, id: 8 },
|
||||
fail: true },
|
||||
];
|
||||
|
||||
store.deleteIndex("myindex");
|
||||
index = store.createIndex("myindex", "x", { multiEntry: true, unique: true });
|
||||
is(index.multiEntry, true, "index created with multiEntry");
|
||||
|
||||
let i;
|
||||
let indexes;
|
||||
for (i = 0; i < tests.length; ++i) {
|
||||
let test = tests[i];
|
||||
let testName = " for " + JSON.stringify(test);
|
||||
let req;
|
||||
if (test.add) {
|
||||
req = store.add(test.add);
|
||||
}
|
||||
else if (test.put) {
|
||||
req = store.put(test.put);
|
||||
}
|
||||
else if (test.delete) {
|
||||
req = store.delete(test.delete);
|
||||
}
|
||||
else {
|
||||
ok(false, "borked test");
|
||||
}
|
||||
|
||||
if (!test.fail) {
|
||||
req.onsuccess = grabEventAndContinueHandler;
|
||||
let e = yield;
|
||||
indexes = test.indexes;
|
||||
}
|
||||
else {
|
||||
req.onsuccess = unexpectedSuccessHandler;
|
||||
req.onerror = grabEventAndContinueHandler;
|
||||
ok(true, "waiting for error");
|
||||
let e = yield;
|
||||
ok(true, "got error: " + e.type);
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
let e;
|
||||
req = index.openKeyCursor();
|
||||
req.onsuccess = grabEventAndContinueHandler;
|
||||
for (let j = 0; j < indexes.length; ++j) {
|
||||
e = yield;
|
||||
is(req.result.key, indexes[j].v, "found expected index key at index " + j + testName);
|
||||
is(req.result.primaryKey, indexes[j].k, "found expected index primary key at index " + j + testName);
|
||||
req.result.continue();
|
||||
}
|
||||
e = yield;
|
||||
is(req.result, undefined, "exhausted indexes");
|
||||
|
||||
let tempIndex = store.createIndex("temp index", "x", { multiEntry: true, unique: true });
|
||||
req = tempIndex.openKeyCursor();
|
||||
req.onsuccess = grabEventAndContinueHandler;
|
||||
for (let j = 0; j < indexes.length; ++j) {
|
||||
e = yield;
|
||||
is(req.result.key, indexes[j].v, "found expected temp index key at index " + j + testName);
|
||||
is(req.result.primaryKey, indexes[j].k, "found expected temp index primary key at index " + j + testName);
|
||||
req.result.continue();
|
||||
}
|
||||
e = yield;
|
||||
is(req.result, undefined, "exhausted temp index");
|
||||
store.deleteIndex("temp index");
|
||||
}
|
||||
|
||||
|
||||
openRequest.onsuccess = grabEventAndContinueHandler;
|
||||
yield;
|
||||
|
||||
let trans = db.transaction(["mystore"], IDBTransaction.READ_WRITE);
|
||||
store = trans.objectStore("mystore");
|
||||
index = store.index("myindex");
|
||||
is(index.multiEntry, true, "index still is multiEntry");
|
||||
trans.oncomplete = grabEventAndContinueHandler;
|
||||
yield;
|
||||
|
||||
finishTest();
|
||||
yield;
|
||||
}
|
||||
</script>
|
||||
<script type="text/javascript;version=1.7" src="helpers.js"></script>
|
||||
</head>
|
||||
|
||||
<body onload="runTest();"></body>
|
||||
|
||||
</html>
|
Загрузка…
Ссылка в новой задаче