-- WalnutDBImpl.mesa
-- Contents: implementation for making changes in Walnut's database
-- this is the monitored access to the database

-- Last Edited by: Willie-Sue, December 13, 1983 2:28 pm
-- Last Edited by: Donahue, July 15, 1983 1:17 pm

DIRECTORY
 BasicTime USING [GMT, Now],
DB USING [ BoolType, EntitySet, GMT, IntType, RopeType, TimeType,
     CreateRelship, DeclareAttribute, DeclareDomain, DeclareEntity, DeclareIndex,
     DeclareRelation, DeclareRelship, DestroyEntity, DestroyRelship, DomainSubset,
     Eq, GetF, GetName, GetP, GetPList, NextEntity, NextRelship, Null, RelationSubset,
     ReleaseEntitySet, ReleaseRelshipSet, SetP, SetPList, S2V, T2V, V2I, V2T],
IO,
 Rope,
 WalnutDB,
 WalnutVoice USING [MakeInterestEntry, VoiceMoveTo],
 WalnutLog USING [MsgRec, MessageRecObject,
     currentSegment, msgIDRope,
     GenerateNewMsgID, RopeFromLog, TiogaTextFromLog],
 WalnutSendOps USING [userRName, simpleUserName, RFC822Date],
 WalnutStream USING [MakeLogEntry];

WalnutDBImpl: CEDAR MONITOR
IMPORTS
  DB, IO, Rope, BasicTime,
  WalnutDB, WalnutLog, WalnutSendOps, WalnutStream, WalnutVoice
EXPORTS WalnutDB =

BEGIN OPEN DB, WalnutDB;

MsgDomain: PUBLIC Domain;
MsgSetDomain: PUBLIC Domain;

activeMsgSet: PUBLIC MsgSet;
deletedMsgSet: PUBLIC MsgSet;

-- ********************************************************
-- Relations on Msg entities: all have one relship per Msg unless otherwise noted.

msNumInSet: PUBLIC Relation;
msNumInSetOf: PUBLIC Attribute; -- MsgSet
msNumInSetIs: PUBLIC Attribute; -- INT

mDateCode: PUBLIC Relation;
mDateCodeOf: PUBLIC Attribute; -- Msg
mDateCodeIs: PUBLIC Attribute; -- Time

mSubject: PUBLIC Relation;
mSubjectOf: PUBLIC Attribute; -- Msg
mSubjectIs: PUBLIC Attribute; -- string

mCategory: PUBLIC Relation;
mCategoryOf: PUBLIC Attribute; -- Msg
mCategoryIs: PUBLIC Attribute; -- MsgSet
mCategoryDate: PUBLIC Attribute; -- Index on Category & Date

mInReplyTo: PUBLIC Relation;
mInReplyToOf: PUBLIC Attribute; -- Msg
mInReplyToMsg: PUBLIC Attribute; -- Msg
mInReplyToIs: PUBLIC Attribute; -- the string from the msg

mTOCEntry: PUBLIC Relation;
mTOCEntryOf: PUBLIC Attribute; -- Msg
mTOCEntryIs: PUBLIC Attribute; -- string

mLogPos: PUBLIC Relation;
mLogPosOf: PUBLIC Attribute; -- Msg
mPrefixPos: PUBLIC Attribute; -- int
mHeadersPos: PUBLIC Attribute; -- int

mMsgLength: PUBLIC Relation;
mMsgLengthOf: PUBLIC Attribute; -- Msg
mMsgLengthIs: PUBLIC Attribute; -- int

mHasBeenRead: PUBLIC Relation;
mHasBeenReadOf: PUBLIC Attribute; -- Msg
mHasBeenReadIs: PUBLIC Attribute; -- bool

NumInNegative: SIGNAL[msgSet: ROPE] = CODE;

-- ********************************************************

SetUpdatesPendingProc: PROC[BOOL]← DefaultUpdateProc;

-- ********************************************************
-- each operation here has a parallel operation in WalnutDBLog, which writes an entry
-- on the current log

-- the following are exported in WalnutDB

DeclareMsg
: PUBLIC ENTRY PROC[mName: ROPE, version: Version← NewOrOld]
  RETURNS [msg: Msg, existed: BOOL] =
-- Creates a new Msg entity (unless version = oldOnly), and sets its name to be mName.
-- is called when messages have been read from the log or another file.
-- If the Msg already exists, returns it and existed=TRUE
BEGIN ENABLE UNWIND => NULL;
msg← DeclareEntity[MsgDomain, mName, OldOnly];
IF msg#NIL THEN RETURN[msg, TRUE];
IF version = NewOrOld THEN
{ msg← DeclareEntity[MsgDomain, mName];
SetUpdatesPendingProc[TRUE];
};
RETURN[msg, FALSE]
END;

DeclareMsgSet: PUBLIC ENTRY PROC[msName: ROPE, version: Version← NewOrOld]
  RETURNS [msgSet: MsgSet, existed: BOOL] =
-- Creates a new MsgSet entity (unless version = oldOnly), and sets its name to be mName.
-- is called when user creates a message set, or a log file is read
-- If the MsgSet already exists, returns it and existed=TRUE.
BEGIN ENABLE UNWIND => NULL;
msgSet← DeclareEntity[MsgSetDomain, msName, OldOnly];
IF msgSet#NIL THEN RETURN[msgSet, TRUE];
IF version = NewOrOld THEN
{ msgSet← DeclareEntity[MsgSetDomain, msName];
 SetUpdatesPendingProc[TRUE];
};
 RETURN[msgSet, FALSE];
END;

-- DestroyMsg is not allowed; this is ONLY done by the Expunge operation

DestroyMsgSet: PUBLIC ENTRY PROC[msgSet: MsgSet] RETURNS[newRelList: LIST OF Relship] =
-- Destroys the given msgSet, removing any messages from it first
BEGIN ENABLE UNWIND => NULL;
 rel, newRel: Relship;
 relListEnd: LIST OF Relship;
 rs: RelshipSet← RelationSubset[mCategory, LIST[[mCategoryIs, msgSet]]];
UNTIL DB.Null[rel← NextRelship[rs]] DO
  msg: Msg← V2E[GetF[rel, mCategoryOf]];
IF (newRel← RemoveFrom[msg, msgSet, rel]) # NIL THEN
IF relListEnd=NIL THEN newRelList← relListEnd← CONS[newRel, NIL]
ELSE relListEnd.rest← relListEnd← CONS[newRel, NIL];
ENDLOOP;

 ReleaseRelshipSet[rs];
IF ~EqEntities[msgSet, activeMsgSet] THEN DestroyEntity[msgSet];
 SetUpdatesPendingProc[TRUE];
END;

AddMsgToMsgSet: PUBLIC ENTRY PROC[msg: Msg, msgSet: MsgSet]
  RETURNS[rel: Relship, existed: BOOL] =
-- Adds Msg to MsgSet, if it's not already in it
-- IF msgSet=deletedMsgSet, does nothing & returns rel=NIL, existed=FALSE
{ ENABLE UNWIND => NULL;
IF Eq[msgSet, deletedMsgSet] THEN RETURN[NIL, FALSE]; -- can't AddTo "Deleted"
[rel, existed]← AddTo[msg, msgSet]
};

RemoveMsgFromMsgSet: PUBLIC ENTRY PROC[msg: Msg, msgSet: MsgSet, rel: Relship← NIL]
  RETURNS[newRel: Relship] =
-- rel can be NIL, in which case it is found from the database
-- IF removing msg from msgSet would leave it in no MsgSet, then msg gets added to
-- the distinguished MsgSet Deleted, and returns that new Relship
-- does nothing if this Relship doesn't exist (no ERROR generated)

{ ENABLE UNWIND => NULL; RETURN[RemoveFrom[msg, msgSet, rel]]};

SetMsgHasBeenRead: PUBLIC ENTRY PROC[msg: Msg] =
-- sets the mHasBeenReadIs attribute for msg to TRUE (no check on old value)
BEGIN ENABLE UNWIND => NULL;
 []← SetP[msg, mHasBeenReadIs, B2V[TRUE]];
 SetUpdatesPendingProc[TRUE];
END;

-- * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *

-- declare and initialize Walnut's data schema

