<> <> <> <> <> <<>> DIRECTORY Process USING [Abort, CheckForAbort, GetCurrent, InvalidProcess], Rope USING [Compare, Equal, ROPE], LoganBerry, LoganBerryEntry USING [GetAttr], LoganQueryClass; LoganQueryClassImpl: CEDAR PROGRAM IMPORTS Process, Rope, LoganBerry, LoganBerryEntry EXPORTS LoganQueryClass ~ BEGIN OPEN LoganQueryClass; ROPE: TYPE ~ Rope.ROPE; <> NextEntry: PROC [cursor: Cursor, dir: CursorDirection ¬ increasing] RETURNS [entry: LoganBerry.Entry] = { <> <> IF cursor.class.retrieve = NIL THEN RETURN[NIL]; Process.CheckForAbort[]; RETURN[cursor.class.retrieve[cursor, dir]]; }; EndGenerate: PROC [cursor: Cursor] RETURNS [] = { <> IF cursor.class.destroy = NIL THEN RETURN; cursor.class.destroy[cursor]; }; <<$Simple query class>> SimpleClass: QueryClass = NEW[QueryClassRec ¬ [$Simple, SimpleRetrieve, SimpleDestroy]]; <<>> SimpleCursor: TYPE = REF LoganBerry.Cursor; SimpleRetrieve: RetrieveProc = { <<[cursor: Cursor, dir: CursorDirection _ increasing] RETURNS [entry: LoganBerry.Entry]>> WITH cursor.data SELECT FROM sc: SimpleCursor => RETURN[LoganBerry.NextEntry[cursor: sc­, dir: dir]]; ENDCASE => ERROR LoganBerry.Error[$BadCursor, "Bad $Simple cursor"]; }; SimpleDestroy: DestroyProc = { <<[cursor: Cursor] RETURNS []>> WITH cursor.data SELECT FROM sc: SimpleCursor => LoganBerry.EndGenerate[cursor: sc­]; ENDCASE => ERROR LoganBerry.Error[$BadCursor, "Bad $Simple cursor"]; }; GenerateEntries: PUBLIC PROC [db: LoganBerry.OpenDB, key: LoganBerry.AttributeType, start: LoganBerry.AttributeValue ¬ NIL, end: LoganBerry.AttributeValue ¬ NIL] RETURNS [cursor: Cursor] = { <> <> sc: SimpleCursor ¬ NEW[LoganBerry.Cursor]; sc­ ¬ LoganBerry.GenerateEntries[db: db, key: key, start: start, end: end]; cursor.class ¬ SimpleClass; cursor.data ¬ sc; }; <<>> <<$Filter query class>> FilterClass: QueryClass = NEW[QueryClassRec ¬ [$Filter, FilterRetrieve, FilterDestroy]]; <<>> FilterCursor: TYPE = REF FilterCursorRec; FilterCursorRec: TYPE = RECORD [ input: Cursor, pattern: ROPE, pparsed: REF ¬ NIL, filter: MatchProc, atype: LoganBerry.AttributeType, stopNG: BOOLEAN ¬ FALSE ]; FilterRetrieve: RetrieveProc = { <<[cursor: Cursor, dir: CursorDirection _ increasing] RETURNS [entry: LoganBerry.Entry]>> WITH cursor.data SELECT FROM fc: FilterCursor => { match, nothingGreater: BOOLEAN ¬ FALSE; numAtypes: INT; entry ¬ NextEntry[fc.input, dir]; UNTIL match OR entry = NIL DO <> numAtypes ¬ 0; FOR e: LoganBerry.Entry ¬ entry, e.rest WHILE e # NIL DO IF e.first.type = fc.atype THEN { numAtypes ¬ numAtypes + 1; [match, nothingGreater, fc.pparsed] ¬ fc.filter[e.first.value, fc.pattern, fc.pparsed]; IF match THEN EXIT; }; ENDLOOP; IF NOT match THEN IF nothingGreater AND fc.stopNG AND dir = increasing AND numAtypes = 1 THEN entry ¬ NIL -- can stop before actual end of input ELSE entry ¬ NextEntry[fc.input, dir]; ENDLOOP; }; ENDCASE => ERROR LoganBerry.Error[$BadCursor, "Bad $Filter cursor"]; }; FilterDestroy: DestroyProc = { <<[cursor: Cursor] RETURNS []>> WITH cursor.data SELECT FROM fc: FilterCursor => EndGenerate[fc.input]; ENDCASE => ERROR LoganBerry.Error[$BadCursor, "Bad $Filter cursor"]; }; FilterEntries: PUBLIC PROC [input: Cursor, pattern: ROPE, filter: MatchProc, atype: LoganBerry.AttributeType, stopIfNothingGreater: BOOLEAN ¬ FALSE] RETURNS [output: Cursor] = { <> fc: FilterCursor ¬ NEW[FilterCursorRec]; IF filter = NIL THEN ERROR LoganBerry.Error[$BadCursor, "NIL filter proc provided"]; fc.input ¬ input; fc.pattern ¬ pattern; fc.filter ¬ filter; fc.atype ¬ atype; fc.stopNG ¬ stopIfNothingGreater; output.class ¬ FilterClass; output.data ¬ fc; }; <<$Merger query class>> MergerClass: QueryClass = NEW[QueryClassRec ¬ [$Merger, MergerRetrieve, MergerDestroy]]; <<>> MergerCursor: TYPE = REF MergerCursorRec; MergerCursorRec: TYPE = RECORD [ input: ARRAY [0..1] OF Cursor, atype: LoganBerry.AttributeType, prefetch: BOOLEAN ¬ FALSE, -- is there a prefetched entry? prefetchEntry: LoganBerry.Entry, -- prefetched entry if one exists prefetchValue: LoganBerry.AttributeValue, -- value saved for performance reasons prefetchInput: [0..1] ¬ 0, -- which input the prefetch came from prefetchDir: CursorDirection ¬ increasing -- direction of prefetch ]; MergerRetrieve: RetrieveProc = { <<[cursor: Cursor, dir: CursorDirection _ increasing] RETURNS [entry: LoganBerry.Entry]>> WITH cursor.data SELECT FROM mc: MergerCursor => { newInput: [0..1]; newEntry: LoganBerry.Entry; newValue: LoganBerry.AttributeValue; returnNew: BOOLEAN; IF mc.prefetch AND NOT mc.prefetchDir = dir THEN { -- prefetched in wrong direction <> [] ¬ NextEntry[mc.input[mc.prefetchInput], dir]; mc.prefetch ¬ FALSE; }; IF NOT mc.prefetch THEN { -- go ahead and get an entry mc.prefetchEntry ¬ NextEntry[mc.input[0], dir]; mc.prefetchValue ¬ LoganBerryEntry.GetAttr[mc.prefetchEntry, mc.atype]; mc.prefetchInput ¬ 0; mc.prefetchDir ¬ dir; mc.prefetch ¬ TRUE; }; newInput ¬ ABS[1-mc.prefetchInput]; newEntry ¬ NextEntry[mc.input[newInput], dir]; IF mc.prefetchEntry = NIL THEN { -- one input already exhausted IF newEntry = NIL THEN -- exhausted both inputs so reset state mc.prefetch ¬ FALSE; RETURN[newEntry]; }; IF newEntry = NIL THEN { -- just exhausted input entry ¬ mc.prefetchEntry; mc.prefetchEntry ¬ NIL; mc.prefetchInput ¬ newInput; RETURN[entry]; }; newValue ¬ LoganBerryEntry.GetAttr[newEntry, mc.atype]; returnNew ¬ SELECT Rope.Compare[newValue, mc.prefetchValue, FALSE] FROM less => (dir = increasing), greater => (dir = decreasing), ENDCASE => TRUE; IF returnNew THEN { entry ¬ newEntry; } ELSE { entry ¬ mc.prefetchEntry; mc.prefetchEntry ¬ newEntry; mc.prefetchValue ¬ newValue; mc.prefetchInput ¬ newInput; }; }; ENDCASE => ERROR LoganBerry.Error[$BadCursor, "Bad $Merger cursor"]; }; MergerDestroy: DestroyProc = { <<[cursor: Cursor] RETURNS []>> WITH cursor.data SELECT FROM mc: MergerCursor => { EndGenerate[mc.input[0]]; EndGenerate[mc.input[1]]; }; ENDCASE => ERROR LoganBerry.Error[$BadCursor, "Bad $Merger cursor"]; }; MergeEntries: PUBLIC PROC [input1, input2: Cursor, atype: LoganBerry.AttributeType] RETURNS [output: Cursor] = { <> mc: MergerCursor ¬ NEW[MergerCursorRec]; mc.input[0] ¬ input1; mc.input[1] ¬ input2; mc.atype ¬ atype; mc.prefetch ¬ FALSE; output.class ¬ MergerClass; output.data ¬ mc; }; <<$AntiFilter query class>> <> <<>> AntiFilterClass: QueryClass = NEW[QueryClassRec ¬ [$AntiFilter, AntiFilterRetrieve, FilterDestroy]]; AntiFilterRetrieve: RetrieveProc = { <<[cursor: Cursor, dir: CursorDirection _ increasing] RETURNS [entry: LoganBerry.Entry]>> WITH cursor.data SELECT FROM fc: FilterCursor => { match, nothingGreater: BOOLEAN ¬ TRUE; numAtypes: INT; entry ¬ NextEntry[fc.input, dir]; UNTIL NOT match OR entry = NIL DO <> match ¬ FALSE; <> numAtypes ¬ 0; FOR e: LoganBerry.Entry ¬ entry, e.rest WHILE e # NIL DO IF e.first.type = fc.atype THEN { numAtypes ¬ numAtypes + 1; [match, nothingGreater, fc.pparsed] ¬ fc.filter[e.first.value, fc.pattern, fc.pparsed]; IF match THEN EXIT; }; ENDLOOP; IF match THEN entry ¬ NextEntry[fc.input, dir]; ENDLOOP; }; ENDCASE => ERROR LoganBerry.Error[$BadCursor, "Bad $Filter cursor"]; }; AntiFilterEntries: PUBLIC PROC [input: Cursor, pattern: ROPE, filter: MatchProc, atype: LoganBerry.AttributeType] RETURNS [output: Cursor] = { <> fc: FilterCursor ¬ NEW[FilterCursorRec]; IF filter = NIL THEN ERROR LoganBerry.Error[$BadCursor, "NIL filter proc provided"]; fc.input ¬ input; fc.pattern ¬ pattern; fc.filter ¬ filter; fc.atype ¬ atype; fc.stopNG ¬ FALSE; output.class ¬ AntiFilterClass; output.data ¬ fc; }; <<$Duplicator query class>> DuplicatorClass: QueryClass = NEW[QueryClassRec ¬ [$Duplicator, DuplicatorRetrieve, DuplicatorDestroy]]; <<>> DuplicatorCursor: TYPE = REF DuplicatorCursorRec; DuplicatorCursorRec: TYPE = RECORD [ input: Cursor, -- input cursor num: [0..1], -- which output cursor am I? shared: SharedDuplicateData -- stuff common to both output queues ]; SharedDuplicateData: TYPE = REF SharedDuplicateDataRec; SharedDuplicateDataRec: TYPE = RECORD [ closed: ARRAY [0..1] OF BOOLEAN ¬ ALL[FALSE], -- cursor destroyed? prefetched: ARRAY [0..1] OF LIST OF LoganBerry.Entry ¬ ALL[NIL] -- entries ready for output cursors ]; DuplicatorRetrieve: RetrieveProc = { <<[cursor: Cursor, dir: CursorDirection _ increasing] RETURNS [entry: LoganBerry.Entry]>> WITH cursor.data SELECT FROM dc: DuplicatorCursor => { other: [0..1] ¬ IF dc.num=0 THEN 1 ELSE 0; IF dc.shared.prefetched[dc.num] = NIL THEN { <> entry ¬ NextEntry[dc.input, dir]; IF entry # NIL AND NOT dc.shared.closed[other] THEN { <> IF dc.shared.prefetched[other] = NIL THEN { dc.shared.prefetched[other] ¬ LIST[entry]; } ELSE { eList: LIST OF LoganBerry.Entry ¬ dc.shared.prefetched[other]; UNTIL eList.rest = NIL DO eList ¬ eList.rest; ENDLOOP; eList.rest ¬ LIST[entry]; }; }; } ELSE { <> entry ¬ dc.shared.prefetched[dc.num].first; dc.shared.prefetched[dc.num] ¬ dc.shared.prefetched[dc.num].rest; }; }; ENDCASE => ERROR LoganBerry.Error[$BadCursor, "Bad $Duplicator cursor"]; }; DuplicatorDestroy: DestroyProc = { <<[cursor: Cursor] RETURNS []>> WITH cursor.data SELECT FROM dc: DuplicatorCursor => { dc.shared.closed[dc.num] ¬ TRUE; IF dc.shared.closed[0] AND dc.shared.closed[1] THEN EndGenerate[dc.input]; }; ENDCASE => ERROR LoganBerry.Error[$BadCursor, "Bad $Duplicator cursor"]; }; DuplicateEntries: PUBLIC PROC [input: Cursor] RETURNS [output1, output2: Cursor] = { <> dc: DuplicatorCursor; sd: SharedDuplicateData ¬ NEW[SharedDuplicateDataRec]; sd.prefetched[0] ¬ NIL; sd.prefetched[1] ¬ NIL; dc ¬ NEW[DuplicatorCursorRec]; dc.num ¬ 0; dc.input ¬ input; dc.shared ¬ sd; output1.class ¬ DuplicatorClass; output1.data ¬ dc; dc ¬ NEW[DuplicatorCursorRec]; dc.num ¬ 1; dc.input ¬ input; dc.shared ¬ sd; output2.class ¬ DuplicatorClass; output2.data ¬ dc; }; <<$UnDuplicator query class>> UnDuplicatorClass: QueryClass = NEW[QueryClassRec ¬ [$UnDuplicator, UnDuplicatorRetrieve, UnDuplicatorDestroy]]; <<>> UnDuplicatorCursor: TYPE = REF UnDuplicatorCursorRec; UnDuplicatorCursorRec: TYPE = RECORD [ input: Cursor, -- input cursor atype: LoganBerry.AttributeType, prev: LoganBerry.AttributeValue ¬ NIL -- previous attribute value ]; UnDuplicatorRetrieve: RetrieveProc = { <<[cursor: Cursor, dir: CursorDirection _ increasing] RETURNS [entry: LoganBerry.Entry]>> WITH cursor.data SELECT FROM udc: UnDuplicatorCursor => { value: LoganBerry.AttributeValue; DO -- until get new value entry ¬ NextEntry[udc.input, dir]; value ¬ LoganBerryEntry.GetAttr[entry, udc.atype]; IF entry = NIL OR NOT Rope.Equal[value, udc.prev, FALSE] THEN EXIT; ENDLOOP; udc.prev ¬ value; }; ENDCASE => ERROR LoganBerry.Error[$BadCursor, "Bad $UnDuplicator cursor"]; }; UnDuplicatorDestroy: DestroyProc = { <<[cursor: Cursor] RETURNS []>> WITH cursor.data SELECT FROM udc: UnDuplicatorCursor => EndGenerate[udc.input]; ENDCASE => ERROR LoganBerry.Error[$BadCursor, "Bad $UnDuplicator cursor"]; }; UnDuplicateEntries: PUBLIC PROC [input: Cursor, atype: LoganBerry.AttributeType] RETURNS [output: Cursor] = { <> udc: UnDuplicatorCursor ¬ NEW[UnDuplicatorCursorRec]; udc.input ¬ input; udc.atype ¬ atype; output.class ¬ UnDuplicatorClass; output.data ¬ udc; }; <<$Subtracter query class>> SubtracterClass: QueryClass = NEW[QueryClassRec ¬ [$Subtracter, SubtracterRetrieve, SubtracterDestroy]]; <<>> SubtracterCursor: TYPE = REF SubtracterCursorRec; SubtracterCursorRec: TYPE = RECORD [ input: Cursor, -- input cursor except: Cursor, -- exceptions ptype: LoganBerry.AttributeType, -- primary key atype: LoganBerry.AttributeType, -- attribute on which streams are ordered avalue: LoganBerry.AttributeValue ¬ NIL, -- attribute value for current batch exceptEntry: LoganBerry.Entry ¬ NIL, -- next entry from except cursor exceptBatch: LIST OF LoganBerry.Entry ¬ NIL, -- more entries from except cursor nextExceptEntry: LoganBerry.Entry ¬ NIL -- first entry from next batch on except cursor ]; <> NextBatch: PROC [sc: SubtracterCursor, dir: CursorDirection] RETURNS [first: LoganBerry.Entry] ~ { sc.exceptEntry ¬ sc.nextExceptEntry; sc.nextExceptEntry ¬ NIL; sc.exceptBatch ¬ NIL; IF sc.exceptEntry = NIL THEN { sc.exceptEntry ¬ NextEntry[sc.except, dir]; IF sc.exceptEntry = NIL THEN RETURN[NIL]; }; sc.avalue ¬ LoganBerryEntry.GetAttr[sc.exceptEntry, sc.atype]; sc.nextExceptEntry ¬ NextEntry[sc.except, dir]; IF sc.ptype # sc.atype AND sc.nextExceptEntry # NIL THEN { -- get complete batch tail: LIST OF LoganBerry.Entry ¬ NIL; WHILE Rope.Equal[LoganBerryEntry.GetAttr[sc.nextExceptEntry, sc.atype], sc.avalue, FALSE] DO <> IF sc.exceptBatch = NIL THEN { sc.exceptBatch ¬ LIST[sc.exceptEntry]; tail ¬ sc.exceptBatch; }; tail.rest ¬ LIST[sc.nextExceptEntry]; tail ¬ tail.rest; sc.nextExceptEntry ¬ NextEntry[sc.except, dir]; ENDLOOP; }; RETURN[sc.exceptEntry]; }; DiscardFromBatch: PROC [sc: SubtracterCursor, discard: LoganBerry.Entry] RETURNS [] ~ { prev, ptr: LIST OF LoganBerry.Entry; IF sc.exceptBatch = NIL THEN { IF sc.exceptEntry = discard THEN sc.exceptEntry ¬ NIL; RETURN; }; prev ¬ NIL; ptr ¬ sc.exceptBatch; WHILE ptr # NIL AND NOT ptr.first = discard DO prev ¬ ptr; ptr ¬ ptr.rest; ENDLOOP; IF ptr # NIL THEN { IF prev = NIL THEN { sc.exceptBatch ¬ sc.exceptBatch.rest; sc.exceptEntry ¬ sc.exceptBatch.first; } ELSE { prev.rest ¬ ptr.rest; }; }; }; SubtracterRetrieve: RetrieveProc = { <<[cursor: Cursor, dir: CursorDirection _ increasing] RETURNS [entry: LoganBerry.Entry]>> WITH cursor.data SELECT FROM sc: SubtracterCursor => { IF sc.exceptEntry = NIL THEN sc.exceptEntry ¬ NextBatch[sc, dir]; entry ¬ NextEntry[sc.input, dir]; WHILE sc.exceptEntry # NIL DO -- until get entry that should not be discarded entryAValue: LoganBerry.AttributeValue ¬ LoganBerryEntry.GetAttr[entry, sc.atype]; SELECT Rope.Compare[entryAValue, sc.avalue, FALSE] FROM less => EXIT; -- return entry equal => { -- need to check primary keys match: LoganBerry.Entry ¬ NIL; SELECT TRUE FROM sc.ptype = sc.atype => match ¬ sc.exceptEntry; sc.exceptBatch = NIL => { exceptPValue, entryPValue: LoganBerry.AttributeValue; exceptPValue ¬ LoganBerryEntry.GetAttr[sc.exceptEntry, sc.ptype]; entryPValue ¬ LoganBerryEntry.GetAttr[entry, sc.ptype]; IF exceptPValue = entryPValue THEN match ¬ sc.exceptEntry; }; ENDCASE => { <> entryPValue: LoganBerry.AttributeValue ¬ LoganBerryEntry.GetAttr[entry, sc.ptype]; FOR eList: LIST OF LoganBerry.Entry ¬ sc.exceptBatch, eList.rest WHILE eList # NIL DO exceptPValue: LoganBerry.AttributeValue ¬ LoganBerryEntry.GetAttr[eList.first, sc.ptype]; IF exceptPValue = entryPValue THEN {match ¬ eList.first; EXIT}; ENDLOOP; }; IF match = NIL THEN EXIT; -- return entry <> entry ¬ NextEntry[sc.input, dir]; DiscardFromBatch[sc, match]; IF sc.exceptEntry = NIL THEN sc.exceptEntry ¬ NextBatch[sc, dir]; }; greater => { -- discard except batch and try again sc.exceptEntry ¬ NextBatch[sc, dir]; }; ENDCASE; ENDLOOP; }; ENDCASE => ERROR LoganBerry.Error[$BadCursor, "Bad $Subtracter cursor"]; }; SubtracterDestroy: DestroyProc = { <<[cursor: Cursor] RETURNS []>> WITH cursor.data SELECT FROM sc: SubtracterCursor => EndGenerate[sc.input]; ENDCASE => ERROR LoganBerry.Error[$BadCursor, "Bad $Subtracter cursor"]; }; SubtractEntries: PUBLIC PROC [input, except: Cursor, ptype, atype: LoganBerry.AttributeType] RETURNS [output: Cursor] = { <> sc: SubtracterCursor ¬ NEW[SubtracterCursorRec]; sc.input ¬ input; sc.except ¬ except; sc.ptype ¬ ptype; sc.atype ¬ atype; output.class ¬ SubtracterClass; output.data ¬ sc; }; <<$Feeder query class>> FeederClass: QueryClass = NEW[QueryClassRec ¬ [$Feeder, FeederRetrieve, FeederDestroy]]; <<>> FeederCursor: TYPE = REF FeederCursorRec; FeederCursorRec: TYPE = RECORD [ input: Cursor -- input cursor ]; FeederRetrieve: RetrieveProc = { <<[cursor: Cursor, dir: CursorDirection _ increasing] RETURNS [entry: LoganBerry.Entry]>> WITH cursor.data SELECT FROM fc: FeederCursor => { entry ¬ NIL; IF fc.input.class # NIL THEN entry ¬ NextEntry[fc.input, dir]; }; ENDCASE => ERROR LoganBerry.Error[$BadCursor, "Bad $Feeder cursor"]; }; FeederDestroy: DestroyProc = { <<[cursor: Cursor] RETURNS []>> WITH cursor.data SELECT FROM fc: FeederCursor => EndGenerate[fc.input]; ENDCASE => ERROR LoganBerry.Error[$BadCursor, "Bad $Feeder cursor"]; }; FeedEntries: PUBLIC PROC [] RETURNS [output: Cursor] = { <> fc: FeederCursor ¬ NEW[FeederCursorRec]; fc.input.class ¬ NIL; output.class ¬ FeederClass; output.data ¬ fc; }; SetFeederInput: PUBLIC PROC [feeder, input: Cursor] = { <> WITH feeder.data SELECT FROM fc: FeederCursor => fc.input ¬ input; ENDCASE => ERROR LoganBerry.Error[$BadCursor, "Bad $Feeder cursor"]; }; <<$Aborter query class>> AborterClass: QueryClass = NEW[QueryClassRec ¬ [$Aborter, AborterRetrieve, AborterDestroy]]; <<>> AborterCursor: TYPE = REF AborterCursorRec; AborterCursorRec: TYPE = RECORD [ input: Cursor, aborted: BOOLEAN ¬ FALSE, -- if TRUE then cursor has been aborted process: PROCESS ¬ NIL -- process currently executing retrieve operation ]; AborterRetrieve: RetrieveProc = { <<[cursor: Cursor, dir: CursorDirection _ increasing] RETURNS [entry: LoganBerry.Entry]>> ENABLE ABORTED => GOTO Aborted; WITH cursor.data SELECT FROM ac: AborterCursor => { IF ac.aborted THEN GOTO Aborted; TRUSTED {ac.process ¬ LOOPHOLE[Process.GetCurrent[], PROCESS];}; entry ¬ NextEntry[ac.input, dir]; ac.process ¬ NIL; }; ENDCASE => ERROR LoganBerry.Error[$BadCursor, "Bad $Aborter cursor"]; EXITS Aborted => RETURN[NIL]; }; AborterDestroy: DestroyProc = { <<[cursor: Cursor] RETURNS []>> WITH cursor.data SELECT FROM ac: AborterCursor => EndGenerate[ac.input]; ENDCASE => ERROR LoganBerry.Error[$BadCursor, "Bad $Aborter cursor"]; }; EnableAborts: PUBLIC PROC [input: Cursor] RETURNS [output: Cursor] ~ { <> ac: AborterCursor ¬ NEW[AborterCursorRec]; ac.input ¬ input; output.class ¬ AborterClass; output.data ¬ ac; }; AbortQuery: PUBLIC PROC [cursor: Cursor] RETURNS [] ~ TRUSTED { <> <> WITH cursor.data SELECT FROM ac: AborterCursor => { ac.aborted ¬ TRUE; IF ac.process # NIL THEN Process.Abort[ac.process ! Process.InvalidProcess => CONTINUE]; }; ENDCASE => NULL; }; END. <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <<>>