Project

General

Profile

Bug #9030

FIND statements should use an internal buffer cursor

Added by Alexandru Lungu about 2 years ago. Updated about 12 hours ago.

Status:
WIP
Priority:
Normal
Target version:
-
Start date:
Due date:
% Done:

90%

billable:
No
vendor_id:
GCD
case_num:
version_reported:
version_resolved:
production:
No
env_name:
topics:

Related issues

Related to Database - Bug #11953: Consider using the AST transformation pipeline philosophy for FQL preprocessing New

History

#1 Updated by Alexandru Lungu about 2 years ago

This task extracts the issue from #8752-3 and discussion from #6667-1054 to #6667-1087.

def temp-table tt field f1 as int index idx f1 desc.

create tt.
tt.f1 = 1.
create tt.
tt.f1 = 2.

open query q2 for each tt by f1.
get first q2.

open query q for each tt by f1.
get next q.
message tt.f1. // 4GL 1 (It is the first record found by q by f1 asc - not affected by q2)

find next tt.
message tt.f1. // 4GL 2 (It is the first record found by the FIND by f1 desc - not affected by q or q2)

open query q3 for each tt by f1.
get first q3.
get next q3.
message tt.f1. // 4GL 2 (It is the second record found by q3 by f1 asc - not affected by previous queries)

find next tt.
message tt.f1. // 4GL 1 (!!!! Important: It is the second record found by FIND by f1 desc)

The issue is that 4GL seems to use a buffer-level cursor to resolve the FIND queries, while FWD is honoring some offEnd and sortIndex properties of the RecordBuffer that can be spoiled by other queries that are run in parallel. Also, the cursor is not stateful in FWD, in the sense that FindQuery is simply honoring the current record loaded in the buffer (which can be loaded by another query).

#2 Updated by Constantin Asofiei about 2 years ago

Alexandru, this may not be a query, but a cursor on the index. For example, you can have FIND statements with different WHERE clauses (which can trigger different indexes, depending on the WHERE clause?).

Also, the BUFFER:FIND- methods do not affect this cursor:

def temp-table tt field f1 as int index idx f1 desc.

create tt.
tt.f1 = 1.
create tt.
tt.f1 = 2.
create tt.
tt.f1 = 3.
create tt.
tt.f1 = 4.
release tt.

find first tt.
message tt.f1. // 4

def var hb as handle.
hb = buffer tt:handle.
hb:find-unique("where tt.f1 = 2").
message tt.f1. // 2
find next tt.
message tt.f1. // 3

And not even release - FIND will still know on which record is positioned:

def temp-table tt field f1 as int index idx f1 desc.

create tt.
tt.f1 = 1.
create tt.
tt.f1 = 2.
create tt.
tt.f1 = 3.
create tt.
tt.f1 = 4.
release tt.

find first tt.
message tt.f1. // 4

release tt.

find next tt.
message tt.f1. // 3

Another issue is the AdaptiveFind, which is a performance improvement in FWD for looped FIND statements. This in turn can create a RandomAccessQuery, which needs to act like a FIND, and not as an internal RandomAccessQuery which can be used for AdaptiveQuery in FOR EACH blocks, for example.

I have some changes from trying to solve this for #9015 (I didn't need them in the end for that task), and my problem in those changes is that I'm using a record as the 'cursor' for the FIND statements. And this record can get deleted or evicted from the cache.

#3 Updated by Alexandru Lungu about 2 years ago

Alexandru, this may not be a query, but a cursor on the index. For example, you can have FIND statements with different WHERE clauses (which can trigger different indexes, depending on the WHERE clause?).

I see your point. Tested:

def temp-table tt field f1 as int field f2 as int index idx1 as unique f1 index idx2 as unique f2.
do transaction:
    create tt.
    tt.f1 = 1.
    tt.f2 = 1.
    create tt.
    tt.f1 = 2.
    tt.f2 = 2.
    create tt.
    tt.f1 = 3.
    tt.f2 = 3.
    release tt.
end.

find first tt where f1 > 0.
message tt.f1. // 1
find next tt where f1 > 0.
message tt.f1. // 2
find next tt where f2 > 0.
message tt.f2. // 3

This shows that the last NEXT is still honoring the current cursor and not creating another one. So this is not a cursor on an index, but neither a query with a fixed WHERE clause.
I am rather thinking this does a "snapshot" of the last loaded record by a FIND and uses that as a reference record.
So this is basically what we have now in the FWD run-time, but instead of using the currently loaded record in the buffer as reference, it should use the "last loaded record by a FIND".

#5 Updated by Greg Shah about 2 years ago

Did we recently make changes that caused this issue to be a problem? Surely this has not always been an issue.

#6 Updated by Alexandru Lungu about 2 years ago

Trunk has a change to invalidate AdaptiveQuery right from the start if there are records in the dirty database. This caused some FOR EACH (which were doing large database SQLs) to work as a sequence of FIND NEXT (doing one-by-one retriaval from the database).

I suspect that this latent problem with offEnd and sortIndex is now more visible, considering that more queries behave like a sequence of FIND NEXT, requiring the offEnd and sortIndex to work properly.

#7 Updated by Constantin Asofiei about 2 years ago

The regression in #9015-29 is from trunk rev 15325, for #8433, where a FIND NEXT without the strong scope needs to stop finding records on the second iteration, like this:

do transaction:
   for each book: delete book. end.
   create book.
   book.book-id = 1.
   book.book-title = "abc".
   book.isbn = "1".
   release book.
   create book.
   book.book-id = 2.
   book.book-title = "abc".
   book.isbn = "2".
   release book.
end.

def buffer bbook for book.
def var i as int.
def var k as int.

do while true:
   // do for bbook while true:
    do while true:
      find next bbook where bbook.book-id > i no-error.
      if not avail bbook then leave.
      message k bbook.book-id.
   end.
   pause.
   k = k + 1.
end.

The strong scope makes the buffer re-open its most outer scope, and this requires its offend to reset (at least).

Otherwise, the findings in #9030-1 is new behavior which was not observed until now - I don't think this ever worked properly in FWD.

#8 Updated by Constantin Asofiei about 2 years ago

Alexandru, I've created task branch 9030a from trunk rev 15372. This is the first attempt to solve the buffer's internal cursor on which FIND statements use as a reference to navigate.

Note that the buffer's internal offend I think needs to be limited only to FIND statements, and not internal RandomAccessQuery's used for i.e. FOR EACH/FIRST loops.

BTW, in this attempt, the buffer's offend still had to be reset on outermost query scope. So it may be that this change is required, but not the complete fix.

#9 Updated by Alexandru Lungu almost 2 years ago

Alexandru, I've created task branch 9030a from trunk rev 15372. This is the first attempt to solve the buffer's internal cursor on which FIND statements use as a reference to navigate.

I am deferring some of the discussions on #9241 to this task.

def temp-table tt field f1 as int index idx f1.

do transaction:
    create tt. tt.f1 = 1.
    create tt. tt.f1 = 3.
end.

message "example 1".
do transaction:
    find next tt where tt.f1 = 5 no-error.
    message avail(tt). // no
    find next tt where tt.f1 = 3 no-error. // find after 5
    message avail(tt). // no
end.

message "example 2".
do transaction:
    define buffer buf for tt.
    find next buf where buf.f1 = 2 no-error.
    message avail(buf). // no
    find next buf where buf.f1 = 3 no-error. // find after 2
    message avail(buf) buf.f1. // yes 3
end.

