You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The ORM doesn't support the range type though, so an additional library and custom type is required.
The library is pretty old though. I had to modernize it in TypeScript and an ESM module as:
Click for details:
exporttypeTstzRangeBounds='()'|'[]'|'[)'|'(]';/** * Represents a range of time with a beginning and end. * * @example * const range = new TstzRange(new Date("2021-01-01T00:00:00Z"), new Date("2021-01-02T00:00:00Z"), "[)"); */exportdefaultclassTstzRange{staticparse(range: string,parseEndpoint: (str: string)=>Date|null=(str)=>newDate(str)): TstzRange{constmatch=range.match(/([\[\(])(.*?),(.*?)([\]\)])/);if(!match)thrownewError("Invalid range format");const[,startBound,begin,end,endBound]=match,beginDate=begin.trim()==="-infinity" ? null : parseEndpoint(begin.trim()),endDate=end.trim()==="infinity" ? null : parseEndpoint(end.trim());returnnewTstzRange(beginDate,endDate,`${startBound}${endBound}`asTstzRangeBounds);}/** * Returns the union of two ranges. */staticunion(a: TstzRange,b: TstzRange): TstzRange{constnewBegin=!a.begin||(b.begin&&a.compareBegin(b.begin)>0) ? b.begin : a.begin,newEnd=!a.end||(b.end&&a.compareEnd(b.end)<0) ? b.end : a.end,newBoundStart=a.bounds[0]==='['||b.bounds[0]==='[' ? '[' : '(',newBoundEnd=a.bounds[1]===']'||b.bounds[1]===']' ? ']' : ')',newBounds=`${newBoundStart}${newBoundEnd}`asTstzRangeBounds;returnnewTstzRange(newBegin,newEnd,newBounds);}constructor(/** * The beginning of the range. * If `null`, the range is unbounded on the left. */publicbegin: Date|null,/** * The end of the range. * If `null`, the range is unbounded on the right. */publicend: Date|null,/** * The bounds of the range. * * - `()` for exclusive * - `[]` for inclusive * - `[)` for left-inclusive * - `(]` for right-inclusive */bounds?: TstzRangeBounds){if(begin&&end&&begin.getTime()>end.getTime())thrownewError("Begin date must be before end date");if(!bounds){constbStart=begin ? "[" : "(",bEnd=end ? "]" : ")";this.bounds=`${bStart}${bEnd}`;}else{this.bounds=bounds;}}bounds: TstzRangeBounds;/** * Compares the beginning of the range with the given date. */compareBegin(begin: Date): number{if(!this.begin)return-1;returnthis.begin.getTime()-begin.getTime();}/** * Compares the end of the range with the given date. */compareEnd(end: Date): number{if(!this.end)return1;returnthis.end.getTime()-end.getTime();}/** * Checks if the range contains the given date. */contains(value: Date): boolean{const[$0,$1]=this.bounds,{ begin, end }=this,isAfterBegin=$0==="[" ? value>=begin! : value>begin!,isBeforeEnd=$1==="]" ? value<=end! : value<end!;returnisAfterBegin&&isBeforeEnd;}/** * Checks if the range intersects (overlaps) with another range. */intersects(other: TstzRange): boolean{// If either range is empty, they don't intersectif(this.isEmpty()||other.isEmpty())returnfalse;// Check if either range contains the other's begin or end, or if they are directly overlappingreturn(((this.begin&&other.contains(this.begin))??false)||((this.end&&other.contains(this.end))??false)||((other.begin&&this.contains(other.begin))??false)||((other.end&&this.contains(other.end))??false));}/** * Checks if the range is bounded. */isBounded(): boolean{returnthis.begin!==null&&this.end!==null;}/** * Checks if the range is empty. */isEmpty(): boolean{returnthis.begin===this.end;}/** * Checks if the range is finite. */isFinite(): boolean{returnthis.begin!==null&&this.end!==null;}/** * Checks if the range is infinite. */isInfinite(): boolean{returnthis.begin===null||this.end===null;}/** * Checks if the range is unbounded. */isUnbounded(): boolean{returnthis.isInfinite();}toJSON(): string{returnJSON.stringify({begin: this.begin ? this.begin.toISOString() : null,end: this.end ? this.end.toISOString() : null,bounds: this.bounds,});}/** * Returns a string representation of the range. * * @example * const range = new TstzRange(new Date("2021-01-01T00:00:00Z"), new Date("2021-01-02T00:00:00Z"), "[)"); * console.log(range.toString()); // [2021-01-01T00:00:00.000Z, 2021-01-02T00:00:00.000Z) */toString(): string{constbeginStr=this.begin ? this.begin.toISOString() : "-infinity",endStr=this.end ? this.end.toISOString() : "infinity";return`${this.bounds[0]}${beginStr}, ${endStr}${this.bounds[1]}`;}valueOf(): string{returnthis.toString();}}
Sigh... This implementation needs alot of workarounds to function
The SQL Versioning function
a. This has to be embedded in a migration file and unmanaged by the ORM as the ORM does not have a means of defining it
A trigger definition hack on every base entity. Ex:
~50 History tables to go with the corresponding entities
a. Maybe there is a way to dynamically define the resulting Single Table Inheritance schema from the class hierarchy
The Custom type declaration to map the TypeScript TstzRange class to the datbase value:
Even with the custom type declaration, range comparisons won't work as desired with the database as postgres utilizes a custom operator: <@. I believe the ORM solution to this is to perform the comparisons in the application layer instead of the database via the TstzRangeType.prototype.compareValues(a, b) method.
New Approach
A simpler, more correct, approach may be the design of TommCatt:
erDiagram
REQUIREMENT {
uuid req_id PK
foo static_attr1
bar static_attr2
}
REQUIREMENT_VERSION {
uuid req_id PK,FK
date effective PK
bit deleted
foo volatile_attr1
bar volatile_attr2
}
REQUIREMENT ||--o{ REQUIREMENT_VERSION : versions
Loading
The domain classes will need to all become readonly.
All CRUD actions become database INSERTs
Deletions are accomplished by inserting a new record with the deleted bit set
Interactors and Repositories will probably need to be re-introduced
Each Domain class will need a corresponding *Version class
This will have to be a parallel class hierarchy to take advantage of STI from the ORM
With the addition of extra fields effective, deleted, the Domain entities would no longer be agnostic of the data layer in violation of Clean Architecture. This implies that the decorators would need to be replaced with data model definitions again.
Original Approach
Postgres does not support Temporal tables.
Extensions are not allowed on Azure Postgres. Here is an alternative:
https://github.com/nearform/temporal_tables
The ORM doesn't support the range type though, so an additional library and custom type is required.
The library is pretty old though. I had to modernize it in TypeScript and an ESM module as:
Click for details:
Sigh... This implementation needs alot of workarounds to function
a. This has to be embedded in a migration file and unmanaged by the ORM as the ORM does not have a means of defining it
a. Maybe there is a way to dynamically define the resulting Single Table Inheritance schema from the class hierarchy
TstzRange
class to the datbase value:Click for details
Even with the custom type declaration, range comparisons won't work as desired with the database as postgres utilizes a custom operator:
<@
. I believe the ORM solution to this is to perform the comparisons in the application layer instead of the database via theTstzRangeType.prototype.compareValues(a, b)
method.New Approach
A simpler, more correct, approach may be the design of TommCatt:
deleted
bit set*Version
classeffective
,deleted
, the Domain entities would no longer be agnostic of the data layer in violation of Clean Architecture. This implies that the decorators would need to be replaced with data model definitions again.The text was updated successfully, but these errors were encountered: