<> <> <> <> <> <> <> DIRECTORY Atom, Basics USING [LowHalf], DBEnvironment, DBStorage, DBTuplesConcrete, DB, DBModel, DBModelPrivate, Rope; DBModelGlobalImpl: CEDAR PROGRAM IMPORTS Atom, DBStorage, DBModel, DB, DBModelPrivate, Basics, Rope EXPORTS DB, DBModel, DBEnvironment = BEGIN OPEN DB, DBModelPrivate, DBModel; <> initialized: BOOL_ FALSE; surrogatesEnabled: BOOL_ TRUE; <> TupleObject: PUBLIC TYPE = DBTuplesConcrete.TupleObject; EntityObject: PUBLIC TYPE = TupleObject; RelshipObject: PUBLIC TYPE = TupleObject; EntitySetObject: PUBLIC TYPE = DBTuplesConcrete.EntitySetObject; RelshipSetObject: PUBLIC TYPE = DBTuplesConcrete.RelshipSetObject; <> Tuple, TupleSet, Index, IndexFactor: TYPE = REF TupleObject; Domain, Relation, Entity, Attribute, DataType: PUBLIC TYPE = REF EntityObject; Relship: PUBLIC TYPE = REF RelshipObject; SystemATuple: TYPE = REF attribute TupleObject; SystemTSTuple: TYPE = REF tupleSet TupleObject; EntitySet: PUBLIC TYPE = REF EntitySetObject; RelshipSet: PUBLIC TYPE = REF RelshipSetObject; T2SAT: PUBLIC PROCEDURE[t: Tuple] RETURNS [SystemATuple] = { RETURN [NARROW[t]] }; T2ST: PUBLIC PROCEDURE[t: Tuple] RETURNS [SystemTSTuple] = -- INLINE -- { RETURN [NARROW[t]] }; T2STT: PUBLIC PROCEDURE[t: Tuple] RETURNS [SystemTSTuple] = -- INLINE -- { RETURN [NARROW[t]] }; <> QInitialize: PUBLIC PROC[ nCachePages: NAT, nFreeTuples: NAT, cacheFileName: ROPE] = { <> <> IF initialized THEN RETURN; initialized_ TRUE; IF cacheFileName=NIL THEN cacheFileName_ "DBSegment.VM"; DBStorage.Initialize[nCachePages, nFreeTuples, cacheFileName]; InitializeSystemTuples[]; }; <> QOpenTransaction: PUBLIC PROC[ segment: Segment, useTrans: Transaction_ NIL] = { IF NOT initialized THEN ERROR Error[DatabaseNotInitialized]; DBStorage.OpenTransaction[segment, useTrans, FALSE]; }; QMarkTransaction: PUBLIC PROC[trans: Transaction] = { IF trans#NIL THEN DBStorage.FinishTransaction[trans, FALSE, TRUE]}; QAbortTransaction: PUBLIC PROC[trans: Transaction] = { FlushCaches[]; IF trans#NIL THEN DBStorage.FinishTransaction[trans, TRUE, FALSE] }; QCloseTransaction: PUBLIC PROC[trans: Transaction] = { <> FlushCaches[]; IF trans#NIL THEN DBStorage.FinishTransaction[trans, FALSE, FALSE]; }; <> QFlushCache: PUBLIC PROC[trans: Transaction] = { IF trans#NIL THEN DBStorage.FlushTransaction[trans, FALSE, TRUE]}; QAbortCache: PUBLIC PROC[trans: Transaction] = { FlushCaches[]; IF trans#NIL THEN DBStorage.FlushTransaction[trans, TRUE, FALSE] }; QEndTransaction: PUBLIC PROC[trans: Transaction] = { <> FlushCaches[]; IF trans#NIL THEN DBStorage.FlushTransaction[trans, FALSE, FALSE]; }; <> QDeclareSegment: PUBLIC PROC[ filePath: ROPE, segment: Segment, number: SegmentIndex _ 0, readonly: BOOL_ FALSE, createIfNotFound: BOOL_ TRUE, nPagesInitial, nPagesPerExtent: NAT ] = { <> <> <> fileVersion: DBStorage.VersionOptions_ IF createIfNotFound THEN None ELSE OldFileOnly; IF readonly AND createIfNotFound THEN ERROR Error[WriteNotAllowed]; IF NOT initialized THEN ERROR Error[DatabaseNotInitialized]; IF segment=NIL THEN segment_ Atom.MakeAtom[ParseSegmentName[filePath].name]; IF number=0 THEN number_ MapSegmentToNumber[segment]; DBStorage.AttachSegment[ filePath, segment, number, readonly, fileVersion, FALSE, nPagesInitial, nPagesPerExtent]; }; QEraseSegment: PUBLIC PROC[segment: Segment, useTrans: Transaction] = { <> filePath: ROPE; number: SegmentIndex; readOnly: BOOL; trans: Transaction; nPagesInitial, nPagesPerExtent: INT; [filePath, number, trans, readOnly, nPagesInitial, nPagesPerExtent]_ DBStorage.GetSegmentInfo[segment]; QCloseTransaction[trans]; -- does nothing if no transaction open DBStorage.AttachSegment[ filePath, segment, number, readOnly, None, TRUE, nPagesInitial, nPagesPerExtent]; QOpenTransaction[segment: segment, useTrans: useTrans]; QMarkTransaction[DBStorage.GetSegmentInfo[segment].trans]; -- useTrans might be NIL }; QGetSegmentInfo: PUBLIC PROC[segment: Segment] RETURNS [ filePath: ROPE, number: SegmentIndex, trans: Transaction, readOnly: BOOL] = { CheckNIL[segment]; [filePath, number, trans, readOnly, , ] _ DBStorage.GetSegmentInfo[segment]; IF filePath = NIL THEN number _ MapSegmentToNumber[segment ! Error => CONTINUE]; RETURN[filePath, number, trans, readOnly] }; QGetSegments: PUBLIC PROC RETURNS [sl: LIST OF Segment] = { addToList: PROC [s: Segment, segmentIndex: DBStorage.SegmentIndex]RETURNS [stop: BOOL] = {sl_ CONS[s, sl]; RETURN[FALSE]}; sl_ NIL; DBStorage.EnumerateSegments[addToList]; RETURN[sl]; }; QGetBuiltinSegments: PUBLIC PROC RETURNS[LIST OF Segment] = { <> sai: LIST OF Segment _ CONS[$Squirrel, NIL]; -- known start last: LIST OF Segment _ sai; FOR sL: LIST OF SegmentAndIndex _ mapSegmentToNumberList.rest, sL.rest UNTIL sL=NIL DO last.rest _ CONS[sL.first.segment, NIL]; last _ last.rest; ENDLOOP; RETURN[sai]; }; MapSegmentToNumber: PROC [segment: Segment] RETURNS [NAT] = { FOR mL: LIST OF SegmentAndIndex _ mapSegmentToNumberList, mL.rest UNTIL mL=NIL DO IF segment = mL.first.segment THEN RETURN[mL.first.index]; ENDLOOP; ERROR Error[CannotDefaultSegment]; -- seg number not given & can't guess }; SegmentAndIndex: TYPE = RECORD[segment: Segment, index: SegmentIndex]; mapSegmentToNumberList: LIST OF SegmentAndIndex = LIST[ [$Squirrel, 100B], [$Foo, 101B], [$Icons, 140B], [$Walnut, 200B], [$Hickory, 210B], [$Grapenut, 220B], [$Coconut, 230B], [$Help, 240B], [$Nuthatch, 250B], [$Finger, 260B], [$Test, 300B], [$Whiteboard, 310B], [$Tool, 320B], [$WalnutSortDef, 330B], -- reserve [330B .. 350B) for Pasadena [$Chestnut, 400B] ]; <> QDeclareDomain: PUBLIC PROC [ name: ROPE, segment: Segment, version: Version_ NewOrOld, estRelships: INT_ 5] RETURNS [d: Domain] = { <> IF NOT initialized THEN ERROR Error[DatabaseNotInitialized]; IF name=NIL THEN name_ ""; IF name.Length[]#0 THEN BEGIN d_ QFetchEntity[DomainDomain, name, segment]; IF d#NIL THEN IF version=NewOnly THEN ERROR Error[AlreadyExists] ELSE RETURN[d]; IF version=OldOnly THEN RETURN[NIL]; END; <> d_ DBStorage.CreateSystemPageTuple[T2ST[DomainDomain].vTuple, NIL, segment]; DBStorage.CreateTupleset[d, Basics.LowHalf[estRelships]]; SetValFromHandle[d, tupleSetNameHandle, RopeType, Unlinked, name]; DBStorage.InsertIntoIndex[GetDomainIndex[segment], ConvertToUpper[name], d]; <> []_ DBStorage.AddField[d, MakeFD[RopeType, DefaultNameLength]]; []_ SafeSetP[d, dIndexProp, CreateTupleSetIndex[d]]; DBModelPrivate.FlushCaches[]; }; QDeclareRelation: PUBLIC PROC[ name: ROPE, segment: Segment, version: Version_ NewOrOld] RETURNS [r: Relation] = { <> IF NOT initialized THEN ERROR Error[DatabaseNotInitialized]; IF name=NIL THEN name_ ""; IF name.Length[]#0 THEN BEGIN r_ QFetchEntity[RelationDomain, name, segment]; IF r#NIL THEN IF version=NewOnly THEN ERROR Error[AlreadyExists] ELSE RETURN[r]; IF version=OldOnly THEN RETURN[NIL]; END; <> r_ DBStorage.CreateSystemPageTuple[T2ST[RelationDomain].vTuple, NIL, segment]; DBStorage.CreateTupleset[r, 0 -- NO refs to Relships --]; []_ SafeSetP[r, r1to1Prop, B2V[FALSE]]; SetValFromHandle[r, tupleSetNameHandle, RopeType, Unlinked, name]; DBStorage.InsertIntoIndex[GetRelationIndex[segment], ConvertToUpper[name], r]; DBModelPrivate.FlushCaches[]; }; QDeclareAttribute: PUBLIC PROC [ r: Relation, name: ROPE, type: DataType_ NIL, uniqueness: Uniqueness _ None, length: INT_ 0, link: LinkType_ Linked, version: Version_ NewOrOld] RETURNS[a: Attribute] = { <> <> NameEqual: PROC [e: Entity] RETURNS[BOOL] = {RETURN[name.Equal[QNameOf[e], FALSE]]}; fh: DBStorage.FieldHandle; attCount: CARDINAL; IF r=NIL THEN RETURN[NIL]; CheckRelation[r]; IF link=Colocated THEN ERROR Error[NotImplemented]; a_ SearchEntityList[VL2EL[QGetPList[r, aRelationOf]], NameEqual]; IF a#NIL THEN IF version=NewOnly THEN ERROR Error[AlreadyExists] ELSE BEGIN -- Check that existing attribute of proper type. IF type#NIL AND NOT Eq[V2E[QGetP[a, aTypeIs]], type] THEN ERROR Error[MismatchedExistingAttribute]; RETURN[a] END; <> IF version = OldOnly THEN RETURN[NIL]; IF type=NIL THEN ERROR Error[NILArgument]; -- Must not default type, if new! a_ DBStorage.CreateSystemPageTuple[T2ST[AttributeDomain].vTuple, r]; QChangeName[a, name]; DBModelPrivate.SetTypeAndLink[a, type, link]; []_ SafeSetP[a, aRelationIs, r]; []_ SafeSetP[a, aLengthIs, I2V[length]]; []_ SafeSetP[a, aUniquenessIs, U2V[uniqueness] ]; <> attCount_ NumberOfAttributes[r]; <> <<(1) this is the first attribute of r>> <<(2) it is a single-attribute primary key>> <<(3) it is an entity-valued attribute but not of type AnyDomainType>> <<(4) the domain it references is empty (has not entities yet)>> <<(5) the domain it refs has no subdomains (else they'd need the attr too)>> <> <> <> IF attCount=1 AND uniqueness=Key AND surrogatesEnabled THEN SELECT type FROM AnyDomainType, RopeType, BoolType, IntType => NULL; ENDCASE => IF NOT Eq[QDomainOf[type], DomainDomain] THEN ERROR Error[IllegalValueType] <> ELSE IF EmptyDomain[type] AND GetCachedDomainInfo[type].subDomains=NIL THEN BEGIN -- this is it! we can do the surrogate relation optimization! []_ SafeSetP[r, r1to1Prop, B2V[TRUE]]; []_ SafeSetP[a, aDomainIs, type]; RETURN END; IF V2B[SafeGetP[r, r1to1Prop]] THEN <1:>> BEGIN storageDomain: Domain_ V2E[SafeGetP[GetFirstAttribute[r], aTypeIs]]; fh_ DBStorage.AddField[storageDomain, MakeFD[type, length, link=Linked, a]]; []_ SafeSetP[a, aHandleProp, fh]; END ELSE -- we are defining an attribute of a normal relation: BEGIN fh_ DBStorage.AddField[r, MakeFD[type, length, link=Linked, a]]; []_ SafeSetP[a, aHandleProp, fh]; END; DBModelPrivate.FlushCaches[]; }; QDeclareSubType: PUBLIC PROC [of, is: Domain] = { <> <> st: Relship; rs: RelshipSet_ QRelationSubset[dSubType, LIST[ [dSubTypeIs, is], [dSubTypeOf, of] ] ]; theseBetterNotIncludeSuper: LIST OF Domain_ GetCachedDomainInfo[is].subDomains; segment: Segment_ QSegmentOf[of]; FOR dlT: LIST OF Domain_ theseBetterNotIncludeSuper, dlT.rest UNTIL dlT=NIL DO IF Eq[dlT.first, of] THEN ERROR Error[IllegalSuperType] -- super is already of a subdomain! ENDLOOP; IF QNextRelship[rs]#NIL THEN <> {QReleaseRelshipSet[rs]; RETURN}; QReleaseRelshipSet[rs]; IF NOT EmptyDomain[of] THEN <> ERROR Error[NotImplemented]; -- But client can resume if knows no surrogates <> st_ DBStorage.CreateSystemPageTuple[T2ST[T2CT[dSubType]].vTuple, NIL, segment]; SafeSetF[st, dSubTypeIs, is]; SafeSetF[st, dSubTypeOf, of]; }; QDeclareIndex: PUBLIC PROC[ r: Relation, order: AttributeList, version: Version] RETURNS[i: Index] = { <> <> <> <> <> count: INT; if: IndexFactor; ifs: LIST OF IndexFactor; <> <> ifs_ VL2EL[QGetPList[order.first, ifAttributeOf]]; FOR ifsT: LIST OF IndexFactor_ ifs, ifsT.rest UNTIL ifsT=NIL DO i_ V2E[QGetP[ifsT.first, ifIndexIs]]; IF SameIndex[i, order] THEN IF version=NewOnly THEN ERROR Error[AlreadyExists] ELSE RETURN[i]; REPEAT FINISHED => IF version=OldOnly THEN RETURN[NIL]; ENDLOOP; <> IF NOT EmptyRelation[r] THEN ERROR Error[NotImplemented]; i_ CreateTupleSetIndex[r]; count_ 0; FOR orderL: AttributeList_ order, orderL.rest UNTIL orderL=NIL DO if_ DBStorage.CreateSystemPageTuple[T2STT[IndexFactorDomain].vTuple, r]; []_ SafeSetP[if, ifIndexIs, i]; -- Each factor points to its index... []_ SafeSetP[if, ifOrdinalPositionIs, NEW[INT_ count]]; -- ... gives its pos'n in the order []_ SafeSetP[if, ifAttributeIs, orderL.first]; -- .. and the attribute it refers to count_ count+1; ENDLOOP; DBModelPrivate.FlushCaches[]; }; CreateTupleSetIndex: PUBLIC PROC[ts: TupleSet] RETURNS [i: Index] = { <> i_ DBStorage.CreateSystemPageTuple[T2STT[IndexDomain].vTuple, ts]; DBStorage.CreateIndex[i]; -- storage level sets iHandleProp }; QDeclareProperty: PUBLIC PROC [ relationName: ROPE, of: Domain, is: DataType, segment: Segment, uniqueness: Uniqueness_ None, version: Version_ NewOrOld] RETURNS [aIs: Attribute] = { r: Relation_ QDeclareRelation[relationName, segment, version]; rOf: Attribute_ QDeclareAttribute[r, "of", of, Key, 0, Linked, version]; rIs: Attribute_ QDeclareAttribute[r, "is", is, uniqueness, 0, Linked, version]; RETURN[rIs]; }; QDestroyDomain: PUBLIC PROC [d: Domain] = { <> <> <> e: Entity; r: Relship; es: EntitySet_ QDomainSubset[d]; -- all entities al: LIST OF Attribute_ VL2EL[QGetPList[d, aTypeOf] ]; -- reffing attr's supers: RelshipSet_ QRelationSubset[dSubType, LIST[[dSubTypeIs, d]] ]; -- supers dIndex: Index; subDomains: LIST OF Domain; [nameIndex: dIndex, subDomains: subDomains]_ GetCachedDomainInfo[d]; IF subDomains#NIL THEN ERROR Error[NotImplemented]; -- must destroy the domain's subdomains first! DBStorage.DeleteFromIndex[ GetDomainIndex[QSegmentOf[d]], ConvertToUpper[QNameOf[d]], d]; WHILE (r_ QNextRelship[supers])#NIL DO <> SafeSetF[r, dSubTypeOf, NIL]; SafeSetF[r, dSubTypeIs, NIL]; DBStorage.DestroyTuple[r]; ENDLOOP; QReleaseRelshipSet[supers]; WHILE (e_ QNextEntity[es])#NIL DO QDestroyEntity[e] ENDLOOP; QReleaseEntitySet[es]; FOR alT: LIST OF Attribute_ al, alT.rest UNTIL alT=NIL DO IF NOT Null[alT.first] THEN -- this check needed 'cause two attrs of reln may ref d {reln: Relation_ V2E[SafeGetP[alT.first, aRelationIs]]; QDestroyRelation[reln]}; ENDLOOP; []_ SafeSetP[d, dIndexProp, NIL]; QDestroyIndex[dIndex]; DestroyDictionaryEntity[d]; -- destroys any user links to d, all ours should be gone }; QDestroyRelation: PUBLIC PROC [r: Relation] = { <> rel: Relship; rs: RelshipSet_ QRelationSubset[r, NIL]; al: LIST OF Attribute_ VL2EL[QGetPList[r, aRelationOf] ]; -- R's attributes WHILE (rel_ QNextRelship[rs])#NIL DO QDestroyRelship[rel] ENDLOOP; -- destroy relships QReleaseRelshipSet[rs]; FOR alT: LIST OF Attribute_ al, alT.rest UNTIL alT=NIL DO <> []_ SafeSetP[alT.first, aRelationIs, NIL]; []_ SafeSetP[alT.first, aTypeEntityProp, NIL]; []_ SafeSetP[alT.first, aDomainIs, NIL]; DBStorage.DestroyTuple[alT.first]; ENDLOOP; DBStorage.DeleteFromIndex[ GetRelationIndex[QSegmentOf[r]], ConvertToUpper[QNameOf[r]], r]; DBModelPrivate.FlushCaches[]; DestroyDictionaryEntity[r]; }; QDestroyAttribute: PUBLIC PROC[a: Attribute] = { ERROR Error[NotImplemented] }; QDestroySubType: PUBLIC PROC [of, is: Domain] = { <> <> <> st: Relship; rs: RelshipSet_ QRelationSubset[dSubType, LIST[ [dSubTypeIs, is], [dSubTypeOf, of] ] ]; IF (st_ QNextRelship[rs])=NIL THEN <> {QReleaseRelshipSet[rs]; RETURN}; QReleaseRelshipSet[rs]; SafeSetF[st, dSubTypeOf, NIL]; SafeSetF[st, dSubTypeIs, NIL]; DBStorage.DestroyTuple[st]; }; QDestroyIndex: PUBLIC PROC[i: Index] = { <> <> ifs: LIST OF IndexFactor_ VL2EL[QGetPList[i, ifIndexOf]]; DBStorage.DestroyIndex[i]; FOR ifsT: LIST OF IndexFactor_ ifs, ifsT.rest UNTIL ifsT=NIL DO []_ SafeSetP[ifsT.first, ifIndexIs, NIL]; DBStorage.DestroyTuple[ifsT.first] ENDLOOP; DBModelPrivate.FlushCaches[]; DestroyDictionaryEntity[i]; }; DestroyDictionaryEntity: PROC[e: Entity] = { <> <> <> <> DestroyLinksTo[e]; DBStorage.DestroyTuple[e]; }; SameIndex: PROC[ i: Index, nl: AttributeList] RETURNS[BOOLEAN] = { <> <> a: Attribute; if: IndexFactor; count: INT_ 0; ifs: LIST OF Attribute_ VL2EL[QGetPList[i, ifIndexOf]]; FOR nlT: AttributeList _ nl, nlT.rest UNTIL nlT=NIL DO IF ifs=NIL THEN -- there more attributes in nl than index factors in ifs RETURN[FALSE]; a_ nlT.first; if_ ifs.first; ifs_ ifs.rest; IF count#V2I[SafeGetP[if, ifOrdinalPositionIs]] THEN ERROR InternalError; IF NOT Eq[a, V2E[SafeGetP[if, ifAttributeIs]]] THEN RETURN[FALSE]; count_ count+1; ENDLOOP; IF ifs#NIL THEN RETURN[FALSE]; -- there were more index factors than attributes in nl RETURN[TRUE] }; END. Changed by Rick on 9-Sep-81 12:03:07: Changed openState to databaseOpen BOOLEAN; patched ExtraCedar to start Pine exactly once, it seems to be the best solution till Pine is incorporated in Cedar. Changed by Rick on 26-Nov-81 13:38:45: Must check that no subdomains for surrogate relation optimization. Changed by Rick on 26-Nov-81 16:20:04: Open/Create Database now get server from dbName instead of separate argument. Changed by Rick on 6-Jan-82 12:25:15: EmptyDomain should not search subdomains. Don't need Nto1fromRole in normal PropertyObject (QDeclarePropertyFromAttrs). Changed by Rick on 22-Jan-82 9:20:21: Implemented QDestroyDomain which also destroys SubType connections. Changed by Rick on April 15, 1982 7:01 pm: CreatePropertyFromAttrs checks args. Changed by Rick on April 24, 1982 12:16 am: QDeclareProperty uses version. QDestroyDomain also destroys any relations that directly reference the domain. QDestroyRelation needs to remove from index. Changed by Rick on April 24, 1982 5:50 pm: QDestroyRelation must nil out any domain refs when destroying attributes else leave dangling ref in group. It must also FlushAttributeCache. Changed by Rick on April 28, 1982 10:47 am: upper-case domain/relation index entries. Changed by Rick on May 4, 1982 1:01 pm: DBStorage.CloseCommunication no longer does CloseDatabase, so call this explicitly now. OpenDatabase now calls CloseCommunication when the VersionProblem error is raised, to leave things in a consistent (unopened) state. Generate RelationOrDomainSubsetsStillOpen at end of CloseDatabase so works properly if client proceeds OR resumes. Changed by Rick on May 6, 1982 6:39 pm: Forgot to call ConvertToUpper when removing domains and relations from indexes. Moved DestroyIndex here from DBViewSystemImpl. Changed by Rick on July 30, 1982 1:56 pm: Changed DeclareSubType to do more thorough checking of given sub and super. Changed DestroyDomain to run faster, by destroying the whole B-Tree index at once. This necessitated changing DestroyEntity in DBViewBasicImpl to ignore the B-Tree entry removal if it can't find a B-Tree for the domain. Changed by Rick on August 1, 1982 11:15 am: Check for databaseOpen on DeclareRelation, DeclareDomain, CloseDatabase, AbortTransaction. Changed by Rick on November 12, 1982 2:26 pm: New properties, indices, etc. Added DeclareIndex. Added assignment of aDomainProp in DeclareAttribute. Changed by Rick on December 13, 1982 8:45 pm: Changes for segments and various other cleanups. Indices not yet tested, however. Modified/added the global database, transaction, and segment procedures. Modified DeclareRelation and DeclareDomain to take and use segment argument. Changed BOOL databaseOpen=>initialized. Changed by Rick on January 16, 1983 3:27 pm: bug in DeclareIndex if index existed. Changed by Rick on February 1, 1983 8:50 am: Made all Declare procedures flush caches, just for safety; DeclareIndex was invalidating the relation info cache without flushing it! Declaring and Destroying Subtypes should be ok, for now, since they're not cached. Changed by Willie-Sue on February 3, 1983: add noLog arg to QOpenTransaction Changed by Rick on February 18, 1983 4:57 pm: Forgot to nil out ref to relation when destroying attributes in DestroyRelation. Changed by Rick Cattell on March 15, 1983 10:04 am: Domain caching. Changed by Rick Cattell on April 12, 1983 4:55 pm: A hard-to-find bug. Failed to NIL out the aDomainIs field of an attribute when destroying a relation, causing a dangling back reference in the domain it used to reference. This was being tickled by Maxwell's domain editor, which routinely creates and destroys relations just to see if Cypress has any complaints about the form of the attributes. Hmm. Wish there was an easier way to track down these "dangling groups" problems, which have happened several times before and are always difficult since the damage was done at an ealier time. We could do a GetAllRefAttributes before destroying system entities; we do this in DestroyDictionaryEntity for relations and domains already, in fact. Not sure if this would work for attributes. Changed by Willie-Sue on February 14, 1985: added useTrans argument to QEraseSegment; also made it do a MarkTransaction instead of a Close. Added QGetBuiltinSegments and some segment numbers. Added tioga formatting.