/// <summary> /// Internal implementation of insert a document /// </summary> private void InsertDocument(Snapshot snapshot, BsonDocument doc, BsonAutoId autoId, IndexService indexer, DataService data) { // if no _id, use AutoId if (!doc.TryGetValue("_id", out var id)) { doc["_id"] = id = autoId == BsonAutoId.ObjectId ? new BsonValue(ObjectId.NewObjectId()) : autoId == BsonAutoId.Guid ? new BsonValue(Guid.NewGuid()) : this.GetSequence(snapshot, autoId); } else if (id.IsNumber) { // update memory sequence of numeric _id this.SetSequence(snapshot, id); } // test if _id is a valid type if (id.IsNull || id.IsMinValue || id.IsMaxValue) { throw LiteException.InvalidDataType("_id", id); } // storage in data pages - returns dataBlock address var dataBlock = data.Insert(doc); IndexNode last = null; // for each index, insert new IndexNode foreach (var index in snapshot.CollectionPage.GetCollectionIndexes()) { // for each index, get all keys (supports multi-key) - gets distinct values only // if index are unique, get single key only var keys = index.BsonExpr.Execute(doc, _header.Pragmas.Collation); // do a loop with all keys (multi-key supported) foreach (var key in keys) { // insert node var node = indexer.AddNode(index, key, dataBlock, last); last = 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> /// Implement internal update document /// </summary> private bool UpdateDocument(Snapshot snapshot, CollectionPage col, BsonDocument doc, IndexService indexer, DataService data) { // normalize id before find var id = doc["_id"]; // validate id for null, min/max values if (id.IsNull || id.IsMinValue || id.IsMaxValue) { throw LiteException.InvalidDataType("_id", id); } // find indexNode from pk index var pkNode = indexer.Find(col.PK, id, false, LiteDB.Query.Ascending); // if not found document, no updates if (pkNode == null) { return(false); } // update data storage data.Update(col, pkNode.DataBlock, doc); // get all current non-pk index nodes from this data block (slot, key, nodePosition) var oldKeys = indexer.GetNodeList(pkNode.NextNode) .Select(x => new Tuple <byte, BsonValue, PageAddress>(x.Slot, x.Key, x.Position)) .ToArray(); // build a list of all new key index keys var newKeys = new List <Tuple <byte, BsonValue, string> >(); foreach (var index in col.GetCollectionIndexes().Where(x => x.Name != "_id")) { // getting all keys from expression over document var keys = index.BsonExpr.Execute(doc, _header.Pragmas.Collation); foreach (var key in keys) { newKeys.Add(new Tuple <byte, BsonValue, string>(index.Slot, key, index.Name)); } } if (oldKeys.Length == 0 && newKeys.Count == 0) { return(true); } // get a list of all nodes that are in oldKeys but not in newKeys (must delete) var toDelete = new HashSet <PageAddress>(oldKeys .Where(x => newKeys.Any(n => n.Item1 == x.Item1 && n.Item2 == x.Item2) == false) .Select(x => x.Item3)); // get a list of all keys that are not in oldKeys (must insert) var toInsert = newKeys .Where(x => oldKeys.Any(o => o.Item1 == x.Item1 && o.Item2 == x.Item2) == false) .ToArray(); // if nothing to change, just exit if (toDelete.Count == 0 && toInsert.Length == 0) { return(true); } // delete nodes and return last keeped node in list var last = indexer.DeleteList(pkNode.Position, toDelete); // now, insert all new nodes foreach (var elem in toInsert) { var index = col.GetCollectionIndex(elem.Item3); last = indexer.AddNode(index, elem.Item2, pkNode.DataBlock, last); } 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; })); }