InitializeDBVars: PUBLIC ENTRY PROC =
BEGIN ENABLE UNWIND => NULL;

 MsgDomain←
  DeclareDomain[name: "Msg", segment: WalnutLog.currentSegment, estRelships: 10];
 MsgSetDomain← DeclareDomain["MsgSet", WalnutLog.currentSegment];

 msNumInSet← DeclareRelation["msNumInSet", WalnutLog.currentSegment];
  msNumInSetOf← DeclareAttribute[msNumInSet, "of", MsgSetDomain, Key]; -- MsgSet
  msNumInSetIs← DeclareAttribute[msNumInSet, "is", IntType]; -- INT

 mDateCode← DeclareRelation["mDateCode", WalnutLog.currentSegment];
  mDateCodeOf← DeclareAttribute[mDateCode, "of", MsgDomain, Key]; -- Msg
  mDateCodeIs← DeclareAttribute[mDateCode, "is", TimeType]; -- Time:

 []← DeclareIndex[mDateCode, LIST[mDateCodeIs], NewOrOld];

 mSubject← DeclareRelation["mSubject", WalnutLog.currentSegment];
  mSubjectOf← DeclareAttribute[mSubject, "of", MsgDomain, Key]; -- Msg
  mSubjectIs← DeclareAttribute[mSubject, "is", RopeType]; -- string
 mCategory← DeclareRelation["mCategory", WalnutLog.currentSegment];
  mCategoryOf← DeclareAttribute[mCategory, "of", MsgDomain]; -- Msg
  mCategoryIs← DeclareAttribute[mCategory, "is", MsgSetDomain --, link: FALSE --]; -- MsgSet
  mCategoryDate← DeclareAttribute[mCategory, "date", TimeType]; -- Time

 []← DeclareIndex[mCategory, LIST[mCategoryIs, mCategoryDate], NewOrOld];

 mInReplyTo← DeclareRelation["mInReplyTo", WalnutLog.currentSegment];
  mInReplyToOf← DeclareAttribute[mInReplyTo, "of", MsgDomain, OptionalKey]; -- Msg
  mInReplyToIs← DeclareAttribute[mInReplyTo, "is", RopeType]; -- Msg
 mTOCEntry← DeclareRelation["mTOCEntry", WalnutLog.currentSegment];
  mTOCEntryOf← DeclareAttribute[mTOCEntry, "of", MsgDomain, Key]; -- Msg
  mTOCEntryIs← DeclareAttribute[mTOCEntry, "is", RopeType]; -- string

 mLogPos← DeclareRelation["mLogPos", WalnutLog.currentSegment];
  mLogPosOf← DeclareAttribute[mLogPos, "of", MsgDomain, Key]; -- Msg
  mPrefixPos← DeclareAttribute[mLogPos, "prefix", IntType]; -- int
  mHeadersPos← DeclareAttribute[mLogPos, "headers", IntType]; -- int
 mMsgLength← DeclareRelation["mMsgLength", WalnutLog.currentSegment];
mMsgLengthOf← DeclareAttribute[mMsgLength, "of", MsgDomain, Key]; -- Msg
  mMsgLengthIs← DeclareAttribute[mMsgLength, "length", IntType]; -- int
 mHasBeenRead← DeclareRelation["mHasBeenRead", WalnutLog.currentSegment];
mHasBeenReadOf← DeclareAttribute[mHasBeenRead, "of", MsgDomain, Key]; -- Msg
  mHasBeenReadIs← DeclareAttribute[mHasBeenRead, "is", BoolType]; -- bool

 activeMsgSet← DeclareEntity[MsgSetDomain, "Active"];  -- NewOrOld
 deletedMsgSet← DeclareEntity[MsgSetDomain, "Deleted"];
END;

TiogaMsgFromLog: PUBLIC PROC[msg: Msg]
  RETURNS[contents: TiogaContents, startPos, length: INT] =
-- reads tioga text for msg from log

BEGIN
 startPos← V2I[GetP[msg, mHeadersPos]];
 length← V2I[GetP[msg, mMsgLengthIs]];
 contents← WalnutLog.TiogaTextFromLog[startPos, length];
END;

NumInMsgSet: PUBLIC ENTRY PROC[msgSet: MsgSet] RETURNS[INT] =
{ ENABLE UNWIND => NULL; RETURN[V2I[GetP[msgSet, msNumInSetIs]]] };

ArchiveMsgSet: PUBLIC ENTRY PROC[msgSet: MsgSet, strm: IO.STREAM, doDelete: BOOL]
  RETURNS[newRelList: LIST OF Relship] =