This shows that the reference used by FIND NEXT is not a record. In example 1, the reference is a virtual location in the index that has f1 = 5. In the second example, the virtual location is on f1 = 3.
I am checking out 9030a to review the current implementation. I also have some changes to FqlPreprocessor to detect the indexed properties used in range index queries.
The idea is to store a bit of information on the RecordBuffer reflecting the last FIND NEXT executed on it, so the next FIND NEXT will use it as reference. Instead of using a reference record, it will use a reference virtual record if that makes sense.

My algorithm:
  • attempt to analyze the where similar to the OE DB planner, identifying the parts used for the index range query and the parts used for filtering.
    • I am starting light: the where should formed out of AND clauses. We are interested only on the flat property matches over indexed fields - these are used for the index range query. Other clauses can be considered part of the filtering.
    • The virtual record will be formed out of the r-values from the property matches that resemble a prefix of the index (first N-1 should be EQUAL, Nth can be either equal or />= comparison for DESC).
    • I am planning to extend collectPropertyMatches to do that.
  • after the execution of FIND NEXT, if no record is found, store the virtual record at the Record Buffer level.
  • each subsequent FIND NEXT will augment with an active bundle based on this virtual record.
  • what triggers the reset of the virtual record? still to be investigated.

Last interesting finding:

def temp-table tt field f1 as int index idx f1.

do transaction:
    create tt. tt.f1 = 1.
    create tt. tt.f1 = 3.
end.

message "example 1".
do transaction:
    def buffer buf for tt.
    find next tt where tt.f1 > 10 no-error.
    message avail(tt). // no
    create buf. buf.f1 = 100. release buf.
    find next tt no-error. // find after 10
    message avail(tt) tt.f1. // yes 100
end.

FIND NEXT stores the virtual record with f1 set on 10 and so the upcoming FIND NEXT will find the new record with f1 = 100.

#10 Updated by Alexandru Lungu almost 2 years ago

Extra findings:
  • If the buffer is sent as parameter, it keeps it state in regard to FIND NEXT.
  • If the table is sent as parameter (even input-output), the default buffer won't share the state of the buffer (expected, as the callee has its own buffers defined).
  • If the table is by-reference, the buffer keeps its state.
  • If the FIND NEXT operations are done in separate siblings do transaction blocks, the buffer still keeps its state.

I was trying to confirm that this is a buffer things and not a scope thing. I was having a wild thought that maybe the FIND NEXT was updating a scoped-state instead of a buffer-state. I think this can be denied by my findings.

#12 Updated by Alexandru Lungu almost 2 years ago

Extra findings:

  • doing a FIND NEXT on another index doesn't seem to reset the cursor
    def temp-table tt field f1 as int index idx f1 index idx2 f2.
    
    do transaction:
        create tt. tt.f1 = 1. tt.f2 = 1.
        create tt. tt.f1 = 5. tt.f2 = 5.
        create tt. tt.f1 = 10. tt.f2 = 10.
    end.
    
    message "example 4".
    do transaction:
       find next tt where tt.f1 = 4 no-error.
       message avail(tt). // no
       find next tt no-error. 
       message avail(tt) tt.f1. // yes 5
       find next tt where tt.f1 > -1 use-index idx2 no-error.
       message avail(tt) tt.f1. // yes 10
    end.

On the last message I would have expected to see yes 1 as the index was reset. I suppose that the first FIND is setting the sort index on the buffer. Changing the where/index doesn't reset that sort index.

FIND FIRST is not resetting the cursor:

def temp-table tt field f1 as int index idx f1 index idx2 f2.

do transaction:
    create tt. tt.f1 = 1. tt.f2 = 3.
    create tt. tt.f1 = 5. tt.f2 = 2.
    create tt. tt.f1 = 10. tt.f2 = 1.
end.

message "example 5".
do transaction:
   find next tt where tt.f1 = 4 no-error.
   message avail(tt). // no
   find next tt no-error. 
   message avail(tt) tt.f1. // yes 5
   find first tt where tt.f1 > -1 use-index idx2 no-error.
   message avail(tt) tt.f1. // yes 10
   find next tt no-error.
   message avail(tt). // no
end.

On last message, if the index was idx2, the FIND NEXT should have gone to f1 = 5, but it doesn't. It still iterates on the idx1.

ATM, I think the FIND NEXT statement freezes the index. Constantin, did you find otherwise?

#13 Updated by Alexandru Lungu almost 2 years ago

  • % Done changed from 0 to 30
  • Status changed from New to WIP
  • Assignee set to Alexandru Lungu

Implemented 9030b/rev 15611

  • Includes an alpha version of FindSupport that managed the algorithm presented above. It is basically a more specialized cursor dedicated to buffers used inside FIND NEXT/PREV statements.
  • Includes changes to the property matcher in FqlPreprocessor to better mimic the planning decisions that led to "virtual reference record" technique.
Issues to be fixed:
  • The operators of the property matches are not considered yet (EQ vs GT vs LT, etc.). Also, not only the indexed property matches are considered - this should be fixed asap.
  • There seems to be an edge case when no index is defined - needs a bit more investigation. From the looks of it, it works considering recid asc as a sort index indeed.
I am testing #9214 unit test suite.
  • trunk baseline: 68% tests passing
  • 9030b: 38% tests passing (no fatal exceptions, just bad outputs).

#14 Updated by Alexandru Lungu almost 2 years ago

  • % Done changed from 30 to 50
Committed 9030b/rev 15612 and 9030b/rev. 15613:
  • Finished the implementation at the level of fixing the unit tests made for this feature. 82% are passing, but the remaining tests are failing due to an unrelated problem: bad index selection in FWD.
The tests were relevant for this feature, especially in terms of combining NEXT/PREV/FIRST/LAST clauses to see how the virtual reference record is set.
  • What they lack are multi-field indexes, so that I couldn't properly test what happens if only a prefix is present in the where clause.
  • Also, I need to better investigate RAQ in the context of CompoundQuery, invalidated AdaptiveQuery and AdaptiveFind. All of the examples run were on a FindQuery alone.
  • I admit that I have a trivial implementation for parsing literals from the FQL to resolve the property matches. This covers only boolean, numeric, decimal, string and subst parameters (that are resolved with OrmUtils.normalizeValue).
  • There is still work to be done on some javadoc.
  • Lastly, my attempt has some implementation details that doesn't honor the dirty share manager.
    • I need to better understand how dirty sharing conflicts with the virtual reference rows.
    • If a FIND NEXT is run without a WHERE (or a where without indexed property matches) and no record is found, then the virtual reference record is that very last record in that index. For this, I simply use an FQL to extract the last record in the primary database, but this doesn't honor the dirty database as well.

#15 Updated by Alexandru Lungu almost 2 years ago

Committed 9030b/rev. 15614: minor fixes
  • reintroduced off-end on RAQ to properly support invalidated AdaptiveQuery to avoid running infinitely.
  • use java.util.Date in Record instead of presuming using the sql one.
  • fixed unique-index-lookup checker from FQL preprocessor. This was relying on the property matcher that was changed in the meantime - it can return an empty list of matches instead on null.

I fixed this was some problems on a large customer application unit tests. There are still tests failing now and then, but the real problem is that it gets stuck at some point. Continuing investigation.

#18 Updated by Alexandru Lungu 7 days ago

  • Assignee changed from Alexandru Lungu to Stefanel Pezamosca

#19 Updated by Stefanel Pezamosca 7 days ago

I will create a new 9030c branch to work on this as the a/b branches are quite old. I will checkout them only to pick up some changes.

#20 Updated by Stefanel Pezamosca 6 days ago

  • reviewer Alexandru Lungu added

