/// <summary> /// Insert a new node index inside an collection index. /// </summary> private IndexNode AddNode(CollectionIndex index, BsonValue key, PageAddress dataBlock, byte level, IndexNode last) { // get a free index page for head note var bytesLength = IndexNode.GetNodeLength(level, key, out var keyLength); // test for index key maxlength if (keyLength > MAX_INDEX_KEY_LENGTH) { throw LiteException.InvalidIndexKey($"Index key must be less than {MAX_INDEX_KEY_LENGTH} bytes."); } var indexPage = _snapshot.GetFreeIndexPage(bytesLength, ref index.FreeIndexPageList); // create node in buffer var node = indexPage.InsertIndexNode(index.Slot, level, key, dataBlock, bytesLength); // now, let's link my index node on right place var cur = this.GetNode(index.Head); // using as cache last IndexNode cache = null; // scan from top left for (int i = index.MaxLevel - 1; i >= 0; i--) { // get cache for last node cache = cache != null && cache.Position == cur.Next[i] ? cache : this.GetNode(cur.Next[i]); // for(; <while_not_this>; <do_this>) { ... } for (; cur.Next[i].IsEmpty == false; cur = cache) { // get cache for last node cache = cache != null && cache.Position == cur.Next[i] ? cache : this.GetNode(cur.Next[i]); // read next node to compare var diff = cache.Key.CompareTo(key, _collation); // if unique and diff = 0, throw index exception (must rollback transaction - others nodes can be dirty) if (diff == 0 && index.Unique) { throw LiteException.IndexDuplicateKey(index.Name, key); } if (diff == 1) { break; } } if (i <= (level - 1)) // level == length { // cur = current (immediately before - prev) // node = new inserted node // next = next node (where cur is pointing) node.SetNext((byte)i, cur.Next[i]); node.SetPrev((byte)i, cur.Position); cur.SetNext((byte)i, node.Position); var next = this.GetNode(node.Next[i]); if (next != null) { next.SetPrev((byte)i, node.Position); } } } // if last node exists, create a double link list if (last != null) { ENSURE(last.NextNode == PageAddress.Empty, "last index node must point to null"); last.SetNextNode(node.Position); } // fix page position in free list slot _snapshot.AddOrRemoveFreeIndexList(node.Page, ref index.FreeIndexPageList); return(node); }
/// <summary> /// Create a new index (or do nothing if already exists) to a collection/field /// </summary> public bool EnsureIndex(string collection, string name, BsonExpression expression, bool unique) { if (collection.IsNullOrWhiteSpace()) { throw new ArgumentNullException(nameof(collection)); } if (name.IsNullOrWhiteSpace()) { throw new ArgumentNullException(nameof(name)); } if (expression == null) { throw new ArgumentNullException(nameof(expression)); } if (expression.IsIndexable == false) { throw new ArgumentException("Index expressions must contains at least one document field. Used methods must be immutable. Parameters are not supported.", nameof(expression)); } if (name.Length > INDEX_NAME_MAX_LENGTH) { throw LiteException.InvalidIndexName(name, collection, "MaxLength = " + INDEX_NAME_MAX_LENGTH); } if (!name.IsWord()) { throw LiteException.InvalidIndexName(name, collection, "Use only [a-Z$_]"); } if (name.StartsWith("$")) { throw LiteException.InvalidIndexName(name, collection, "Index name can't starts with `$`"); } if (name == "_id") { return(false); // always exists } return(this.AutoTransaction(transaction => { var snapshot = transaction.CreateSnapshot(LockMode.Write, collection, true); var col = snapshot.CollectionPage; var indexer = new IndexService(snapshot); var data = new DataService(snapshot); // check if index already exists var current = col.GetCollectionIndex(name); // if already exists, just exit if (current != null) { // but if expression are different, throw error if (current.Expression != expression.Source) { throw LiteException.IndexAlreadyExist(name); } return false; } LOG($"create index `{collection}.{name}`", "COMMAND"); // create index head var index = indexer.CreateIndex(name, expression.Source, unique); var count = 0u; // read all objects (read from PK index) foreach (var pkNode in new IndexAll("_id", LiteDB.Query.Ascending).Run(col, indexer)) { using (var reader = new BufferReader(data.Read(pkNode.DataBlock))) { var doc = reader.ReadDocument(expression.Fields); // first/last node in this document that will be added IndexNode last = null; IndexNode first = null; // get values from expression in document var keys = expression.Execute(doc); // adding index node for each value foreach (var key in keys) { // when index key is an array, get items inside array. // valid only for first level (if this items are another array, this arrays will be indexed as array) if (key.IsArray) { var arr = key.AsArray; foreach (var itemKey in arr) { // insert new index node var node = indexer.AddNode(index, itemKey, pkNode.DataBlock, last, _flipCoin); if (first == null) { first = node; } last = node; count++; } } else { // insert new index node var node = indexer.AddNode(index, key, pkNode.DataBlock, last, _flipCoin); if (first == null) { first = node; } last = node; count++; } } // fix single linked-list in pkNode if (first != null) { last.SetNextNode(pkNode.NextNode); pkNode.SetNextNode(first.Position); } } transaction.Safepoint(); } index.KeyCount = count; return true; })); }
/// <summary> /// Create a new index (or do nothing if already exists) to a collection/field /// </summary> public async Task <bool> EnsureIndexAsync(string collection, string name, BsonExpression expression, bool unique) { if (collection.IsNullOrWhiteSpace()) { throw new ArgumentNullException(nameof(collection)); } if (name.IsNullOrWhiteSpace()) { throw new ArgumentNullException(nameof(name)); } if (expression == null) { throw new ArgumentNullException(nameof(expression)); } if (expression.IsIndexable == false) { throw new ArgumentException("Index expressions must contains at least one document field. Used methods must be immutable. Parameters are not supported.", nameof(expression)); } if (name.Length > INDEX_NAME_MAX_LENGTH) { throw LiteException.InvalidIndexName(name, collection, "MaxLength = " + INDEX_NAME_MAX_LENGTH); } if (!name.IsWord()) { throw LiteException.InvalidIndexName(name, collection, "Use only [a-Z$_]"); } if (name.StartsWith("$")) { throw LiteException.InvalidIndexName(name, collection, "Index name can't starts with `$`"); } if (expression.IsScalar == false && unique) { throw new LiteException(0, "Multikey index expression do not support unique option"); } if (expression.Source == "$._id") { return(false); // always exists } return(await this.AutoTransaction(async transaction => { var snapshot = await transaction.CreateSnapshot(LockMode.Write, collection, true); var collectionPage = snapshot.CollectionPage; var indexer = new IndexService(snapshot, _header.Pragmas.Collation); var data = new DataService(snapshot); // check if index already exists var current = collectionPage.GetCollectionIndex(name); // if already exists, just exit if (current != null) { // but if expression are different, throw error if (current.Expression != expression.Source) { throw LiteException.IndexAlreadyExist(name); } return false; } LOG($"create index `{collection}.{name}`", "COMMAND"); // create index head var index = await indexer.CreateIndex(name, expression.Source, unique); var count = 0u; // read all objects (read from PK index) await foreach (var pkNode in new IndexAll("_id", LiteDB.Query.Ascending).Run(collectionPage, indexer)) { await using (var reader = await BufferReaderAsync.CreateAsync(data.Read(pkNode.DataBlock))) { var doc = await reader.ReadDocument(expression.Fields); // first/last node in this document that will be added IndexNode last = null; IndexNode first = null; // get values from expression in document var keys = expression.GetIndexKeys(doc, _header.Pragmas.Collation); // adding index node for each value foreach (var key in keys) { // insert new index node var node = await indexer.AddNode(index, key, pkNode.DataBlock, last); if (first == null) { first = node; } last = node; count++; } // fix single linked-list in pkNode if (first != null) { last.SetNextNode(pkNode.NextNode); pkNode.SetNextNode(first.Position); } } await transaction.Safepoint(); } return true; })); }