-- needs to be entirely inside the DB monitor
-- if doDelete is TRUE, relturns list of newRels for msgs added to Deleted
BEGIN ENABLE UNWIND => NULL;
 rel, newRel: Relship;
 endRL: LIST OF Relship← NIL;
 rs: RelshipSet← RelationSubset[mCategory, LIST[[mCategoryIs, msgSet]]];
 restOfPrefix: ROPE← Rope.Cat["\nCategories: ", DB.GetName[msgSet], "\n"];
UNTIL DB.Null[rel← NextRelship[rs]] DO
  msg: Msg← V2E[GetF[rel, mCategoryOf]];
  startPos: INT← V2I[GetP[msg, mHeadersPos]];
  length: INT← V2I[GetP[msg, mMsgLengthIs]];
  fullText: ROPE← WalnutLog.RopeFromLog[startPos, length];
  prefix: ROPE← Rope.Cat[WalnutLog.msgIDRope, ": ", DB.GetName[msg], restOfPrefix];
  []← WalnutStream.MakeLogEntry[strm, message, fullText, prefix];
IF doDelete THEN
  { newRel← RemoveFrom[msg, msgSet, rel];
    IF newRelList = NIL THEN newRelList← endRL← CONS[newRel, NIL]
    ELSE endRL← endRL.rest← CONS[newRel, NIL];
   };
ENDLOOP;
 ReleaseRelshipSet[rs];
END;

NameToEntity: PUBLIC ENTRY PROC[d: Domain, name: ROPE, version: Version]
  RETURNS[e: Entity] =
BEGIN ENABLE UNWIND => NULL;
 e← DeclareEntity[d, name, OldOnly];
IF e=NIL AND version=NewOrOld THEN e← DeclareEntity[d, name, NewOnly];
END;

GetEntitiesInDomain: PUBLIC ENTRY PROC[d: Domain, alphaOrder: BOOL]
  RETURNS[eL: LIST OF Entity] =
-- returns list of entities in given domain
BEGIN ENABLE UNWIND => NULL;
 eLend: LIST OF Entity;
 es: EntitySet← IF alphaOrder THEN DomainSubset[d, "", "\177"] ELSE DomainSubset[d];
 e: Entity← NextEntity[es];
IF e=NIL THEN RETURN[NIL];
 eL← eLend← CONS[e, NIL];
UNTIL DB.Null[e← NextEntity[es]] DO eLend← eLend.rest← CONS[e, NIL]; ENDLOOP;
 ReleaseEntitySet[es];
END;

RelationSubsetList: PUBLIC ENTRY PROC[r: Relation, constraint: AttributeValueList← NIL]
  RETURNS[relList: LIST OF Relship] =
BEGIN ENABLE UNWIND => NULL;
 rs: RelshipSet← RelationSubset[r, constraint];
 rel: Relship← NextRelship[rs];
 rLend: LIST OF Relship;
IF rel = NIL THEN RETURN;
 relList← rLend← CONS[rel, NIL];
UNTIL DB.Null[rel← NextRelship[rs]] DO rLend← rLend.rest← CONS[rel, NIL]; ENDLOOP;
 ReleaseRelshipSet[rs];
END;

GetFE: PUBLIC ENTRY PROC[rel: Relship, a: Attribute] RETURNS [Entity] =
{ ENABLE UNWIND => NULL; RETURN[V2E[DB.GetF[rel, a]]] };

GetName: PUBLIC ENTRY PROC[e: Entity] RETURNS [ROPE] =
 { ENABLE UNWIND => NULL; RETURN[DB.GetName[e]] };

Null: PUBLIC ENTRY PROC[e: Entity] RETURNS [BOOL] =
 { ENABLE UNWIND => NULL; RETURN[DB.Null[e]] };

MGetP: PUBLIC ENTRY PROC[m: Msg, prop: Attribute] RETURNS [Value] =
-- Finds the one unique value for prop for m or NIL if none.
-- SIGNALs NonUniquePropertyValue if more than one value.
 { ENABLE UNWIND => NULL; RETURN[DB.GetP[m, prop]] };

MSetPList: PUBLIC ENTRY PROC[m: Msg, prop: Attribute, vl: LIST OF Value] =
-- Sets the values of prop for m to be those in vl.
-- Erases any previous property values.
BEGIN ENABLE UNWIND => NULL;
DB.SetPList[m, prop, vl];
 SetUpdatesPendingProc[TRUE];
END;