9030c: plan

9030a and 9030b are about 1,400 trunk revisions behind, so I created 9030c from trunk rev 16787 and plan to port only the good parts of them.

Taken from 9030a and 9030b

  • From 9030a: the cursor is used only by FIND statements (a flag on FindQuery), not by the RandomAccessQuery instances which FOR EACH and AdaptiveQuery use internally; the cursor is reset when the buffer's outermost scope opens (the #8433 case); the BUFFER:FIND-FIRST/LAST/UNIQUE/BY-ROWID() methods neither use nor move it.
  • From 9030b: FindSupport, a cursor per buffer which keeps a "virtual reference record" when a FIND finds nothing, and the FQLPreprocessor matching of the WHERE clause which finds the index bracket of a FIND (equality prefix, then a range bound).
  • Not taken:
    • 9030a's record-based cursor: #9030-8 shows the reference is not a record.
    • 9030a's use of setDynamicPredicate, which now has side effects in trunk.
    • 9030b's removal of the buffer's offEnd/sortIndex and current-record fallback for all queries: trunk's AdaptiveQuery.quickRevalidate() relies on them, so only FIND statements will change.

Problems found in the 9030b code

I plan to fix these while porting it:

  1. Shared property matcher. 9030b made the FQLPreprocessor property matcher lenient, which misleads H2 direct access, cache invalidation and server-side join decisions: uniqueFld = ? and fa + fb = fc looks like a unique lookup. I plan to keep the matcher as it is and add a separate list of the FIND matches.
  2. Stale values. The substitution values are stored in the cached preprocessor, so later FINDs reuse the values of the first one. I plan to take them from each FIND's arguments.
  3. Comparisons. The operators of the WHERE comparisons (=, <, >) and whether the field belongs to the index are ignored; a function around a field is ignored too (substring(c,1,2) = 'ab' is read as c = 'ab'). I plan to build the bracket from the operators, only from fields of the index, and to trust only the upper/rtrim folding around a field. BEGINS needs its own range.
  4. Value types. The virtual record stores literals without converting them to the field's type (dec-field > 5 stores an integer), which throws ClassCastException on the next comparison. I plan to convert each value to its field's type.
  5. "Last record" lookup. When a FIND without an indexed match finds nothing, 9030b queries the last record of the index: it ignores the temp-table multiplex and the dirty database, costs a query on every failed FIND, and is wrong once records are created after the FIND. I plan to replace it with positions before or after all the values of an index field, passed to the NEXT/PREV statements as constant true/false conditions (StatementTemplate bound values), and one small query only when start and end of the bracket can't be told apart otherwise.
  6. Looped FINDs. AdaptiveFind is not connected to the cursor, so a looped FIND NEXT after a FIND FIRST restarts from the first record; this is probably the hang seen on 9030b.
  7. CAN-FIND moves the cursor. It must not.

OpenEdge behaviour

I plan to measure every rule on OpenEdge 12.8 (about 260 cases), and make these the specification. The cursor is the index key of the last record:

  • found by a FIND, or read by FOR EACH/FIRST/LAST (joins and BY included), or by a FIND inside a DO/REPEAT PRESELECT block;
  • not moved by OPEN QUERY/GET, dynamic queries, FOR EACH ... BREAK BY, the BUFFER:FIND-* methods, CAN-FIND, CREATE, RELEASE, DELETE, UNDO, or FIND CURRENT on a temp-table;
  • moved by FIND CURRENT on a permanent table.

Also:

  • The key is frozen when the record is found: changing its fields later doesn't move the cursor.
  • A FIND which finds nothing moves the cursor between records, depending on the index range of its WHERE in its direction: to the end of the range if records in it were filtered out; otherwise a FIND FIRST/LAST/UNIQUE leaves it at the start of the range, and a FIND NEXT/PREV moves it to the end if there were records past it, else leaves it where it started. It never moves backwards. A bound like f > 1 ends before the records equal to 1.
  • An ambiguous FIND UNIQUE leaves the cursor on the first match; a failed FIND by ROWID leaves it where it was.
  • A FIND on another index continues from the last record found; a place between records, or past the end, belongs to the index of its FIND only.
  • Re-entering a DO FOR block (strong scope) resets the cursor; REPEAT FOR iterations don't.
  • BUFFER parameters and BY-REFERENCE tables share the cursor; a table passed by value doesn't.

Functional problems found

