/// <summary> /// Write a DeletedRecord in deleteIn dataset for the specified key /// instead of actually deleting the record. This ensures that /// a record in another dataset does not become visible during /// lookup in a sequence of datasets. /// /// To avoid an additional roundtrip to the data store, the delete /// marker is written even when the record does not exist. /// </summary> public override void Delete <TKey, TRecord>(TypedKey <TKey, TRecord> key, TemporalId deleteIn) { // Error message if data source is readonly or has CutoffTime set CheckNotReadOnly(deleteIn); // For pinned records, delete in root dataset irrespective of the value of saveTo if (IsPinned <TRecord>()) { deleteIn = TemporalId.Empty; } // Create DeletedRecord with the specified key var record = new DeletedRecord { Key = key.Value }; // Get collection var collection = GetOrCreateCollection <TRecord>(); // This method guarantees that TemporalIds will be in strictly increasing // order for this instance of the data source class always, and across // all processes and machine if they are not created within the same // second. var recordId = CreateOrderedTemporalId(); record.Id = recordId; // Assign dataset and then initialize, as the results of // initialization may depend on record.DataSet record.DataSet = deleteIn; // By design, insert will fail if TemporalId is not unique within the collection collection.BaseCollection.InsertOne(record); }
/// <summary> /// The returned TemporalIds have the following order guarantees: /// /// * For this data source instance, to arbitrary resolution; and /// * Across all processes and machines, to one second resolution /// /// One second resolution means that two TemporalIds created within /// the same second by different instances of the data source /// class may not be ordered chronologically unless they are at /// least one second apart. /// </summary> public override TemporalId CreateOrderedTemporalId() { // Generate TemporalId and check that it is later // than the previous generated TemporalId TemporalId result = TemporalId.Next(); int retryCounter = 0; while (result <= prevTemporalId_) { // Getting inside the while loop will be very rare as this would // require the increment to roll from max int to min int within // the same second, therefore it is a good idea to log the event if (retryCounter++ == 0) { Context.Log.Warning("MongoDB generated TemporalId not in increasing order, retrying."); } // If new TemporalId is not strictly greater than the previous one, // keep generating new TemporalIds until it changes result = TemporalId.Next(); } // Report the number of retries if (retryCounter != 0) { Context.Log.Warning($"Generated TemporalId in increasing order after {retryCounter} retries."); } // Update previous TemporalId and return prevTemporalId_ = result; return(result); }
/// <summary> /// Save one record to the specified dataset. After the method exits, /// record.DataSet will be set to the value of the dataSet parameter. /// /// All Save methods ignore the value of record.DataSet before the /// Save method is called. When dataset is not specified explicitly, /// the value of dataset from the context, not from the record, is used. /// The reason for this behavior is that the record may be stored from /// a different dataset than the one where it is used. /// /// This method guarantees that TemporalIds of the saved records will be in /// strictly increasing order. /// </summary> public static void SaveOne <TRecord>(this DataSource obj, TRecord record, TemporalId saveTo) where TRecord : Record { // Pass a list of records with one element to SaveMany obj.SaveMany(new List <TRecord> { record }, saveTo); }
/// <summary> /// Load record by its TemporalId. /// /// Error message if there is no record for the specified TemporalId, /// or if the record exists but is not derived from TRecord. /// </summary> public static TRecord Load <TRecord>(this DataSource obj, TemporalId id) where TRecord : Record { var result = obj.LoadOrNull <TRecord>(id); if (result == null) { throw new Exception( $"Record with TemporalId={id} is not found in data store {obj.DataSourceName}."); } return(result); }
/// <summary> /// Create from collection for the base type Record, the lookup /// list of datasets, and IOrderedQueryable(TRecord). /// /// This constructor is private and is intended for use by the /// implementation of this class only. /// </summary> private TemporalMongoQuery(TemporalMongoCollection <TRecord> collection, TemporalId loadFrom, IOrderedQueryable <TRecord> orderedQueryable) { if (orderedQueryable == null) { throw new Exception( "Attempting to create a query from a null orderedQueryable."); } collection_ = collection; loadFrom_ = loadFrom; orderedQueryable_ = orderedQueryable; }
/// <summary> /// Get query for the specified type. /// /// After applying query parameters, the lookup occurs first in /// descending order of dataset TemporalIds, and then in the descending /// order of record TemporalIds within the first dataset that /// has at least one record. Both dataset and record TemporalIds /// are ordered chronologically to one second resolution, /// and are unique within the database server or cluster. /// /// The root dataset has empty TemporalId value that is less /// than any other TemporalId value. Accordingly, the root /// dataset is the last one in the lookup order of datasets. /// /// Generic parameter TRecord is not necessarily the root data type; /// it may also be a type derived from the root data type. /// </summary> public override IQuery <TRecord> GetQuery <TRecord>(TemporalId loadFrom) { // For pinned records, load from root dataset irrespective of the value of loadFrom if (IsPinned <TRecord>()) { loadFrom = TemporalId.Empty; } // Get or create collection, then create query from collection var collection = GetOrCreateCollection <TRecord>(); return(new TemporalMongoQuery <TRecord>(collection, loadFrom)); }
/// <summary> /// Error message if either ReadOnly flag or CutoffTime is set /// for the data source. /// </summary> private void CheckNotReadOnly(TemporalId dataSetId) { if (ReadOnly != null && ReadOnly.Value) { throw new Exception( $"Attempting write operation for data source {DataSourceName} where ReadOnly flag is set."); } if (CutoffTime != null) { throw new Exception( $"Attempting write operation for data source {DataSourceName} where " + $"CutoffTime is set. Historical view of the data cannot be written to."); } }
//--- CONSTRUCTORS /// <summary> /// Create query from collection and dataset. /// </summary> public TemporalMongoQuery(TemporalMongoCollection <TRecord> collection, TemporalId loadFrom) { collection_ = collection; loadFrom_ = loadFrom; // Create queryable from typed collection rather than base // collection so LINQ queries can be applied to properties // of the generic parameter type TRecord var queryable = collection_.TypedCollection.AsQueryable(); // Without explicitly applying OfType, the match for _t // may or may not be added. This step ensures the match // for _t exists and is the first stage in the aggregation // pipeline queryable_ = queryable.OfType <TRecord>(); }
/// <summary> /// Load record by its TemporalId. /// /// Return null if there is no record for the specified TemporalId; /// however an exception will be thrown if the record exists but /// is not derived from TRecord. /// </summary> public override TRecord LoadOrNull <TRecord>(TemporalId id) { if (CutoffTime != null) { // Return null if argument TemporalId is greater than // or equal to CutoffTime. if (id >= CutoffTime.Value) { return(null); } } // Find last record in last dataset without constraining record type. // The result may not be derived from TRecord. var baseResult = GetOrCreateCollection <TRecord>() .BaseCollection .AsQueryable() .FirstOrDefault(p => p.Id == id); // Check not only for null but also for the DeletedRecord if (baseResult != null && !baseResult.Is <DeletedRecord>()) { // Record is found but we do not yet know if it has the right type. // Attempt to cast Record to TRecord and check if the result is null. TRecord result = baseResult.As <TRecord>(); if (result == null) { // If cast result is null, the record was found but it is an instance // of class that is not derived from TRecord, in this case the API // requires error message, not returning null throw new Exception( $"Stored type {result.GetType().Name} for TemporalId={id} and " + $"Key={result.Key} is not an instance of the requested type {typeof(TRecord).Name}."); } // Initialize before returning result.Init(Context); return(result); } else { // Record not found or is a DeletedRecord, return null return(null); } }
/// <summary> /// Returns enumeration of import datasets for specified dataset data, /// including imports of imports to unlimited depth with cyclic /// references and duplicates removed. /// /// The list will not include datasets that are after the value of /// CutoffTime if specified, or their imports (including /// even those imports that are earlier than the constraint). /// </summary> public IEnumerable <TemporalId> GetDataSetLookupList(TemporalId dataSetId) { // Root dataset has no imports (there is not even a record // where these imports can be specified). // // Return list containing only the root dataset (TemporalId.Empty) and exit if (dataSetId == TemporalId.Empty) { return(new TemporalId[] { TemporalId.Empty }); } if (importDict_.TryGetValue(dataSetId, out HashSet <TemporalId> result)) { // Check if the lookup list is already cached, return if yes return(result); } else { // Otherwise load from storage (returns null if not found) DataSet dataSetRecord = LoadOrNull <DataSet>(dataSetId); if (dataSetRecord == null) { throw new Exception($"Dataset with TemporalId={dataSetId} is not found."); } if (dataSetRecord.DataSet != TemporalId.Empty) { throw new Exception($"Dataset with TemporalId={dataSetId} is not stored in root dataset."); } // Build the lookup list result = BuildDataSetLookupList(dataSetRecord); // Add to dictionary and return importDict_.Add(dataSetId, result); return(result); } }
/// <summary> /// Write a DeletedRecord in deleteIn dataset for the specified key /// instead of actually deleting the record. This ensures that /// a record in another dataset does not become visible during /// lookup in a sequence of datasets. /// /// To avoid an additional roundtrip to the data store, the delete /// marker is written even when the record does not exist. /// </summary> public static void Delete <TKey, TRecord>(this Context obj, TypedKey <TKey, TRecord> key, TemporalId deleteIn) where TKey : TypedKey <TKey, TRecord>, new() where TRecord : TypedRecord <TKey, TRecord> { obj.DataSource.Delete(key, deleteIn); }
/// <summary> /// Save multiple records to the specified dataset. After the method exits, /// for each record the property record.DataSet will be set to the value of /// the saveTo parameter. /// /// All Save methods ignore the value of record.DataSet before the /// Save method is called. When dataset is not specified explicitly, /// the value of dataset from the context, not from the record, is used. /// The reason for this behavior is that the record may be stored from /// a different dataset than the one where it is used. /// /// This method guarantees that TemporalIds of the saved records will be in /// strictly increasing order. /// </summary> public static void SaveMany <TRecord>(this Context obj, IEnumerable <TRecord> records, TemporalId saveTo) where TRecord : Record { obj.DataSource.SaveMany(records, saveTo); }
/// <summary> /// Get query for the specified type. /// /// After applying query parameters, the lookup occurs first in /// descending order of dataset TemporalIds, and then in the descending /// order of record TemporalIds within the first dataset that /// has at least one record. Both dataset and record TemporalIds /// are ordered chronologically to one second resolution, /// and are unique within the database server or cluster. /// /// The root dataset has empty TemporalId value that is less /// than any other TemporalId value. Accordingly, the root /// dataset is the last one in the lookup order of datasets. /// /// Generic parameter TRecord is not necessarily the root data type; /// it may also be a type derived from the root data type. /// </summary> public override IQuery <TRecord> GetQuery <TRecord>(TemporalId loadFrom) { throw MethodCalledForNullDataSourceError(); }
/// <summary> /// Load record from context.DataSource, overriding the dataset /// specified in the context with the value specified as the /// second parameter. The lookup occurs in the specified dataset /// and its imports, expanded to arbitrary depth with repetitions /// and cyclic references removed. /// /// IMPORTANT - this overload of the method loads from loadFrom /// dataset, not from context.DataSet. /// /// If Record property is set, its value is returned without /// performing lookup in the data store; otherwise the record /// is loaded from storage and cached in Record and the /// cached value is returned from subsequent calls. /// /// Once the record has been cached, the same version will be /// returned in subsequent calls with the same key instance. /// Create a new key or call earRecord() method to force /// reloading new version of the record from storage. /// /// Error message if the record is not found or is a DeletedRecord. /// </summary> public static TRecord Load <TKey, TRecord>(this DataSource obj, TypedKey <TKey, TRecord> key, TemporalId loadFrom) where TKey : TypedKey <TKey, TRecord>, new() where TRecord : TypedRecord <TKey, TRecord> { // This method will return null if the record is // not found or the found record is a DeletedRecord var result = obj.LoadOrNull(key, loadFrom); // Error message if null, otherwise return if (result == null) { throw new Exception( $"Record with key {key} is not found in dataset with TemporalId={loadFrom}."); } return(result); }
/// <summary> /// Write a DeletedRecord in deleteIn dataset for the specified key /// instead of actually deleting the record. This ensures that /// a record in another dataset does not become visible during /// lookup in a sequence of datasets. /// /// To avoid an additional roundtrip to the data store, the delete /// marker is written even when the record does not exist. /// </summary> public abstract void Delete <TKey, TRecord>(TypedKey <TKey, TRecord> key, TemporalId deleteIn) where TKey : TypedKey <TKey, TRecord>, new() where TRecord : TypedRecord <TKey, TRecord>;
/// <summary> /// Get query for the specified type. /// /// After applying query parameters, the lookup occurs first in /// descending order of dataset TemporalIds, and then in the descending /// order of record TemporalIds within the first dataset that /// has at least one record. Both dataset and record TemporalIds /// are ordered chronologically to one second resolution, /// and are unique within the database server or cluster. /// /// The root dataset has empty TemporalId value that is less /// than any other TemporalId value. Accordingly, the root /// dataset is the last one in the lookup order of datasets. /// /// Generic parameter TRecord is not necessarily the root data type; /// it may also be a type derived from the root data type. /// </summary> public abstract IQuery <TRecord> GetQuery <TRecord>(TemporalId loadFrom) where TRecord : Record;
//--- PRIVATE /// <summary> /// Populate key elements from an array of tokens starting /// at the specified token index. Elements that are themselves /// keys may use more than one token. /// /// This method returns the index of the first unused token. /// The returned value is the same as the length of the tokens /// array if all tokens are used. /// /// If key AKey has two elements, B and C, where /// /// * B has type BKey which has two string elements, and /// * C has type string, /// /// the semicolon delimited key has the following format: /// /// BToken1;BToken2;CToken /// /// To avoid serialization format uncertainty, key elements /// can have any atomic type except Double. /// </summary> private int PopulateFrom(string[] tokens, int tokenIndex) { // Get key elements using reflection var elementInfoArray = DataTypeInfo.GetOrCreate(this).DataElements; // If singleton is detected process it separately, then exit if (elementInfoArray.Length == 0) { // Check that string key is empty if (tokens.Length != 1 || tokens[0] != String.Empty) { throw new Exception($"Type {GetType()} has key {string.Join(";", tokens)} while " + $"for a singleton the key must be an empty string (String.Empty). " + $"Singleton key is a key that has no key elements."); } // Return the length of empty key which consists of one (empty) token return(1); } // Check that there are enough remaining tokens in the key for each key element if (tokens.Length - tokenIndex < elementInfoArray.Length) { throw new Exception( $"Key of type {GetType().Name} requires at least {elementInfoArray.Length} elements " + $"{String.Join(";", elementInfoArray.Select(p => p.Name).ToArray())} while there are " + $"only {tokens.Length - tokenIndex} remaining key tokens: {string.Join(";", tokens)}."); } // Iterate over element info elements, advancing tokenIndex by the required // number of tokens for each element. In case of embedded keys, the value of // tokenIndex is advanced by the recursive call to InitFromTokens method // of the embedded key. foreach (var elementInfo in elementInfoArray) { // Get element type Type elementType = elementInfo.PropertyType; // Convert string token to value depending on elementType if (elementType == typeof(string)) { CheckTokenNotEmpty(tokens, tokenIndex); string token = tokens[tokenIndex++]; elementInfo.SetValue(this, token); } else if (elementType == typeof(double) || elementType == typeof(double?)) { throw new Exception( $"Key element {elementInfo.Name} has type Double. Elements of this type " + $"cannot be part of key due to serialization format uncertainty."); } else if (elementType == typeof(bool) || elementType == typeof(bool?)) { CheckTokenNotEmpty(tokens, tokenIndex); string token = tokens[tokenIndex++]; bool tokenValue = bool.Parse(token); elementInfo.SetValue(this, tokenValue); } else if (elementType == typeof(int) || elementType == typeof(int?)) { CheckTokenNotEmpty(tokens, tokenIndex); string token = tokens[tokenIndex++]; int tokenValue = int.Parse(token); elementInfo.SetValue(this, tokenValue); } else if (elementType == typeof(long) || elementType == typeof(long?)) { CheckTokenNotEmpty(tokens, tokenIndex); string token = tokens[tokenIndex++]; long tokenValue = long.Parse(token); elementInfo.SetValue(this, tokenValue); } else if (elementType == typeof(LocalDate) || elementType == typeof(LocalDate?)) { CheckTokenNotEmpty(tokens, tokenIndex); // Inside the key, LocalDate is represented as readable int in // non-delimited yyyymmdd format, not as delimited ISO string. // // First parse the string to int, then convert int to LocalDate. string token = tokens[tokenIndex++]; if (!Int32.TryParse(token, out int isoInt)) { throw new Exception( $"Element {elementInfo.Name} of key type {GetType().Name} has type LocalDate and value {token} " + $"that cannot be converted to readable int in non-delimited yyyymmdd format."); } LocalDate tokenValue = LocalDateUtil.FromIsoInt(isoInt); elementInfo.SetValue(this, tokenValue); } else if (elementType == typeof(LocalTime) || elementType == typeof(LocalTime?)) { CheckTokenNotEmpty(tokens, tokenIndex); // Inside the key, LocalTime is represented as readable int in // non-delimited hhmmssfff format, not as delimited ISO string. // // First parse the string to int, then convert int to LocalTime. string token = tokens[tokenIndex++]; if (!Int32.TryParse(token, out int isoInt)) { throw new Exception( $"Element {elementInfo.Name} of key type {GetType().Name} has type LocalTime and value {token} " + $"that cannot be converted to readable int in non-delimited hhmmssfff format."); } LocalTime tokenValue = LocalTimeUtil.FromIsoInt(isoInt); elementInfo.SetValue(this, tokenValue); } else if (elementType == typeof(LocalMinute) || elementType == typeof(LocalMinute?)) { CheckTokenNotEmpty(tokens, tokenIndex); // Inside the key, LocalMinute is represented as readable int in // non-delimited hhmm format, not as delimited ISO string. // // First parse the string to int, then convert int to LocalTime. string token = tokens[tokenIndex++]; if (!Int32.TryParse(token, out int isoInt)) { throw new Exception( $"Element {elementInfo.Name} of key type {GetType().Name} has type LocalMinute and value {token} " + $"that cannot be converted to readable int in non-delimited hhmm format."); } LocalMinute tokenValue = LocalMinuteUtil.FromIsoInt(isoInt); elementInfo.SetValue(this, tokenValue); } else if (elementType == typeof(LocalDateTime) || elementType == typeof(LocalDateTime?)) { CheckTokenNotEmpty(tokens, tokenIndex); // Inside the key, LocalDateTime is represented as readable long in // non-delimited yyyymmddhhmmssfff format, not as delimited ISO string. // // First parse the string to long, then convert int to LocalDateTime. string token = tokens[tokenIndex++]; if (!Int64.TryParse(token, out long isoLong)) { throw new Exception( $"Element {elementInfo.Name} of key type {GetType().Name} has type LocalDateTime and value {token} " + $"that cannot be converted to readable long in non-delimited yyyymmddhhmmssfff format."); } LocalDateTime tokenValue = LocalDateTimeUtil.FromIsoLong(isoLong); elementInfo.SetValue(this, tokenValue); } else if (elementType == typeof(Instant) || elementType == typeof(Instant?)) { CheckTokenNotEmpty(tokens, tokenIndex); // Inside the key, Instant is represented as readable long in // non-delimited yyyymmddhhmmssfff format, not as delimited ISO string. // // First parse the string to long, then convert int to Instant. string token = tokens[tokenIndex++]; if (!Int64.TryParse(token, out long isoLong)) { throw new Exception( $"Element {elementInfo.Name} of key type {GetType().Name} has type Instant and value {token} " + $"that cannot be converted to readable long in non-delimited yyyymmddhhmmssfff format."); } Instant tokenValue = InstantUtil.FromIsoLong(isoLong); elementInfo.SetValue(this, tokenValue); } else if (elementType == typeof(TemporalId) || elementType == typeof(TemporalId?)) { CheckTokenNotEmpty(tokens, tokenIndex); string token = tokens[tokenIndex++]; TemporalId tokenValue = TemporalId.Parse(token); elementInfo.SetValue(this, tokenValue); } else if (elementType.BaseType == typeof(Enum)) // TODO Support nullable Enum in key { CheckTokenNotEmpty(tokens, tokenIndex); string token = tokens[tokenIndex++]; object tokenValue = Enum.Parse(elementType, token); elementInfo.SetValue(this, tokenValue); } else if (typeof(Key).IsAssignableFrom(elementType)) { Key keyElement = (Key)Activator.CreateInstance(elementType); tokenIndex = keyElement.PopulateFrom(tokens, tokenIndex); elementInfo.SetValue(this, keyElement); } else { // Field type is unsupported for a key, error message throw new Exception( $"Element {elementInfo.Name} of key type {GetType().Name} has type {elementType} that " + $"is not one of the supported key element types. Available key element types are " + $"string, bool, int, long, LocalDate, LocalTime, LocalMinute, LocalDateTime, Instant, Enum, or Key."); } } return(tokenIndex); }
/// <summary> /// Load record by string key from the specified dataset or /// its list of imports. The lookup occurs first in descending /// order of dataset TemporalIds, and then in the descending /// order of record TemporalIds within the first dataset that /// has at least one record. Both dataset and record TemporalIds /// are ordered chronologically to one second resolution, /// and are unique within the database server or cluster. /// /// The root dataset has empty TemporalId value that is less /// than any other TemporalId value. Accordingly, the root /// dataset is the last one in the lookup order of datasets. /// /// The first record in this lookup order is returned, or null /// if no records are found or if DeletedRecord is the first /// record. /// /// Return null if there is no record for the specified TemporalId; /// however an exception will be thrown if the record exists but /// is not derived from TRecord. /// </summary> public override TRecord LoadOrNull <TKey, TRecord>(TypedKey <TKey, TRecord> key, TemporalId loadFrom) { throw MethodCalledForNullDataSourceError(); }
/// <summary> /// Load record by its TemporalId. /// /// Return null if there is no record for the specified TemporalId; /// however an exception will be thrown if the record exists but /// is not derived from TRecord. /// </summary> public override TRecord LoadOrNull <TRecord>(TemporalId id) { throw MethodCalledForNullDataSourceError(); }
/// <summary> /// Write a DeletedRecord in deleteIn dataset for the specified key /// instead of actually deleting the record. This ensures that /// a record in another dataset does not become visible during /// lookup in a sequence of datasets. /// /// To avoid an additional roundtrip to the data store, the delete /// marker is written even when the record does not exist. /// </summary> public override void Delete <TKey, TRecord>(TypedKey <TKey, TRecord> key, TemporalId deleteIn) { throw MethodCalledForNullDataSourceError(); }
/// <summary> /// Save multiple records to the specified dataset. After the method exits, /// for each record the property record.DataSet will be set to the value of /// the saveTo parameter. /// /// All Save methods ignore the value of record.DataSet before the /// Save method is called. When dataset is not specified explicitly, /// the value of dataset from the context, not from the record, is used. /// The reason for this behavior is that the record may be stored from /// a different dataset than the one where it is used. /// /// This method guarantees that TemporalIds of the saved records will be in /// strictly increasing order. /// </summary> public override void SaveMany <TRecord>(IEnumerable <TRecord> records, TemporalId saveTo) { throw MethodCalledForNullDataSourceError(); }
/// <summary> /// Load record by its TemporalId. /// /// Return null if there is no record for the specified TemporalId; /// however an exception will be thrown if the record exists but /// is not derived from TRecord. /// </summary> public abstract TRecord LoadOrNull <TRecord>(TemporalId id) where TRecord : Record;
/// <summary> /// Load record by string key from the specified dataset or /// its list of imports. The lookup occurs first in descending /// order of dataset TemporalIds, and then in the descending /// order of record TemporalIds within the first dataset that /// has at least one record. Both dataset and record TemporalIds /// are ordered chronologically to one second resolution, /// and are unique within the database server or cluster. /// /// The root dataset has empty TemporalId value that is less /// than any other TemporalId value. Accordingly, the root /// dataset is the last one in the lookup order of datasets. /// /// The first record in this lookup order is returned, or null /// if no records are found or if DeletedRecord is the first /// record. /// /// Return null if there is no record for the specified TemporalId; /// however an exception will be thrown if the record exists but /// is not derived from TRecord. /// </summary> public abstract TRecord LoadOrNull <TKey, TRecord>(TypedKey <TKey, TRecord> key, TemporalId loadFrom) where TKey : TypedKey <TKey, TRecord>, new() where TRecord : TypedRecord <TKey, TRecord>;
/// <summary> /// Gets ImportsCutoffTime from the dataset detail record. /// Returns null if dataset detail record is not found. /// /// Imported records (records loaded through the Imports list) /// where TemporalId is greater than or equal to CutoffTime /// will be ignored by load methods and queries, and the latest /// available record where TemporalId is less than CutoffTime will /// be returned instead. /// /// This setting only affects records loaded through the Imports /// list. It does not affect records stored in the dataset itself. /// /// Use this feature to freeze Imports as of a given CreatedTime /// (part of TemporalId), isolating the dataset from changes to the /// data in imported datasets that occur after that time. /// </summary> public TemporalId?GetImportsCutoffTime(TemporalId dataSetId) { // TODO - implement when stored in dataset return(null); }
/// <summary> /// Save multiple records to the specified dataset. After the method exits, /// for each record the property record.DataSet will be set to the value of /// the saveTo parameter. /// /// All Save methods ignore the value of record.DataSet before the /// Save method is called. When dataset is not specified explicitly, /// the value of dataset from the context, not from the record, is used. /// The reason for this behavior is that the record may be stored from /// a different dataset than the one where it is used. /// /// This method guarantees that TemporalIds of the saved records will be in /// strictly increasing order. /// </summary> public abstract void SaveMany <TRecord>(IEnumerable <TRecord> records, TemporalId saveTo) where TRecord : Record;
/// <summary> /// Load record by string key from the specified dataset or /// its list of imports. The lookup occurs first in descending /// order of dataset TemporalIds, and then in the descending /// order of record TemporalIds within the first dataset that /// has at least one record. Both dataset and record TemporalIds /// are ordered chronologically to one second resolution, /// and are unique within the database server or cluster. /// /// The root dataset has empty TemporalId value that is less /// than any other TemporalId value. Accordingly, the root /// dataset is the last one in the lookup order of datasets. /// /// The first record in this lookup order is returned, or null /// if no records are found or if DeletedRecord is the first /// record. /// /// Return null if there is no record for the specified TemporalId; /// however an exception will be thrown if the record exists but /// is not derived from TRecord. /// </summary> public static TRecord LoadOrNull <TKey, TRecord>(this Context obj, TypedKey <TKey, TRecord> key, TemporalId loadFrom) where TKey : TypedKey <TKey, TRecord>, new() where TRecord : TypedRecord <TKey, TRecord> { return(obj.DataSource.LoadOrNull(key, loadFrom)); }
/// <summary> /// Apply the final constraints after all prior Where clauses but before OrderBy clause: /// /// * The constraint on dataset lookup list, restricted by CutoffTime (if not null) /// * The constraint on ID being strictly less than CutoffTime (if not null) /// </summary> public IQueryable <TRecord> ApplyFinalConstraints <TRecord>(IQueryable <TRecord> queryable, TemporalId loadFrom) where TRecord : Record { // For pinned records, load from root dataset irrespective of the value of loadFrom if (IsPinned <TRecord>()) { loadFrom = TemporalId.Empty; } // Get lookup list by expanding the list of imports to arbitrary // depth with duplicates and cyclic references removed. // // The list will not include datasets that are after the value of // CutoffTime if specified, or their imports (including // even those imports that are earlier than the constraint). IEnumerable <TemporalId> dataSetLookupList = GetDataSetLookupList(loadFrom); // Apply constraint that the value is _dataset is // one of the elements of dataSetLookupList_ var result = queryable.Where(p => dataSetLookupList.Contains(p.DataSet)); // Apply revision time constraint. By making this constraint the // last among the constraints, we optimize the use of the index. // // The property savedBy_ is set using either CutoffTime element. // Only one of these two elements can be set at a given time. if (CutoffTime != null) { result = result.Where(p => p.Id < CutoffTime.Value); } return(result); }
/// <summary> /// Get query for the specified type. /// /// After applying query parameters, the lookup occurs first in /// descending order of dataset TemporalIds, and then in the descending /// order of record TemporalIds within the first dataset that /// has at least one record. Both dataset and record TemporalIds /// are ordered chronologically to one second resolution, /// and are unique within the database server or cluster. /// /// The root dataset has empty TemporalId value that is less /// than any other TemporalId value. Accordingly, the root /// dataset is the last one in the lookup order of datasets. /// /// Generic parameter TRecord is not necessarily the root data type; /// it may also be a type derived from the root data type. /// </summary> public static IQuery <TRecord> GetQuery <TRecord>(this Context obj, TemporalId loadFrom) where TRecord : Record { return(obj.DataSource.GetQuery <TRecord>(loadFrom)); }
/// <summary> /// Load record by its TemporalId. /// /// Return null if there is no record for the specified TemporalId; /// however an exception will be thrown if the record exists but /// is not derived from TRecord. /// </summary> public static TRecord LoadOrNull <TRecord>(this Context obj, TemporalId id) where TRecord : Record { return(obj.DataSource.LoadOrNull <TRecord>(id)); }
/// <summary> /// Save record to the specified dataset. After the method exits, /// record.DataSet will be set to the value of the dataSet parameter. /// /// All Save methods ignore the value of record.DataSet before the /// Save method is called. When dataset is not specified explicitly, /// the value of dataset from the context, not from the record, is used. /// The reason for this behavior is that the record may be stored from /// a different dataset than the one where it is used. /// /// This method guarantees that TemporalIds will be in strictly increasing /// order for this instance of the data source class always, and across /// all processes and machine if they are not created within the same /// second. /// </summary> public static void SaveOne <TRecord>(this Context obj, TRecord record, TemporalId saveTo) where TRecord : Record { obj.DataSource.SaveOne(record, saveTo); }