MSetP: PUBLIC ENTRY PROC[m: Msg, prop: Attribute, v: Value] RETURNS [rel: Relship] =
-- Sets the value of prop for m to be v.
-- Erases any previous property value(s).
-- Returns the relship used by the prop.
BEGIN ENABLE UNWIND => NULL;
 rel← DB.SetP[m, prop, v];
 SetUpdatesPendingProc[TRUE];
END;

AcquireDBLock: PUBLIC ENTRY PROC[procToCall: PROC] =
-- to allow extended use of lock on database
BEGIN ENABLE UNWIND => NULL;
 procToCall[];
END;

-- * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *


RegisterUpdatesPendingProc: PUBLIC ENTRY PROC[proc: PROC[BOOL]] =
 { ENABLE UNWIND => NULL; SetUpdatesPendingProc← proc};

UnRegisterUpdatesPendingProc: PUBLIC ENTRY PROC[proc: PROC[BOOL]] =
 { ENABLE UNWIND => NULL;
  IF
SetUpdatesPendingProc = proc THEN SetUpdatesPendingProc← DefaultUpdateProc
  };

DefaultUpdateProc: PROC[BOOL] = { NULL };

-- * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *


MsgRecToMsg
: PUBLIC PROC[mr: WalnutLog.MsgRec]
RETURNS[msg: Msg, mExisted: BOOL,
    
newMsgSetList: LIST OF MsgSet, newRelList: LIST OF RelshipMsgSetPair] =
-- Converts msg from record to entity form
BEGIN
CategoryRelshipList: PROC[msgSetName: ROPE] =
BEGIN
  msgSet: MsgSet;
  msExisted, rExisted: BOOL;
  rel: Relship;

  [msgSet, msExisted]← DeclareMsgSet[msgSetName, NewOrOld];
  IF ~msExisted THEN newMsgSetList← CONS[msgSet, newMsgSetList];
  [rel, rExisted]← AddToMsgSet[msg, msgSet];
  IF ~rExisted THEN newRelList← CONS[[rel, msgSet], newRelList];
END;

DO
 [msg, mExisted]← DeclareMsg[mr.gvID, NewOrOld];
IF ~mExisted THEN
BEGIN
-- make up TOC entry
  pos: INT← mr.gvID.Find["$"]+1;  -- $ is not allowed in names, so it's unique
  date: ROPE← GetTocDate[mr];
  toc: ROPEIF mr.from.Equal[WalnutSendOps.userRName, FALSE] OR
   mr.from.Equal[WalnutSendOps.simpleUserName, FALSE]
    THEN Rope.Concat["To: ", mr.to]
    ELSE mr.from;
  mr.tocEntry← Rope.Cat[date, " ", toc];
  SetMsgAttributes[msg, mr];
  IF mr.voiceID.Length[] # 0 THEN WalnutVoice.MakeInterestEntry[mr.voiceID, mr.gvID];
  IF mr.categoriesList = NIL THEN CategoryRelshipList["Active"] -- add to msgSets
   ELSE
   FOR cL: LIST OF ROPE← mr.categoriesList, cL.rest UNTIL cL = NIL DO
   CategoryRelshipList[cL.first] ENDLOOP;
RETURN
END  -- new msg
  ELSE-- see if any new MsgSets were on categoriesList
BEGIN
-- check if this really is an old msg - only check msgLength & subject fields
  len: INT← V2I[GetP[msg, mMsgLengthIs]];
  subj: ROPE;
  curCatList: LIST OF Value;
  found: BOOLFALSE;
IF len # mr.msgLength THEN { WalnutLog.GenerateNewMsgID[mr]; LOOP};
  subj← V2S[GetP[msg, mSubjectIs]];
IF ~Rope.Equal[subj, mr.subject] THEN { WalnutLog.GenerateNewMsgID[mr]; LOOP};
curCatList← GetPList[msg, mCategoryIs];
FOR newL: LIST OF ROPE← mr.categoriesList, newL.rest UNTIL newL = NIL DO
FOR cL: LIST OF Value← curCatList, cL.rest UNTIL cL = NIL DO
IF Rope.Equal[newL.first, GetName[V2E[cL.first]], FALSE] THEN
{ found← TRUE; EXIT};
ENDLOOP;
IF ~found THEN CategoryRelshipList[newL.first];
ENDLOOP;
RETURN;  -- old msg
END;
ENDLOOP;  -- in case need to change mr.gvID
END;

GetTocDate: PROC[mr: WalnutLog.MsgRec] RETURNS[date: ROPE] =
BEGIN
 tyme: BasicTime.GMT;