The ported implementation passes the scenarios of this issue, but still fails about 30 of the OpenEdge cases (trunk about 60), including some which trunk gets right only by chance. I plan to fix them as follows.

  1. FOR statements and PRESELECT FINDs don't move the cursor. I plan to add a trackFindCursor flag on AbstractQuery, so that the records these queries read set the cursor. QueryWrapper (OPEN QUERY, dynamic queries), BREAK BY and the BUFFER:FIND-* methods will switch it off, and CompoundQuery and AdaptiveQuery will pass it on to their component queries.
  2. The cursor key is not frozen. The cursor holds the live record, so changing one of its index fields moves the cursor. I plan to copy the record just before a setter changes one of its indexed fields (only then, so that FOR EACH ... ASSIGN loops don't pay for a copy per row).
  3. FIND CURRENT on a permanent table doesn't move the cursor. I plan to set the cursor after the reload, for permanent tables only.
  4. Descending indexes. FIND PREV loops and FINDs on a descending index don't share positions with the other FINDs. I plan to normalize the cursor's index key (first component ascending) and invert the FIND direction when the sort is inverted.
  5. Looped FINDs (AdaptiveFind). They return the same record twice when another statement moves the cursor (trunk too), and lose the NO-ERROR off-end behaviour on their fallback query. I plan to use the preselected results only while the cursor is where the loop left it; otherwise a worker query will navigate from the cursor, as FIND NEXT does, with the loop's off-end behaviour.
  6. Every FIND which finds nothing computes its position at once, with an extra query in most cases. I plan to only record the miss, and compute the position when a later FIND NEXT/PREV needs it.
  7. Ambiguous FIND UNIQUE raises its error before the cursor is set. I plan to place the cursor on the first match.
  8. BUFFER parameters don't share the cursor: the parameter's proxy forwards only public methods, so the cursor accessor of RecordBuffer must be public.
  9. Dirty share. A FIND NEXT/PREV must continue from the dirty copy of the cursor record, as trunk does from the current record.
  10. Comparisons with the unknown value. 38 tests of TestQueryUnknownValue fail, on trunk too. OpenEdge sorts ? high only in the comparisons which bracket the index, and evaluates all the other ones as 4GL expressions, where f < ? is unknown and f <= ? matches only unknown values. FWD applies the index meaning to any field of the index, and its FIND-specific code for this never runs (its input is computed after it is used). I plan to add indexBracket() to FQLPreprocessor and apply each meaning to the right comparisons, and to make QueryComponent pass the OPEN QUERY index fields in index order, qualified. Two limits remain:
    1. FOR EACH: the runtime doesn't know the index it brackets; this needs the index from conversion.
    2. UPPER(field) = "A" converts to the same FQL as field = "A"; this needs a conversion change.

Performance problems found

I measured 18 FIND-heavy scenarios on OpenEdge, trunk and 9030c.

  1. FIND NEXT/PREV SQL can't use the index. Moving past a key generates (k > ? or k is null), which is not an index range scan. For example, FIND NEXT alternating two indexes takes 244 ms (OpenEdge 4 ms). I plan to split it into two statements, k > ? then k is null. I will skip the second one when the position is not a known value, and for mandatory and primary key fields, which can't be unknown.
  2. Unknown-value bracket. A lower bound of the bracket matches the unknown values only when no upper bound excludes them. Adding or k is null in that case turns a range scan into a full scan. I plan to add it only when the unknown values are inside the bracket.
  3. REPEAT: FIND NEXT ... NO-LOCK loops on permanent tables. AdaptiveFind builds its query with a SHARE lock, so it selects ids only and reloads each row by id: one database round trip per row (5000 rows: 161 ms, OpenEdge 6 ms). I plan to preselect full records when the FIND is NO-LOCK, as FOR EACH NO-LOCK does; a FIND with a lock will use the worker query. Temp-tables have no locks and are not affected.
  4. Nested FIND loops (FIND FIRST, then FIND NEXT inside REPEAT WHILE AVAILABLE) run one database round trip per FIND NEXT, plus two statements which can't find anything at the end of each group (4900 rows: 296 ms, OpenEdge 6 ms). I plan to:
    1. read records ahead for a sequence of FIND NEXT/PREV with the same query and arguments, in pages of 8, 64, then 256 records, kept in FindSupport;
    2. drop a page when the cursor moves elsewhere, the arguments change, or the table changes, using a new version per table in FastFindCache (every create, delete, index change, rollback, copy, and the end of another session's transaction already invalidates it); a temp-table's version will be dropped with its multiplex;
    3. re-read each record served from a page through the session cache, so that other sessions' committed changes and deletes are seen, and keep the dirty share check on every FIND;
    4. skip the statements which can find nothing: a constant false bound, or a range over a field which the WHERE clause pins by an equality to the same value.
  5. Temp-table FIND cache. When a procedure which used a local buffer for a shared temp-table ends, all the FastFindCache results of the table are dropped, at a cost proportional to their number, although the table is still in use. I plan to drop them only when the temp-table instance (multiplex) is no longer in use.
  6. FIND NEXT/PREV SQL text is rebuilt on every FIND. I plan to cache it in StatementTemplate, keyed by which position values are known, unknown or bounds.

Verification plan

  • ABLUnit tests generated from the OpenEdge probes, each run on OpenEdge 12.8 and FWD:
    • tests/persistence/findcursor: the cursor semantics above (185 tests);
    • tests/query/FindUnknownBracketTest: comparisons with the unknown value (77);
    • tests/persistence/findloop/FindLoopChangesTest: loops which change a record they haven't reached yet, on the database and on a temp-table (36);
    • tests/persistence/findloop/FindMandatoryIndexTest: FIND NEXT/PREV on mandatory index fields (10).
  • A two-session test: one session changes records in the middle of another's read-ahead.
  • The full ABLUnit corpus against trunk, for regressions.
  • The 18-scenario benchmark again, on OpenEdge, trunk and 9030c.

Not in this task

  • Other failing tests which use FIND belong to other issues: #11380 (predicate errors), #10803 (outer-join re-query after a FIND) and #9305 (BEGINS).
  • #11686 (a buffer-level AdaptiveFind for FIND NEXT sequences) and #9078 build on this task.

Let me now if you find anything wrong in this plan.

#21 Updated by Alexandru Lungu 6 days ago

Not taken: 9030a's record-based cursor: #9030-8 shows the reference is not a record.

I think the core discovery of 9030a is that the reference is not a record. I don't understand why it should not be taken. I prefer to port this as well.

Not taken: 9030b's removal of the buffer's offEnd/sortIndex and current-record fallback for all queries: trunk's AdaptiveQuery.quickRevalidate() relies on them, so only FIND statements will change.

This is a technical dept I want purged. The offEnd and sortIndex in the buffer are non-sense in the proposed implementation. I prefer to remove them in this effort. As long as the buffer will embed the AdaptiveFind for resolving the FIND on that buffer, then having sortIndex and offEnd in buffer doesn't make sense; they are resolved from AdaptiveFind query directly.

Shared property matcher. 9030b made the FQLPreprocessor property matcher lenient, which misleads H2 direct access, cache invalidation and server-side join decisions: uniqueFld = ? and fa + fb = fc looks like a unique lookup. I plan to keep the matcher as it is and add a separate list of the FIND matches.

AFAIK, Eduard and Razvan did changes to this area in the past; they may be able to provide feedback on how to improve FQLPreprocessor detection of property matches. If this is a big effort, I suggest having a sub-task to prepare grounds for it. IMHO, FQLPreprocessor is now a monster class that does all kind of optimizations and analysis. Some of this is dependent upon the order of FQL AST manipulation, intermediary analysis, etc. I prefer a TRPL alike approach of FQLPreprocessor, so that AST manipulation can happen in a structures series of phases, including annotations.

I opened #11953. Lets see if we have a resolution on that before stressing FQLPreprocessor even more.

Problems found in the 9030b code

These look reasonable, but they should be proven with tests first.

The key is frozen when the record is found: changing its fields later doesn't move the cursor.

This is what the snapshot does now in FWD.

OpenEdge behaviour

Reasonable, but should be proven with tests.

FIND NEXT/PREV SQL can't use the index. Moving past a key generates (k > ? or k is null), which is not an index range scan. For example, FIND NEXT alternating two indexes takes 244 ms (OpenEdge 4 ms). I plan to split it into two statements, k > ? then k is null. I will skip the second one when the position is not a known value, and for mandatory and primary key fields, which can't be unknown.

This should have been fixed in trunk already, but splitting the (k > ? or k is null) in two statements is a major change. It doesn't relate to FIND NEXT, but to any usage of active bundle (RAQ, prefetching, etc.). I prefer not to touch the policy here part of this task. I rather prefer to use something like ProgressiveResults or PagedResults just like an AdaptiveQuery does. As long as AdaptiveFind extends AdativeQuery, I think that we are simply leveraging the performance of the extended class. Changing that will mean changing any query performance. I am skeptic on this point.

REPEAT: FIND NEXT ... NO-LOCK loops on permanent tables. AdaptiveFind builds its query with a SHARE lock, so it selects ids only and reloads each row by id: one database round trip per row (5000 rows: 161 ms, OpenEdge 6 ms). I plan to preselect full records when the FIND is NO-LOCK, as FOR EACH NO-LOCK does; a FIND with a lock will use the worker query. Temp-tables have no locks and are not affected.

This is another item not related to this task. This is a design choice we have for all queries, not only FIND. I don't think we should scope this task to such decisions.

In essence I want to focus only on the functional part of this task. After we build the infrastructure to move the FIND implementation within the buffer, we can profile the target application and build on top of this architecture. Unfortunately, the current architecture (separate FindQuery) is stiff and we can't improve it at all. So, the task should be to refactor rather than to optimize.

Performance problems found

Please consider each and one of this in separate tasks eventually and work on the orthogonally after fixing #9030.

#22 Updated by Stefanel Pezamosca 6 days ago

I will reanalyze each point to select what should go in 9030c and what should go in separate tasks. Some of the points above are already changed a little after I did some more tests earlier.

#23 Updated by Alexandru Lungu 5 days ago

  • Related to Bug #11953: Consider using the AST transformation pipeline philosophy for FQL preprocessing added

#24 Updated by Stefanel Pezamosca 5 days ago

Thank you for the review. 9030c will contain only the functional changes, plus the removal of the buffer's offEnd and sortIndex. The performance work moves to other tasks.

Review points

  • 9030a. My plan cited the wrong note: the finding that the reference is not a record is #9030-9, not #9030-8. 9030a keeps a Record as the cursor. 9030b implements #9030-9 with a virtual record, and 9030c builds on it. From 9030a I take only the rest: the cursor is used by FIND statements only, it is reset when the outermost scope of the buffer opens, and the BUFFER:FIND-* methods leave it alone.
  • Snapshot. The cursor keeps a Record.snapshot() copy of the record found, made before a setter changes an indexed field. The buffer's snapshot can't be used: any query which loads a record replaces it, including those which must not move the cursor.
  • FQLPreprocessor. It collects the comparisons of the FIND bracket (equalities, ranges and indexed BEGINS among the AND-ed terms) in a separate list, getFindMatches(), by running the existing property matcher on each term. The shared property matches, used by the H2 direct access, the cache invalidation and the server-side joins, stay unchanged.

Scope

  • FindSupport, one cursor per buffer: the last record found, or a place between records after a FIND which found nothing, computed when a later FIND NEXT/PREV needs it.
  • FOR statements and FINDs in a PRESELECT block move the cursor. OPEN QUERY, dynamic queries, BREAK BY, CAN-FIND and the BUFFER:FIND-* methods don't.
  • RandomAccessQuery navigates FINDs from the cursor. FIND CURRENT on a permanent table and an ambiguous FIND UNIQUE (first match) move it.
  • AdaptiveFind uses its preselected results only while the cursor is where the loop left it, else it continues from the cursor.
  • StatementTemplate gets values for a place before or after all the values of an index field (constant true/false conditions), instead of the "last record" query of 9030b.

Removing offEnd and sortIndex from the buffer

With FINDs on FindSupport, only RandomAccessQuery reads them, but they still carry state between queries: setRecord() (RandomAccessQuery, PreselectQuery), the buffer's off-end listener (block start and end), AdaptiveQuery.earlyInvalidate() (start from the first/last record), AdaptiveQuery.repositionByID(), and a worker query created after the AdaptiveQuery results ran off-end (without it, 9030b looped endlessly).

  1. Each RandomAccessQuery keeps its own off-end state, as PreselectQuery already does.
  2. AdaptiveQuery passes it to the worker query it creates (simple and compound): the off-end of the results it replaces, or the start from the first/last record after an early invalidation.
  3. RecordBuffer loses sortIndex, offEnd, the reversible off-end and their methods; setRecord() loses its sortIndex, offEnd and inverseSorting parameters. The buffer's snapshot stays (AdaptiveQuery.quickRevalidate() uses it).

This affects every query which navigates with RandomAccessQuery, so it will be a separate commit, with new tests for invalidated AdaptiveQuery instances (early, after off-end, several tables). Related: #7004, #10026, #11686.

Tests

Each test comes from a 4GL probe run on OpenEdge: the probe output becomes the expected values, and every test passes on OpenEdge. The tests run the same way on FWD (database table testdb, or a temp-table).

Class Tests Pass on trunk What they cover
findcursor/FindCursorNotFoundTest 53 12 The place left by a FIND FIRST/LAST/UNIQUE/NEXT/PREV which finds nothing, then FIND NEXT/PREV: equality prefixes, open and closed ranges, BEGINS, multi-field, descending and mixed-direction indexes, brackets with filtered records, unknown values, date and case-insensitive keys, a function of the field or an OR (whole index), a value on the left, repeated misses, records created after the miss, an empty table.
findcursor/FindCursorForBlockTest 30 22 FOR EACH/FIRST/LAST (BY, USE-INDEX, WHERE, LEAVE, off-end, joins, nesting) and FINDs in DO/REPEAT PRESELECT move the cursor; BREAK BY, OPEN QUERY PRESELECT, dynamic queries and a FOR FIRST which finds nothing don't.
findcursor/FindCursorStatementsTest 27 16 CREATE, RELEASE, DELETE, CAN-FIND, BUFFER:FIND-* methods and QUERY GET don't move the cursor; FIND by ROWID and an ambiguous FIND UNIQUE do; FIND CURRENT on a temp-table doesn't. Keys changed after the FIND, duplicate keys, a table without indexes, one cursor per buffer.
findcursor/FindCursorScopeTest 11 7 A strong scope (DO FOR) resets the cursor, REPEAT FOR iterations and weak scopes don't, UNDO doesn't restore it; BUFFER parameters and BY-REFERENCE tables share it, a table passed by value has its own.
findcursor/FindCursorIndexTest 9 6 FINDs on another index, after a record found or after a miss; USE-INDEX; alternating indexes.
findcursor/FindCursorLoopTest 14 7 Looped FIND NEXT/PREV (AdaptiveFind) when other statements move the cursor (FOR EACH, QUERY GET, another FIND), with records created, deleted, released or with changed keys, duplicate keys.
findcursor/FindCursorPermanentTest 41 28 The same on a database table: FIND CURRENT moves the cursor, lock upgrades, a deleted cursor record, changes through another buffer, secondary indexes, loops.
findloop/FindAfterMissChangesTest 10 0 Records created in the bracket, or deleted, after a FIND NEXT/PREV which found nothing; database and temp-table.
findloop/FindLoopChangesTest 36 32 FIND NEXT loops (NEXT, WHERE and WHILE forms) which create, update or delete records ahead, or move their keys ahead, behind or out of the bracket; database and temp-table.

Moved out of #9030

Item Task
NEXT/PREV statements which can't use the index. To move past the current key of a field which may be unknown, a FIND NEXT/PREV (and every query which navigates by index position) uses (k > ? or k is null), which is not an index range scan. E.g. FIND NEXT alternating two indexes takes 244 ms in FWD, 4 ms in OpenEdge. Proposal: split it into a range statement and a k is null statement, no is null part for mandatory and primary key fields, skip the statements which can find nothing (a range over a field pinned by an equality to the same value, an empty bracket after a FIND which found nothing), and cache the FQL text built per FIND in StatementTemplate. This changes the SQL of all these queries, not only FIND. New task. Related: #6834 (the split was discussed there; closed with the functional fix only), #4556 (index walk over unknown values), #10729 (no is null part for non-nullable fields in other queries).
Fewer round trips for FIND NEXT/PREV. Each FIND NEXT/PREV runs its own SQL statement, and a looped NO-LOCK FIND (AdaptiveFind) preselects only ids and reloads each record by id. Reading the next records ahead (in growing pages) or preselecting full records would save the round trips, but the records read in advance may miss later commits of other sessions, e.g. the change of a non-indexed field. #11723: OpenEdge prefetches FOR EACH NO-LOCK and scrolling queries, but never FIND FIRST/NEXT; each FIND sees the other sessions' commits (#11723-15, #11723-18). Any batching for FIND must therefore stay invisible: a record read in advance must not be served once another session committed a change to it. Related: #10436 (sequential FINDs detected at runtime; the prototype gained nothing).
Constantly false conditions. A query whose WHERE clause is constantly false still runs its SQL statement. The same holds for a FIND NEXT/PREV statement whose position condition is constantly false (a place before or after all the values of an index field). #1919 (WHERE TRUE and WHERE FALSE in the preprocessor; a query which can't find anything should not run).
Temp-table FastFindCache. When a procedure which used a local buffer of a shared temp-table ends, all the FastFindCache results of the temp-table are dropped, at a cost proportional to their number, although other buffers still use the temp-table. Proposal: drop them only when the temp-table instance (multiplex) is no longer in use. New task. Related: #11569, #11638 (FastFindCache invalidation).
Unknown value in queries on an index. OpenEdge sorts the unknown value high only in the comparisons which bracket the index of the query; the other comparisons are evaluated as 4GL expressions, where f < ? is unknown and f <= ? matches only unknown values. FWD gives the index meaning to every comparison of an index field. About 80 tests fail on trunk (TestQueryUnknownValue, FindUnknownBracketTest, TestJoinQueries). The fix is in FQLPreprocessor and in the index fields QueryComponent passes for OPEN QUERY; FOR EACH needs the index from conversion, and UPPER(field) = "A" a conversion change. New task, after #11953 (it changes FQLPreprocessor). Related: #11108 (the f <= ? quirk on index fields), #11330 (the quirk applies only to the index prefix), #10729.
Primary key order of the dirty database statements. When the dirty database dialect differs from the primary one, FQLHelper parses separate sort criteria for the dirty database statements of a permanent table, and their primary key direction may differ from the one of the primary statements (SortCriterion.parse() caches the criteria by sort clause, ignoring the direction). A FIND PREV on a non-unique index can then return a later record. Seen while testing 9030c; no ABLUnit test shows it yet. New task, which needs a reproducer first.
Locks. FWD reports error 142 instead of 396 when a record found NO-LOCK is updated, and it allows the update when the session already holds a lock on the record: RecordBuffer.upgradeLock() checks the lock of the session, not the lock of the buffer. 26 of the 37 FindLockChangeTest tests fail, on trunk too. New task. Related: #10822 (no error 396 when the assigned value is unchanged).

The task column is subject to change, waiting for feedback.

#25 Updated by Alexandru Lungu 5 days ago

NEXT/PREV statements which can't use the index. To move past the current key of a field which may be unknown, a FIND NEXT/PREV (and every query which navigates by index position) uses (k > ? or k is null), which is not an index range scan. E.g. FIND NEXT alternating two indexes takes 244 ms in FWD, 4 ms in OpenEdge. Proposal: split it into a range statement and a k is null statement, no is null part for mandatory and primary key fields, skip the statements which can find nothing (a range over a field pinned by an equality to the same value, an empty bracket after a FIND which found nothing), and cache the FQL text built per FIND in StatementTemplate. This changes the SQL of all these queries, not only FIND.

I think the new implementation for PagedResults can help. It retrieves a list of N records starting from a reference row (or virtual row) comparing to a ProgressiveResults that uses an OFFSET. If N is something like 10, the (k > ? or k is null) syntax will be used to retrieve a page of records in advance, so we may afford wasting time with the or granted that we gain N rows and not only one. This is basically the AdaptiveFind embedding we were talking about.

Fewer round trips for FIND NEXT/PREV. Each FIND NEXT/PREV runs its own SQL statement, and a looped NO-LOCK FIND (AdaptiveFind) preselects only ids and reloads each record by id. Reading the next records ahead (in growing pages) or preselecting full records would save the round trips, but the records read in advance may miss later commits of other sessions, e.g. the change of a non-indexed field.

The goal for the moment is to improve #11686 that uses FIND with NO-LOCK. So the current projection vs full record is reasonable for the performance task we aim for now.

#26 Updated by Stefanel Pezamosca 5 days ago

So, I have 9030c created locally and functional, but it isn't pushed yet.

On the 18 scenarios of a performance benchmark I made, 9030c is within about 20% of trunk in 16 (best of 3 runs and first run). Two temp-table scenarios are 4-5 times slower:

Scenario (1000 iterations) trunk 9030c OpenEdge
FIND FIRST which finds nothing, then FIND NEXT 9 ms, wrong records 41 ms 3-6 ms
FIND NEXT alternating two indexes 62 ms, 198 records found 270 ms, 991 records found 3-6 ms, 991 records found
Trunk is faster because it doesn't follow the cursor:
  • After the FIND FIRST which finds nothing, the looped FIND NEXT (AdaptiveFind) continues in its own preselected results.
    The whole loop runs one statement, but each FIND NEXT returns the record before the miss instead of the one after it (k = 2i instead of 2i + 2).
  • With alternating indexes, trunk returns the same records as OpenEdge until a FIND NEXT runs off the end of the index (iteration 199). Then the remaining 801 FIND NEXTs find nothing and run no statement.

9030c returns the same records as OpenEdge in both, but it is slower because of how each FIND NEXT is written in SQL. To get the next record after the cursor, it asks for k > ? or k is null (a value of k may be unknown). H2 can't jump to the right place in the index for this condition: it reads the records one by one from the start of the temp-table until it reaches the cursor. So the further the cursor is, the slower each FIND NEXT gets. For example, in a temp-table of 10000 records, finding the record after k = 5000 reads 2501 rows with the OR, and only 2 rows with k > ? alone.

An experiment which splits that statement into a k > ? statement and a k is null statement (for all queries), skips the statements which can find nothing and caches their FQL text brings the two scenarios to 20 ms and 8 ms.

So, I could do the following.
  1. #9030: the removal of offEnd and sortIndex from the buffer.
  2. #9030: Maybe implement the (k > ? or k is null) split for the FIND statements only (the cursor queries), so the SQL of the other queries doesn't change.
  3. Separate task: the split for all the queries which navigate by index position.
  4. #11686, after #9030 and #9078: the FIND in the buffer, with one AdaptiveFind per buffer

#27 Updated by Alexandru Lungu 5 days ago

I am not sure I understand why trunk and 9030 diverge.
Is trunk faster because it uses AdaptiveFind and gathers more records in memory vs 9030c that does look-ups one by one? I don't fully understand why k > ? or k is null is a problem just in 9030c.

#28 Updated by Stefanel Pezamosca 5 days ago

Alexandru Lungu wrote:

I am not sure I understand why trunk and 9030 diverge.
Is trunk faster because it uses AdaptiveFind and gathers more records in memory vs 9030c that does look-ups one by one? I don't fully understand why k > ? or k is null is a problem just in 9030c.

Trunk is faster because it does not find the right records or enough records.

#29 Updated by Stefanel Pezamosca 5 days ago

More detail:
  • FIND FIRST that finds nothing, then FIND NEXT: trunk's AdaptiveFind keeps reading its own preselected results and ignores that the FIND FIRST moved the cursor. The whole loop is 1 statement, but every FIND NEXT returns the record before the miss (k = 2i instead of 2i + 2). 9030c follows the cursor, like OpenEdge: one lookup per FIND NEXT, 2 statements per iteration.
  • FIND NEXT alternating two indexes: trunk also looks up one record at a time here, with the same 3 statements per iteration as 9030c. It is faster only because it runs off the end at iteration 199: the other 801 FIND NEXTs find nothing and run no statement (198 records found, OpenEdge 991).

#30 Updated by Alexandru Lungu 5 days ago

Trunk is faster because it does not find the right records or enough records.

Bugs are the best performance improvements :) I understand.

More detail:

Got it.

#31 Updated by Stefanel Pezamosca 4 days ago

  • % Done changed from 50 to 80

Revision 16789 - I removed the sortIndex and offEnd of the last query from the buffer. Each query keeps its own off-end now, so it no longer passes from one query to another through the buffer:

  • RandomAccessQuery keeps the off-end of its last navigation and a reversibleOffEnd flag. Until the next navigation or reset, a PREV reads a FRONT off-end as BACK, so it starts from the last record. A worker query of an AdaptiveQuery gets its starting off-end through setOffEnd().
  • AdaptiveQuery keeps fetchOffEnd, the off-end of its last fetch. A load() counts as a fetch. Unlike offEnd, it survives the reset of the results on invalidation, and it is cleared when the query is initialized, opened or reset (earlyInvalidate()). A worker query, or each component of a compound worker, starts from it (startWorker()), or from either end (the first record for NEXT, the last for PREV) if there was no fetch yet. After a reposition, the worker continues from the repositioned buffers.
  • setRecord() no longer takes the sort index, off-end and inverse sorting. The buffer's off-end listener no longer sets an off-end. I removed RecordBuffer.getOffEnd() and setOffEnd(), resetSortIndex(), the reversible off-end methods, and QueryComponent.getSortIndex().

Future revision 16790 - These continue the FIND cursor (FindSupport) work:

After a round of AI-assisted self-review, I managed to fix several issues in 9030c. There are no regressions in my #9030 unit tests and 9030c fixes several additional issues. However, another review pass might uncover further problems.

  • AdaptiveFind: I dropped its own worker query and the dynamic flag. Before a FIND NEXT, if another statement set or moved the cursor (its version changed), I force the invalidation of the preselected results. A quick revalidation would restart them from the first record. The dynamic mode of AdaptiveQuery then continues from the cursor, and it can switch back to preselect later. A FIND FIRST still ignores the cursor; it only checks for uncommitted changes.
    • A FIND FIRST/NEXT that finds nothing in preselect mode records its index bracket on the cursor (setFindMiss()), using the FQL helper of a query with the same WHERE clause, and invalidates the results. In dynamic mode, the dynamic query records it.
    • When switching back to preselect, the results skip the record the query visited last. For a FIND, that is the cursor's record, not the record in the buffer, because a FIND releases the buffer before it fetches. So I added AdaptiveQuery.getVisitedRecord() and overrode it in AdaptiveFind.
  • FindSupport:
    • The cursor copies its record when any buffer reports a change to it, not only the FIND's buffer and not only before a setter of an indexed field. The copy is the one the reporting buffer already made for the change event (RecordBuffer.getRecordBeforeChange()). If there isn't one, it is a snapshot with the old values of the changed fields put back.
    • Finding the same record again (FIND CURRENT) doesn't move the cursor or change its version, so the preselected results of an AdaptiveFind stay valid.
    • I replaced setOffEnd() with the misses above, plus two new methods:
      • setFirstMatch(): an ambiguous FIND UNIQUE sets the first match as the cursor, as OpenEdge does, but the query runs only when a later FIND uses the cursor (most often none does).
      • setLast(): puts the cursor on the last record of a sort clause, as a FOR EACH over all records leaves it.
    • The arguments of a miss no longer include the multiplex IDs generated for the referenced buffers (FQLPreprocessor.removeGeneratedMultiplexIDs()), so they match the WHERE clause as written, like the FIND matches.
  • deleteForAll(): the FOR EACH it emulates leaves the cursor on the last record deleted, in primary index order (or by primary key if there is no primary index). BufferImpl sets it with setLast(), and the emulating query itself doesn't move the cursor.

Open points I still need to check before the push:

  1. Copy timing. The change is reported after the ASSIGN triggers, and after the batch in an ASSIGN statement, so a FIND NEXT inside an ASSIGN trigger sees the cursor's record already changed. The old setter hook copied it before the change. A buffer that isn't active doesn't copy it either.
  2. Lazy first match. The query of setFirstMatch() runs when the cursor is used, so it sees the data as it is then: if the first match was deleted or changed, or a new record sorts before it, the cursor is not where OpenEdge leaves it. If that query finds nothing, the cursor falls back to the record found before the FIND UNIQUE, because setFirstMatch() doesn't clear it.

I'm currently running the full ABLUnit suite and the performance probes I have available. The performance results will take some time, as I want to compare both cold-run times and average warm-run times for each individual scenario.

I'll post a report with the results, fix the remaining open points and then commit and push 9030c, which currently exists only in a local unbound branch.

#32 Updated by Alexandru Lungu 4 days ago

Copy timing. The change is reported after the ASSIGN triggers, and after the batch in an ASSIGN statement, so a FIND NEXT inside an ASSIGN trigger sees the cursor's record already changed. The old setter hook copied it before the change. A buffer that isn't active doesn't copy it either.

Also account for FIND triggers and their lifecycle. I wonder if we can return error or no-apply from FIND trigger and stop the buffer from being loaded. In that case, will the cursor be updated?

#33 Updated by Stefanel Pezamosca 4 days ago

I ran the #9030 ABLUnit tests and the FIND benchmarks on OpenEdge, trunk rev 16787 and 9030c/r16790.

ABLUnit tests

Suite (# Passed) Tests OpenEdge trunk 9030c
tests/persistence/findcursor 190 190 99 190
FindAfterMissChangesTest 10 10 0 10
FindLoopChangesTest 36 36 32 36
FindMandatoryIndexTest 10 10 9 10
FindLockChangeTest 37 37 11 11
FindUnknownBracketTest 77 77 37 37
TestQueryUnknownValue 98 98 60 60
  • Full ABLUnit suite that I was testing on related with other tasks (3477 tests): trunk 739 failures, 9030c 631; 108 fixed, none broken.
  • The FIND cursor probes I created give the same results as OpenEdge; trunk differs in 7 of 8.
  • The remaining failures are the same on trunk and belong to separate tasks (or should it be fixed now ?):
    • FindLockChangeTest (26): error 142 instead of 396 on updating a NO-LOCK record, and the update checks the session's lock instead of the lock of the FIND which found the record.
    • FindUnknownBracketTest (40) and TestQueryUnknownValue (38): comparisons with the unknown value in indexed queries.

Performance

10 runs of each scenario per session, a fresh FWD server for each session. Cold is the mean of the first runs, warm the mean of the other runs in Milliseconds.

All the scenarios in one session

10 sessions per build (cold: 10 samples, warm: 90).

Scenario OpenEdge trunk cold 9030c cold trunk warm 9030c warm
tt.findFirstHit 22.9 134.4 135.1 44.7 43.5
tt.findFirstMiss 20.5 68.5 66.3 24.5 29.2
tt.findFirstMissFiltered 46.6 108.7 123.3 50.4 53.4
tt.findNextAfterMiss 3.6 29.1 135.0 9.5 42.1
tt.findNextLoop 11.6 48.5 71.8 17.1 19.8
tt.findNextLoopRelease 12.2 17.0 17.7 14.0 13.5
tt.findNextLoopWhere 13.5 149.7 119.9 49.4 46.4
tt.forEach 9.0 17.3 17.7 14.3 11.8
tt.forEachUpdateKey 37.1 146.8 151.1 68.6 68.9
tt.findAlternateIndex 4.1 82.4 (wrong result) 299.9 62.3 (wrong result) 286.6
tt.nestedFindLoops 25.9 135.4 126.0 87.5 84.1
db.findFirstHit 11.7 556.6 550.6 184.4 176.1
db.findFirstMiss 10.8 282.0 275.7 9.0 8.8
db.findFirstMissFiltered 23.6 190.8 181.9 8.1 7.8
db.findNextLoop 6.1 226.6 229.1 159.7 158.9
db.findNextLoopRelease 5.8 171.4 156.3 154.7 154.0
db.forEach 4.2 17.1 16.0 13.3 13.0
db.nestedFindLoops 6.2 379.4 390.2 345.6 339.9
tt.findFirstNextLoop 11.9 31.1 32.1 17.8 18.0
db.findFirstNextLoop 5.4 188.7 178.3 156.9 156.7
db.findNextFindCurrent 14.6 168.3 168.0 155.2 159.2

Each scenario alone

Only one scenario per session, so its first run also pays the class loading and JIT compilation which the earlier scenarios pay in the full probe. 5 sessions per scenario and build (cold: 5 samples, warm: 45).

Scenario OpenEdge trunk cold 9030c cold trunk warm 9030c warm
tt.findFirstHit 23.4 138.6 132.0 45.8 43.2
tt.findFirstMiss 20.2 102.2 119.0 34.2 37.5
tt.findFirstMissFiltered 44.4 195.2 214.0 48.1 52.7
tt.findNextAfterMiss 3.6 72.4 186.6 15.3 51.8
tt.findNextLoop 12.0 67.2 72.0 32.6 36.1
tt.findNextLoopRelease 12.2 72.2 77.2 31.8 31.8
tt.findNextLoopWhere 13.2 215.8 233.6 48.8 53.3
tt.forEach 9.4 55.0 58.2 29.3 30.1
tt.forEachUpdateKey 35.6 217.8 225.8 72.9 72.5
tt.findAlternateIndex 4.4 184.8 (wrong result) 388.8 66.0 (wrong result) 215.2
tt.nestedFindLoops 25.6 344.4 357.0 88.1 87.6
db.findFirstHit 11.8 584.4 588.0 183.5 173.6
db.findFirstMiss 10.6 530.4 549.0 14.5 17.4
db.findFirstMissFiltered 23.6 469.0 469.4 15.5 18.9
db.findNextLoop 6.2 411.8 427.0 162.6 161.1
db.findNextLoopRelease 7.0 434.8 414.8 164.4 164.6
db.forEach 4.2 52.8 50.4 21.4 21.6
db.nestedFindLoops 6.4 700.6 693.2 362.6 355.0
tt.findFirstNextLoop 10.8 98.0 99.6 28.6 28.6
db.findFirstNextLoop 5.8 475.2 464.6 169.5 164.9
db.findNextFindCurrent 14.8 514.0 509.0 182.9 173.3

Overall, except the tests already mentioned the timings are pretty similar and within noise caused by the system load.

#34 Updated by Stefanel Pezamosca 1 day ago

9030c: changes since r16797 (committed in 9030c/r16798)

  • A FIND trigger can reject a record. If the trigger does RETURN ERROR or ends with an error, the record is not loaded. FIND FIRST/LAST/NEXT/PREV, FOR EACH and PRESELECT move on to the next record, and FIND UNIQUE finds nothing. The FIND cursor still lands on the rejected record, so the next FIND NEXT/PREV continues after it.
  • Error 2873. A FIND on a buffer from inside that buffer's own FIND trigger now fails with error 2873. With NO-ERROR, the error is only recorded. The message text was also missing a space.
  • FIND NEXT/PREV ... NO-ERROR. When nothing is found, ERROR-STATUS:ERROR is now set, as in OpenEdge. This doesn't apply inside a PRESELECT block.
  • RETURN ERROR in database triggers. RETURN ERROR, or an error that leaves the trigger, now rejects the event. Before, RETURN ERROR aborted and the error was ignored. The conversion no longer warns about RETURN ERROR in a database trigger. If a CREATE trigger rejects the record, the new record is now discarded.
  • FIND cursor and changed records. When an indexed field of the record under the cursor changes, through any buffer, the cursor keeps a copy of the record as it was before the change. The copy is taken before validation, flush or any trigger runs. After an ambiguous FIND UNIQUE, the cursor is placed on the first matching record right away.

Observed FIND trigger lifecycle:

  • When it fires. The FIND trigger runs every time a record is loaded into the buffer: by FIND, FOR EACH, GET or FIND-FIRST. It does not run for FIND CURRENT or CAN-FIND.
  • Rejecting a record. If the trigger does RETURN ERROR or ends with an error, the record is not loaded. FIND FIRST/LAST/NEXT/PREV, FOR EACH, GET NEXT and FIND-FIRST move on to the next record, and FIND UNIQUE finds nothing.
  • NO-APPLY. RETURN NO-APPLY does nothing in a FIND trigger, the same as in OpenEdge. The record is loaded as usual.
  • The cursor. The FIND cursor still lands on a rejected record, also inside FOR EACH, so the next FIND NEXT/PREV continues after it. If the trigger raises STOP, the buffer is left empty and the cursor stays on the rejected record.
  • FIND inside the trigger. A FIND on the same buffer from inside its own FIND trigger fails with error 2873.

Known differences from OpenEdge, still open:

  • When an unhandled error in the trigger rejects the record of a FIND-UNIQUE method, FWD also shows error 138; OpenEdge shows nothing.
  • When a WRITE trigger rejects a change, FWD does not undo the change. This happened before these changes too.
  • Inside a buffer's own FIND trigger, FOR EACH and GET NEXT on that buffer don't report error 2873 as OpenEdge does, and FIND-FIRST returns no where OpenEdge returns yes.
  • An error in a FOR EACH body inside the trigger is caught by an enclosing DO ON ERROR UNDO, LEAVE. In OpenEdge it escapes that block and rejects the record.

#36 Updated by Stefanel Pezamosca about 12 hours ago

  • % Done changed from 80 to 90

9030c is at r16800. Since my last note (r16798), r16799 and r16800 add review fixes and the #8633 bulk-delete fix. The full ABLUnit suite I'm using still has the same failures (631 of 3518), with nothing newly broken.

What changed since the last note:

Database triggers behave as on trunk again, except where the FIND cursor needs a change.
The error-handling rework for database triggers in r16798/r16799 went beyond this task and caused regressions, so r16800 reverts it. These parts stay:
  • RETURN ERROR in a database trigger rejects the event. On trunk it aborts with IllegalArgumentException.
  • A record rejected by its FIND trigger is skipped, and the FIND cursor moves past it. This also holds when the FIND's block has a CATCH.
  • A FIND on the trigger's own buffer raises error 2873. A FOR or GET on that buffer only shows 2873 and keeps the record, as on trunk.
  • A DELETE vetoed by its trigger now raises ERROR, or sets ERROR-STATUS:ERROR with NO-ERROR. Before, the veto was silent and left an error pending for later statements.
FIND cursor fixes from the code review
  • The cursor keeps its place when UNDO or a buffer copy rewrites the record it is on.
  • A FIND ... OF that finds nothing continues from the right place in the index. 9030c had a regression here compared to trunk.
  • Only records that a FIND cursor is on are tracked for changes, and a buffer releases its cursor when it goes out of scope, so there is less work on every update.
  • FINDs on the same index share the cursor position correctly, even when their sorts differ only in the direction of the primary key.
  • No cursor query runs for an empty temp-table.
  • From r16799:
    • The cursor works with BUFFER parameters, with the looping delete, and when INT64 values are compared with INTEGER fields.
    • REPOSITION and joins skip rejected records, and the FIND-* methods report 2873.
    • Bulk deletes no longer leak change listeners.
Bulk deletes (#8633)
  • FWD turns a delete-only FOR EACH ... : DELETE ... END. loop into a single bulk delete. That delete now leaves the FIND cursor on the last record deleted, in the loop's sort order, as in OpenEdge.
  • Conversion passes the loop's sort (Buffer.deleteFor, Buffer.deleteForAll).
    • A BREAK BY loop doesn't move the cursor.
    • A loop sorted by an expression is no longer turned into a bulk delete.
  • This also works for tables bound with BY-REFERENCE or BIND to a table with different field names.
  • It needs no extra query: the cursor is taken from the record scan the delete already does. Bulk deletes with a WHERE clause are about 25% faster than in r16798.
  • Code converted before this still works, with the old cursor behaviour, until it is reconverted.

I'll continue with doing one more self-review pass to make sure 9030c is in a stable state, fixing what 9030c should fix and not introducing new regressions compared with trunk.

Also available in: Atom PDF