IF mr.date = NIL THEN mr.date← WalnutSendOps.RFC822Date[];
BEGIN  -- get date into canonical form
  ris: IO.STREAMIO.RIS[mr.date];
  tyme← IO.GetTime[ris ! IO.Error => GOTO badTime];
EXITS
  badTime => tyme← BasicTime.Now[];
END;
 date← Rope.Substr[WalnutSendOps.RFC822Date[tyme], 0, 9]; -- only want first 9 chars of date
DO
  i: INT← date.Find["-"];
IF i < 0 THEN RETURN;
  date← date.Replace[i, 1, " "];
ENDLOOP;
END;

AddToMsgSet: ENTRY PROC[msg: Msg, msgSet: MsgSet] RETURNS[rel: Relship, existed: BOOL] =
{ ENABLE UNWIND => NULL;
[rel, existed]← AddTo[msg, msgSet]
};

SetMsgAttributes: ENTRY PROC[msg: Msg, mr: WalnutLog.MsgRec] =
BEGIN ENABLE UNWIND => NULL;
 []← SetP[msg, mDateCodeIs, T2V[LOOPHOLE[mr.dateCode, DB.GMT]]];
 []← SetP[msg, mInReplyToIs, S2V[mr.inReplyTo]];
 []← SetP[msg, mHasBeenReadIs, B2V[mr.hasBeenRead]];
 []← SetP[msg, mTOCEntryIs, S2V[mr.tocEntry]];
 []← SetP[msg, mSubjectIs, S2V[mr.subject]];
 []← SetP[msg, mPrefixPos, I2V[mr.prefixPos]];
 []← SetP[msg, mMsgLengthIs, I2V[mr.msgLength]];
 []← SetP[msg, mHeadersPos, I2V[mr.headersPos]];
END;

-- * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *

AddTo: INTERNAL PROC[msg: Msg, msgSet: MsgSet] RETURNS[rel: Relship, existed: BOOL] =
BEGIN
 avl: AttributeValueList;

 avl← LIST[[mCategoryOf, msg], [mCategoryIs, msgSet],
   [mCategoryDate, GetP[msg, mDateCodeIs]]];
 rel← DeclareRelship[mCategory, avl, OldOnly];
IF existed← (rel # NIL) THEN RETURN;  -- msg is already in this msgSet
 rel← CreateRelship[mCategory, avl];
 ChangeNumInSet[msgSet, 1];
 SetUpdatesPendingProc[TRUE];
 WalnutVoice.VoiceMoveTo[DB.GetName[msgSet], DB.GetName[msg]];
END;

RemoveFrom: INTERNAL PROC[msg: Msg, msgSet: MsgSet, rel: Relship]
  RETURNS[newRel: Relship] =
-- rel can be NIL, in which case it is found from the database
-- does nothing if this Relship doesn't exist (no ERROR generated)

BEGIN
IF rel = NIL THEN
{ rs: RelshipSet← RelationSubset[mCategory, LIST[[mCategoryOf, msg]]];
UNTIL DB.Null[rel← NextRelship[rs]] DO
IF Eq[msgSet, V2E[GetF[rel, mCategoryIs]]] THEN EXIT;
ENDLOOP;
  ReleaseRelshipSet[rs];
  };
IF rel = NIL THEN RETURN;  -- ignore this call
 DestroyRelship[rel];  -- MEraseP[msg, mCategoryIs, msgSet];
 ChangeNumInSet[msgSet, -1];
IF GetPList[msg, mCategoryIs] = NIL THEN newRel← AddTo[msg, deletedMsgSet].rel;
 SetUpdatesPendingProc[TRUE];
END;

ChangeNumInSet: INTERNAL PROC[msgSet: MsgSet, inc: INT] = INLINE
BEGIN
 num: INT← V2I[GetP[msgSet, msNumInSetIs]] + inc;
IF (inc < 0) AND (num = -1) THEN SIGNAL NumInNegative[DB.GetName[msgSet]];
 []← SetP[msgSet, msNumInSetIs, I2V[num]];
END;

-- * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *

-- debugging procs
notExist: ROPE = "MsgSet: %g doesn't exist";
msNumIn: ROPE = "msNumInSetIs for MsgSet: %g is %g messages";
enumNumIn: ROPE = "Enumeration of MsgSet: %g says %g messages";

NumIn: PROC[msName: ROPE] RETURNS[ans: ROPE] =
BEGIN
 msgSet: MsgSet← DeclareEntity[MsgSetDomain, msName, OldOnly];
 num: INT;
IF msgSet = NIL THEN RETURN[IO.PutFR[notExist, IO.rope[msName]]];
 num← V2I[GetP[msgSet, msNumInSetIs]];
RETURN[IO.PutFR[msNumIn, IO.rope[msName], IO.int[num]]];
END;

Enum: PROC[msName: ROPE] RETURNS[ans: ROPE] =
BEGIN
 msgSet: MsgSet← DeclareEntity[MsgSetDomain, msName, OldOnly];
 num: INT← 0;
 rel: Relship;
 rs: RelshipSet;
IF msgSet = NIL THEN RETURN[IO.PutFR[notExist, IO.rope[msName]]];
 rs← RelationSubset[mCategory, LIST[[mCategoryIs, msgSet]]];
UNTIL DB.Null[rel← NextRelship[rs]] DO num← num + 1; ENDLOOP;
 ReleaseRelshipSet[rs];
RETURN[IO.PutFR[enumNumIn, IO.rope[msName], IO.int[num]]];
END;

SetNumIn: PROC[msName: ROPE, num: INT] RETURNS[ans: ROPE] =
BEGIN
 msgSet: MsgSet← DeclareEntity[MsgSetDomain, msName, OldOnly];
IF msgSet = NIL THEN RETURN[IO.PutFR[notExist, IO.rope[msName]]];
 []← SetP[msgSet, msNumInSetIs, I2V[num]];
RETURN[IO.PutFR["msNumInSetIs for MsgSet: %g has been set to %g",
   IO.rope[msName], IO.int[num]]];
END;

CheckAll: PROC RETURNS[ans: ROPE] =
BEGIN
 rs: RelshipSet;
 rel: Relship;
 num, enum: INT;
 msgSet: Entity;
 eL: LIST OF Entity← GetEntitiesInDomain[d: MsgSetDomain, alphaOrder: TRUE];
FOR elT: LIST OF Entity← eL, elT.rest UNTIL elT=NIL DO
msgSet← elT.first;
num← V2I[GetP[msgSet, msNumInSetIs]];
enum← 0;
rs← RelationSubset[mCategory, LIST[[mCategoryIs, msgSet]]];
  UNTIL DB.Null[rel← NextRelship[rs]] DO enum← enum + 1; ENDLOOP;
  ReleaseRelshipSet[rs];
IF num # enum THEN ans← Rope.Cat[ans, " ", GetName[msgSet]];
ENDLOOP;
IF ans.Length[] = 0 THEN RETURN["All msgsets are ok"];
END;

DateCode: PROC[mName: ROPE] RETURNS[ans: ROPE] =
BEGIN
 msg: Msg← DeclareEntity[MsgDomain, mName, OldOnly];
 date: BasicTime.GMT← V2T[GetP[msg, mDateCodeIs]];
 dr: ROPE← WalnutSendOps.RFC822Date[date];

IF msg = NIL THEN RETURN[IO.PutFR["Msg: %g doesn't exist", IO.rope[mName]]];
 date← V2T[GetP[msg, mDateCodeIs]];
RETURN[IO.PutFR["Msg: %g has DateCode: %g", IO.rope[mName], IO.rope[dr]]];
END;

GetMsgAttributes: PROC[name: ROPE] RETURNS[mr: WalnutLog.MsgRec] =
BEGIN
 msg: Msg← DeclareEntity[MsgDomain, name, OldOnly];
IF DB.Null[msg] THEN RETURN[NIL];

 mr← NEW[WalnutLog.MessageRecObject];
 mr.dateCode← V2T[GetP[msg, mDateCodeIs]];
 mr.inReplyTo← V2S[GetP[msg, mInReplyToIs]];
 mr.hasBeenRead← V2B[GetP[msg, mHasBeenReadIs]];
 mr.tocEntry← V2S[GetP[msg, mTOCEntryIs]];
 mr.subject← V2S[GetP[msg, mSubjectIs]];
 mr.prefixPos← V2I[GetP[msg, mPrefixPos]];
 mr.msgLength← V2I[GetP[msg, mMsgLengthIs]];
 mr.headersPos← V2I[GetP[msg, mHeadersPos]];
END;

END.