Bug #10806
Presort queries can sometimes dynamically find records created after opening the query
90%
History
#1 Updated by Alexandru Lungu about 1 year ago
Part of #10737, I discovered that BY clause index overlapping resolution is influenced by WHERE clause. This happens for any kind of query.
For presort however, the regression tests shown that FWD trunk doesn't properly find new records after OPEN QUERY, which apparently is possible. The scenario is rather limited in the sense that it happens only if the BY clauses partially overlap an index, but the WHERE clause ensures that this is enough (with equality checks on leading components).
I committed testcases/1813 with tests/query/index/ByAndWherePresortTestCases.cls. Check with tests/query/index/ByAndWherePreselectTestCases.cls for non-presort cases that work fine in trunk.
#4 Updated by Artur Școlnic about 2 months ago
- Status changed from New to WIP
- Assignee set to Artur Școlnic
#5 Updated by Artur Școlnic about 2 months ago
- Status changed from WIP to Review
- reviewer Alexandru Lungu added
Summary¶
A presort query took a fixed, preselected snapshot of its results, so records created or changed after the query opened were never seen. This makes PresortQuery change-sensitive by inheriting AdaptiveQuery instead of PreselectQuery, so it can switch to dynamic retrieval when the session holds pending changes.
Three files, +91/-10.
The change¶
PresortQuery now extends AdaptiveQuery. Its createResults override became createHighLevelResults (see below), assembleOrderByClause was split so the body keeps its checked exception while the override matches AdaptiveQuery's signature, and executeQuery now asks for a preselected result set explicitly on the template-rowid path.
AdaptiveQuery gained one hook, createHighLevelResults, called from createResults in place of the inline super.createResults(adapter). The default implementation is exactly what it replaced, so AdaptiveFind and every other caller are unaffected; PresortQuery is the only override.
PreselectQuery gained executePreselected, split out of executeQuery, so a subclass whose parent executes lazily can still ask for a fully materialised result set. Java offers no way to reach a grandparent implementation directly.
Why the results wrapper moved¶
This is the part worth a close look. SortedResults materialises and sorts in its constructor and never re-reads its delegate. Building it per delegate change meant that once the query went dynamic, every next() rebuilt the presorted view over a cursor that had already moved, and rows were lost. Building it once per open, over the adapter rather than the delegate, is what AdaptiveQuery already does for its own results; the hook simply lets a subclass participate in that.
The wrapper is cached for the life of an open, and cleared on re-open through open() -> close() -> cleanup().
Testing¶
Patterns covered, in FWD and against an OpenEdge reference where the semantics were in question:
- an uncommitted CREATE that matches the predicate, read from another buffer
- an uncommitted UPDATE that moves a row into or out of the predicate
- a pending change to a field the query SORTS on, and to a field it BREAKS on, where the row still matches the predicate so only its position and group can be wrong
- re-opening a presort statement, which must rebuild the presorted set rather than replay the previous open
- an existing corpus of BY/WHERE index-selection queries in both presort and preselect shapes, as a regression guard
| Suite | Trunk | With change |
|---|---|---|
| Presort visibility (45 tests) | 24 pass / 21 fail | 44 pass / 1 fail |
| BY/WHERE index selection (368 tests) | 351 pass / 17 fail | 351 pass / 17 fail |
The one remaining failure in the first suite fails on trunk as well and is out of scope here. The BY/WHERE failing-method set is identical to trunk, method for method. One test was added for the re-open case and passes, bringing that suite to 46.
Alex, please review the first iteration of the implementation in 10806a.
#6 Updated by Artur Școlnic about 1 month ago
- Status changed from Review to WIP
Will do a few more rounds of self review and generate more tests.
#7 Updated by Artur Școlnic about 1 month ago
I keep finding failing testcases and fixing them, will post here when there are no issues to be solved.
#8 Updated by Artur Școlnic about 1 month ago
OE testing is conclusive when it comes to the queries that are converted to FWD PresortQuerie. Result rows membership is additive, but never subtractive. Once resolved, nothing is removed, but new matches ahead of the cursor are picked up. That's not a preselected snapshot, and it's not a live cursor. It's a frozen-size list whose forward contents are reevaluated. The bottom line is that simply inheriting the behavior of AdaptiveQuery is not enough, I am working on matching it.
#9 Updated by Alexandru Lungu about 1 month ago
Once resolved, nothing is removed, but new matches ahead of the cursor are picked up. That's not a preselected snapshot, and it's not a live cursor.
This sounds like the behavior of a scrolling query, that uses a Cursor. This is implemented for any DynamicQuery: AdaptiveQuery and CompoundQuery.
#10 Updated by Alexandru Lungu about 1 month ago
Artur, please take a step back and do a design plan for this porting. The simple reroute of the extentds from PreselectQuery to AdaptiveQuery doesn't sound enough. There are >10 use-cases that are handled through the P2JQuery API and we should rather have them documented, before porting. I will provide some examples:
- scrolling vs non-scrolling vs forward-only behavior
- invalidation:
- revalidation
cacheOnReval- cross-session invalidation due to foreign commits
- dirty-share honoring including
earlyInvalidation
- results delegation (there are plenty of results, results-adapter, highlevelresults instances). We need to standardize this to be able to make room for
PresortQuery- integration with
ScrollingResults,ForwardResults,PagedResultsorProgressiveResults
- integration with
- FQL composition:
- _multiplex consideration
- breakValue consideration
- sort order consideration and trailing recid
- caching on session commit if result-set is forward only (due to same session-commit).
- consideration of
PresortCompoundQuery - locks, triggers, buffers loading, related buffer handling (
OFsyntax) and embedded buffers (embeddedCAN-FIND) - projection queries vs full queries
- plenty of code has
instanceof AdaptiveQueryor similar.
This task doesn't resonate as a simple porting, but as a very careful surgery that requires a comprehensive plan and skilled surgeon.
Tests are most welcome, but please make them on groups. We know before hand what testing should tests and we can drive the AI to generate very quirky tests to capture revalidation, cacheOnReval and other things like this.
OE testing is conclusive when it comes to the queries that are converted to FWD PresortQuerie. Result rows membership is additive, but never subtractive. Once resolved, nothing is removed, but new matches ahead of the cursor are picked up. That's not a preselected snapshot, and it's not a live cursor. It's a frozen-size list whose forward contents are reevaluated. The bottom line is that simply inheriting the behavior of AdaptiveQuery is not enough, I am working on matching it.
I would have qualified this as the "scrolling" behavior. If not please ignore my comment.
If it is the "scrolling" behavior, than this should be leveraged from the AdaptiveQuery and not reimplemented in PresortQuery.
I would say that most of the bugs that occur in the process is a gap in linking PresortQuery to the AdaptiveQuery parent, as each functionality discovered is most probably already implemented in the super-class, but the sub-class fails to leverage it.
#11 Updated by Artur Școlnic about 1 month ago
First I want to record the 4GL constructs that convert to PresortQuery and their measured behaviour.
| 4GL construct | OE resolution | OE change sensitive | FWD trunk resolution | FWD trunk change sensitive |
|---|---|---|---|---|
BREAK BY <indexed field> |
walks the index, incremental | yes | materialised at open | no |
BREAK BY <fld1> BY <fld2> (= the index) |
walks the index, incremental | yes | materialised at open | no |
BREAK BY <unindexed field> |
materialised at open | no | materialised at open | no |
BY (<computed expression>) |
materialised at open | no | materialised at open | no |
BY <field> DESCENDING (against an ascending index) |
materialised at open | no | materialised at open | no |
BY <field> BY <field> (same field twice) |
materialised at open | no | incremental | yes |
Resolution is read from NUM-RESULTS sampled immediately after OPEN QUERY, before any fetch: equal to the row count means the set was materialised at the open, 0 means resolution is incremental. Change sensitive is whether a row committed after the open, sorting past the end of the set, is delivered by the continuing walk. Both post-open rows are committed, so no cell below depends on any dirty-share setting.
OpenEdge applies one rule: can the database produce this order by walking an index? If it can, the walk is incremental and picks up rows committed ahead of the point resolution has reached. If it cannot, an unindexed break field, a computed expression, a descending sort against an ascending index, a duplicated sort field, the set is materialised at the open and nothing committed afterwards is seen.
FWD trunk diverges in both directions.
- It materialises the two shapes OpenEdge walks lazily. That is #10806.
- It resolves
BY <field> BY <field>incrementally and change-sensitively, where OpenEdge materialises it. This is the opposite divergence.
#12 Updated by Alexandru Lungu about 1 month ago
I think OE change sensitive is matching the FOR EACH conversion decision on FWD (to AdaptiveQuery or PreselectQuery).
In other words, does it make sense to have PresortQuery still extend PreselectQuery and have a separate AdaptivePresortQuery that extends AdaptiveQuery?
#13 Updated by Teodor Gorghe about 1 month ago
Or maybe something that fits in the Composite design pattern, or a Query which uses a delegate (switch between AdaptiveQuery and PreselectQuery, setDelegate).
#14 Updated by Artur Școlnic about 1 month ago
After reanalyzing the tests, the table above has to be corrected.
| 4GL construct | OE resolution | OE change sensitive | FWD trunk resolution | FWD trunk change sensitive |
|---|---|---|---|---|
BREAK BY <indexed field> |
walks the index, incremental | yes | materialised at open | no |
BREAK BY <fld1> BY <fld2> (= the index) |
walks the index, incremental | yes | materialised at open | no |
BREAK BY <unindexed field> |
materialised at open | no | materialised at open | no |
BY (<computed expression>) |
materialised at open | no | materialised at open | no |
The two deleted rows: BY <field> DESCENDING converts to PreselectQuery, and BY <field> BY <field> to AdaptiveQuery. Neither is a presort query, so neither belongs in a PresortQuery table.
What the corrected table says:
- FWD trunk is never change-sensitive for any presort construct.
- OpenEdge is change-sensitive for exactly the two whose order the database can produce by walking an index.
- The other two agree already.
So the design is now simpler.
#15 Updated by Artur Școlnic about 1 month ago
1. The design
Two classes, chosen at conversion:
PresortQuery extends PreselectQuery— unchanged. Materialised and frozen. Already correct forBREAK BY <unindexed>andBY (computed).AdaptivePresortQuery extends AdaptiveQuery— new. Incremental and change sensitive. For aBREAK BYwhose order an index can produce.- Conversion picks between them with the discriminator it already computes:
clientSortinindex_selection.rules, plus the equality brackets that make a partial index match sound. AdaptivePresortQueryadds only break-group support on top ofAdaptiveQuery. Scrolling, invalidation, cursor and results handling all come from the parent, untouched.
2. Why the choice belongs at conversion time
- A partial index match is sound only when the skipped leading sort components are constant —
BREAK BY si2underwhere si1 = ...works only becausesi1is pinned by equality. If it varied, the delivered order would be wrong. - That bracket information exists at conversion and is not exposed at runtime.
QueryComponenthas no accessor for it. - The runtime already has a fork —
createHighLevelResultschoosesIncrementalResultsorSortedResults— and it is mis-gated:sortCriteriaMatchSortPhrasecompares both lists from index 0, so a criterion matching at offset 1 fails and the eager path is always taken. The runtime cannot currently make this call correctly with the information it has.
#16 Updated by Artur Școlnic about 1 month ago
Existing coverage for PresortQuery, by subject
tests/persistence/flushing_validation/query_shape_uncommitted_visibility — 34 classes, 125 methods. Trunk figures are from a run with the dirty-share nodes absent (defaults).
| group | what it targets | methods | red on trunk |
|---|---|---|---|
| Pending-change visibility | Whether a presort read sees a change the same session has made but not committed — through another buffer, in or out of the predicate, on indexed, unindexed and unique fields, with and without a strong scope. The original subject of the task. | 46 | 21 |
| Post-open delivery | Whether a row created or changed after OPEN QUERY is delivered by the continuing walk, and what NUM-RESULTS reports while that happens. |
19 | 8 |
| Result-list editing | CREATE-RESULT-LIST-ENTRY and DELETE-RESULT-LIST-ENTRY: where the entry lands, whether the edit survives a reposition, a commit, and a backward or re-forward move. |
19 | 1 |
| Position and navigation | CURRENT-RESULT-ROW against NUM-RESULTS after GET LAST, GET PREV and a re-forward move; REPOSITION-TO-ROW and REPOSITION-TO-ROWID, ordinal and indexed, single and multi-table. |
21 | 0 |
| Multi-table and lifetime | Multi-table presort joins, break groups across an invalidation, a wide result set walked across a nested scope, a commit and an undone transaction. | 12 | 0 |
| Block semantics | FOR FIRST / FOR LAST over a where clause matching nothing must skip the block silently rather than raise. |
4 | 0 |
| Cost | Emitted measurements rather than thresholds: GET LAST cost, and how many times the sort expression is evaluated across a commit. |
4 | 0 |
| total | 125 | 30 |
#17 Updated by Alexandru Lungu about 1 month ago
Or maybe something that fits in the Composite design pattern, or a Query which uses a delegate (switch between AdaptiveQuery and PreselectQuery, setDelegate).
Do you mean emitting PresortQuery like we do a QueryWrapper to use a delegate? I think it makes sense - it is indeed composition.
However, I am not sure how tightly coupled this PresortQuery shall be. Will it simply proxy the next/prev/first/last/etc. API and build the groups on its own? I think this can work, but I think we are just scratching the surface now. I am not fully confident we see all the functionality or PresortQuery: LAST-OF, FIRST-OF and maybe other aggregates (e.g. ACCUMULATE and ACCUM?).
Two classes, chosen at conversion:
I am inclined to approve this approach. The composition would make sense if the presort implementation would have to share some behavior, but IMHO PresortQuery and AdaptivePresortQuery wouldn't duplicate much of the logic. Also, there might be different logics for these after all, even if we can't find one now, so I would prefer a more pessimistic approach now in order to provide clarity to conversion and have flexibility later on to change only PresortQuery or AdaptivePresortQuery.
#18 Updated by Alexandru Lungu about 1 month ago
- reviewer Constantin Asofiei, Ovidiu Maxiniuc added
- reviewer deleted (
Alexandru Lungu)
#20 Updated by Artur Școlnic about 1 month ago
I committed the initial implementation of the plan above to 10806a, at this point I have only 9 failing tests, no regressions compared to trunk. Working on fixing them.
#21 Updated by Artur Școlnic about 1 month ago
- % Done changed from 0 to 50
The remaining failing tests are:
| test | 4GL shape | query class | fails because | scope |
|---|---|---|---|---|
testPostOpenDeleteOfUnvisitedRow |
FOR EACH … BREAK BY si2, 2 rows; the unvisited last row is deleted and committed mid-walk |
AdaptivePresortQuery |
OpenEdge skips past the dead entry, finds nothing behind it, and leaves the buffer holding the previous record (BBB,BBB, AVAILABLE yes); FWD releases the buffer and ends the walk (BBB) |
in — but the corner, not the rule; the skip rule itself is implemented and verified on four rows |
testCreatedEntryLandsAtRepositionedRow |
BY (<computed expr>), 5 rows; GET LAST → REPOSITION-TO-ROW(2) → CREATE-RESULT-LIST-ENTRY |
PresortQuery (frozen) |
the create succeeds and NUM-RESULTS before it is right (5), but after it reads 0 instead of 6; the sibling arm without the reposition passes |
out — frozen path, restored to trunk form by this branch; result-list editing, not post-open visibility |
testRepeatPreselect |
REPEAT PRESELECT EACH … WHERE si2 = "NEW" EXCLUSIVE-LOCK, after an uncommitted update through a second buffer |
PreselectQuery |
the preselect does not see the session's own uncommitted change — lFound = no |
out — no sort, no presorted layer; would fail identically with the branch removed |
They also fail with trunk.
The branch is at rev 16767.
#22 Updated by Ovidiu Maxiniuc about 1 month ago
- % Done changed from 50 to 0
Review of 10806a / r16767
Presortable.java- line 11: missing H entry and (C) year update. When here, please upgrade the H header format to 110 chars/line (
-T-column should also be dropped);
- line 11: missing H entry and (C) year update. When here, please upgrade the H header format to 110 chars/line (
Presorter.java- lines 234-283: something does not seem fine here. First: at 238/240 it seems more natural to say:
if (!criteriaMatchAt(criteria, offset)) return false;andreturn true;, at the end, if criteria from alloffsetpassed. I could not fully understand the logic ofcriteriaMatchAt(): it iteratessortHelpers, and compare then withSortCriterionstarting atoffset? - line 1495: stray javadoc.
- line 1525: method
backingReplaced()is missing its javadoc. Is it the one from line 1495?
- lines 234-283: something does not seem fine here. First: at 238/240 it seems more natural to say:
AdaptivePresortQuery- line 6:
Copyright (c)should be2026only,-JPRM--column should be dropped.
- line 6:
Note:
This is a complex update, involving both conversion and runtime. It is difficult for me to understand the reasons for some changes without seeing the code in action. So this is the result of first passes through the branch. I need to look again at Presorter and especially to the new AdaptivePresortQuery for more accurate review. Maybe use the AI for suggestions. I'm keeping the task to review state, but feel free to address the few issues I have already found.
#23 Updated by Ovidiu Maxiniuc about 1 month ago
- % Done changed from 0 to 50
This was not intended. Sorry.
#24 Updated by Artur Școlnic about 1 month ago
Presorter.java234-283 I didn't make this one, it flips the meaning.if (!criteriaMatchAt(...)) return falseasks whether every offset matches; we need whether some does.BREAK BY si2underWHERE si1 = ...on an index over(si1, si2)gives a 3-component phrase (si1,si2, PK tiebreaker) and one criterionsi2, matching at offset 1 only inverted, offset 0 fails and we're back on the eager path.
But you were right that it doesn't read well. The bound is now a named lastOffset, there's a comment saying the criteria are a run to find rather than something every offset must satisfy, and criteriaMatchAt says what it walks: the sortHelpers keys in insertion order against criteria.get(offset) onward.
Working on fixing the last 'in scope' test.
#25 Updated by Artur Școlnic about 1 month ago
- Status changed from WIP to Review
- reviewer Alexandru Lungu added
testPostOpenDeleteOfUnvisitedRow — fixed
Pattern. FOR EACH … BREAK BY si2 over two rows (BBB, CCC), converted toAdaptivePresortQuery. BBB is consumed, then CCC — unvisited, and the last row — is
deleted and committed. OpenEdge does not advance past a run of deleted rows at the end
of the list: it hands back BBB once more (rows=2 seq=BBB,BBB, NUM-RESULTS 2) and
only the next move ends the walk. FWD advanced past the end and stopped (rows=1).
Measured across three shapes over the same fixture: a plain AdaptiveQuery and a frozenPresortQuery both end the walk on OpenEdge, and FWD matched both. Only the
index-satisfiable presort diverged.
Fix. Not buffer lifetime — the record is re-fetched every move. It is the position.
In AdaptiveQuery.next's missing-record retry loop, when the skip exhausts the list,
restore the position to the row before it and deliver that row again:
if (!avail && skipped && !deadEndDelivered && redeliverRowBeforeSkippedOffEnd() &&
rowBefore != null && rowBefore >= 0 && results.setRowNumber(rowBefore))
{
avail = true;
redelivered = true;
deadEndDelivered = true;
}
deadEndDelivered is a one-shot, so the following move ends the walk; the cursor entryis suppressed on a re-delivery, so
NUM-RESULTS stays at 2; andredeliverRowBeforeSkippedOffEnd() defaults to false, overridden only inAdaptivePresortQuery, leaving the frozen path untouched.
This was the last failing test, the final score is trunk 30/125 failures, 10806 2/125. The 2 remaining failures are also in trunk and are not in scope.
Alex or Ovidiu, please review 10806a/16769.
#26 Updated by Ovidiu Maxiniuc about 1 month ago
I looked at 10806a/r16769.
This is basically a patch for a quirk. I remember something vaguely similar when reading a stream, if the last line does not finish with the line terminator (\n), the previous line is read twice before returning EOF, and the actual last line is never read.
- it needs to be well tested to be sure we do not affect the normal execution of the query;
- it must be well documented in the code. Luckily, the patch is localised and can be easily annotated with supplementary Java comments/javadoc.
AdaptiveQuery.java:- while the other new local flags used for identification of the exceptional situation are reinitialized each time the
next()method is invoked,deadEndDeliveredis not reset onopen(),first(), etc which should normally invalidate this flag; - what about the mirrored case, when the result is navigated in reverse order (using
previous())? How does the quirk manifest?
- while the other new local flags used for identification of the exceptional situation are reinitialized each time the
#27 Updated by Artur Școlnic about 1 month ago
You were right about deadEndDelivered: it was only cleared by an ordinary forward delivery, so anything else that moved the query left it stale and a stale guard suppresses the re-delivery next time the same dead end is reached, which is the bug it exists to prevent, inverted. Added forgetDeadEnd() and call it from first, last, previous, both reposition variants and cleanup. The field javadoc now says what the guard means and why anything else invalidates it.
New tests proc_probe_dead_end_guard.p + TestDeadEndGuardResetsAndLeavesNormalWalksAlone,
every value measured on OpenEdge:
| pattern | OpenEdge | branch |
|---|---|---|
| clean 3-row walk, no deletes | AAA,BBB,CCC |
same |
| mid row deleted, live row behind it | CCC |
same |
| dead end, pass 1 | BBB |
same |
| dead end, pass 2 on a second open of the same query | BBB |
same |
The pass1/pass2 pair is the regression test for the guard same dead end twice, and a stale guard drops the second. The first two are the "normal execution untouched" check.
The reverse case. There isn't one. OpenEdge rejects backward navigation on a BREAK BY query outright: Cannot LAST or PREV Query when FORWARD-ONLY or -noautoreslist or BREAK BY enabled. (12393), and AdaptivePresortQuery is only emitted for BREAK BY. But FWD
allows the move, which is what makes previous() reachable for a presorted query here when it is unreachable in 4GL so the forgetDeadEnd() call in previous() is doing real work.
I split that into its own test, TestBackwardNavigationOnBreakByIsRejected, which fails. It is pre-existing and separate from this task, nothing in the presort work adds or removes that validation.
Committed the changes to rev 16770.
#28 Updated by Ovidiu Maxiniuc about 1 month ago
- Status changed from Review to Internal Test
- reviewer deleted (
Alexandru Lungu)
Indeed, the latests revision should fix the issue from review. I think you can start testing. Be sure it passes the full set of tests; due to nature of the changes, all projects are affected.
The %done is still at 50%. Is this accurate? If not, please update.
#29 Updated by Artur Școlnic about 1 month ago
- % Done changed from 50 to 100
Thank you, Ovidiu.
Will start with multi session tests, since the new query must be sensitive to other users changes too, then move on to standard regression testing.
#30 Updated by Artur Școlnic about 1 month ago
- % Done changed from 100 to 90
- Status changed from Internal Test to WIP
Multi session tests identified a few issues.
1. Regression: presort walk truncates at the first fetch bracket
BREAK BY over an index-satisfiable sort:
- delete one row ahead of the cursor — 9 rows delivered of 402,
NUM-RESULTS11 - create one row behind the cursor — 10 rows of 403,
NUM-RESULTS11
A commit landing past the end of the set is unaffected. The boundary is ProgressiveResults.BASE@ = 10, the first fetch bracket. NUM-RESULTS = 11 is a correct report of what was resolved, not a separate bug.
Impact: a loop over a presort query silently processes ten rows instead of four hundred, with no error. Not reachable on trunk, where PresortQuery materialises at the open.
2. Pre-existing: dead-end re-delivery does not fire cross-session
When another session deletes every row ahead of the cursor, the row before them is not re-delivered. Fails on trunk and on the branch; AdaptiveQuery.redeliverRowBeforeSkippedOffEnd covers the single-session case only.
Working on fixing the issues and extending the multi session test suite.
#31 Updated by Ovidiu Maxiniuc about 1 month ago
- reviewer Adrian Lungu added
Adding back Alex as reviewer. It was not in my intention to remove him from the list, but somehow, that just happened.
#32 Updated by Artur Școlnic about 1 month ago
- Status changed from WIP to Review
- % Done changed from 90 to 100
- reviewer deleted (
Adrian Lungu)
Issues the multi-session tests found and solved
1. The walk truncated at the first fetch bracket. Any committed change by another session at or behind the resolution point killed the walk at ten rows — 9 rows delivered of 402, no error. A loop over a presort query silently processed ten rows instead of four hundred.
2. The dead-end re-delivery didn't fire cross-session. When another session deleted every row ahead, the walk delivered nothing instead of re-delivering the row before the dead end. Cause: the rebuilt backing handed back rows whose records were gone, so the query reported a result the caller couldn't read.
Both were reachable only from the branch's incremental path, and only cross-session — single-session probes pass. Neither was visible to the existing suite, whose post-open tests all use 2–4 row fixtures and never cross a bracket boundary.
Also found, not fixed: PC06 — with an OUTER JOIN, a foreign delete of the joined row drops the driving row. Fails on trunk too, so it's pre-existing and outside this ticket.
The fix
Three files; every existing presort query keeps today's code path.
- Presortable.consumeBackingReplaced — new default method returning false. Nothing else implements it.
- AdaptivePresortQuery — raises the flag in resetResults, already on the invalidation path. Asking consumes it, bounding the retry to one per replacement.
- Presorter.IncrementalResults.loadRow — on next() == false, retry instead of tearing the backing down if the query says it was replaced; and skip a row none of whose components resolved to a record.
Please review rev 16771.
Meanwhile I will continue testing .
#33 Updated by Artur Școlnic about 1 month ago
- reviewer Alexandru Lungu added
#34 Updated by Ovidiu Maxiniuc about 1 month ago
Indeed, the fix in r16771 should work as explained in #10806-32.
#35 Updated by Artur Școlnic about 1 month ago
Multi tenant app testing is ok, tested conversion, harness, web ui navigation, reports and load testing.
Continuing with etf and chui.
#36 Updated by Artur Școlnic about 1 month ago
Etf housing and chui regression testing passed. Rebased 10806a on trunk/16782.
Will ask colleagues to test the other projects.
#37 Updated by Artur Școlnic 29 days ago
Stefanel and Razvan will help me with further testing, thanks guys!
Please post here the results.
#38 Updated by Alexandru Lungu 29 days ago
- I generated BREAK phrase
- I also loaded BREAK phrase (private)
Artur, please take a look and let me know if some of the documented behavior is out of place. I will review and do some tests on 10806a next.
#39 Updated by Artur Școlnic 29 days ago
Three items from the #10806 measurement work. Evidence: OpenEdge 12.8 single-user, plus the FWD trunk run of the query_shape_uncommitted_visibility corpus.
1. The OPEN QUERY grammar permits a combination the compiler rejects
openQueryStmt lists INDEXED-REPOSITION and BREAK as sibling options in any order, but the reference refuses that pairing at compile time. Re-measured on a temp-table, with controls both ways:
brk_ir.p (BREAK BY + INDEXED-REPOSITION) -> error=yes
[14297] BREAK BY only allowed if query q is not INDEXED-REPOSITION. (14297)
brk_only.p (BREAK BY alone) -> error=no
ir_only.p (plain BY + INDEXED-REPOSITION) -> error=no
Pure language restriction, no database involved. Since backward navigation is also rejected (12393), a BREAK BY query's reposition surface is effectively nil.
2. Pending-change bullet — split out the third case
The third case, "an update to a sort or break field that changes the record's position or group", measured as working on FWD trunk: TestPresortOrdersByPendingSortValue asserts "CCC,ZZZ" where stored values give "ZZZ,CCC", and TestPresortGroupsByPendingBreakValue asserts one group where stored values give two. Both pass — SortedResults resolves keys from temp records set with setTempRecord, which already carry the pending values, so ordering and grouping use the new value while selection does not.
Only the eager SortedResults path was exercised; the IncrementalResults post-commit quickLoad path the private page blames was never reached. Suggested: keep cases one and two, scope case three to the path where it was observed.
3. The unindexed/computed exemption is stated as Sure but recorded as unreconciled
"BREAK BY an unindexed field, or BY a computed expression, behaves the same as the reference." The Performance entry on the same page says the set is taken in full at open "even when an index could produce the order", and the private page calls the IncrementalResults streaming question "the main reconcile point recorded in the audit".
Suggested: move to Unconfirmed. Every corpus probe uses an indexed break key behind a WHERE equality — the mis-gated eager path — so the exemption is untested.
I am committing query_shape_uncommitted_visibility now.
#40 Updated by Stefanel Pezamosca 28 days ago
Artur, I found an issue, please take a look.
A PRESELECT loop with BREAK BY on an indexed field must convert to a PresortQuery, as on trunk, not to an AdaptivePresortQuery. PresortQuery extends PreselectQuery and walks a list built once at open, so a record created in the loop body does not join the walk. AdaptivePresortQuery is sensitive to changes, and the new record joins the walk. The cause is the new index_satisfiable_sort note (index_selection.rules:1167), which ignores the PRESELECT keyword; database_access.rules then emits AdaptivePresortQuery (database_access.rules:1480, 2035).
Testcase:
DEFINE TEMP-TABLE tt NO-UNDO FIELD f AS INTEGER INDEX ixf f.
DEFINE BUFFER b FOR tt.
DEFINE VARIABLE n AS INTEGER NO-UNDO.
DO n = 1 TO 3:
CREATE tt.
tt.f = n.
END.
n = 0.
REPEAT PRESELECT EACH tt BREAK BY tt.f:
FIND NEXT tt NO-ERROR.
IF NOT AVAILABLE tt THEN LEAVE.
n = n + 1.
IF n = 1 THEN DO TRANSACTION:
CREATE b.
b.f = 2.
END.
END.
MESSAGE "visited=" n.
Note: The expected result is visited=3, with 10806a is visited=4
#41 Updated by Artur Școlnic 28 days ago
- Status changed from Review to WIP
- % Done changed from 100 to 90
Thank you, Stefanel. I am on it.
#42 Updated by Artur Școlnic 28 days ago
The new index_satisfiable_sort note (index_selection.rules:1167) is computed from indexSatisfiable and !sortDegraded and table_count == 1, with no reference to the PRESELECT keyword, so database_access.rules (:1480, :2035) emits AdaptivePresortQuery for a REPEAT PRESELECT ... BREAK BY <indexed field>. That class is change sensitive by design, so a record created in the loop body joins the walk. PRESELECT resolves the set at open however the order is produced.
Fix 1 — index_selection.rules: added !descendant(prog.kw_presel, 1) to the note. This restores trunk's query class for every PRESELECT shape and leaves the true arm, and therefore the FOR EACH behaviour the task was written for, untouched. Measured on OpenEdge 12.8, the rule's own comment is correct for FOR EACH: an index-driven sort stays change sensitive, a client-side sort is materialised and frozen. It is simply not a statement about PRESELECT.
Fix 2 — Presorter.java: a BREAK phrase cannot close a group without reading the row after the current one, so the read position of a break walk is one row ahead of the row whose body is running, and a row created into that band is never delivered (seeded BBB,CCC,DDD and paused on BBB: BBX is not delivered, CCX/DDX/ZZZ are, and dropping the BREAK delivers BBX too). IncrementalResults now passes over a supplied row that sorts strictly before the furthest row it had already read when the backing was replaced. Note that AdaptiveQuery.changedRecordSortsAfter cannot be used for this — it is reached through doubleCheckInvalidation, which returns early for a scrolling query, and a BREAK BY query is scrolling by language rule.
Result
| pass | fail | |
|---|---|---|
| OpenEdge 12.8 | 33 | 0 |
| FWD trunk (16782) | 25 | 8 |
| FWD 10806a as committed | 22 | 11 |
| FWD 10806a + both fixes | 32 | 1 |
Across the whole slice, 185 tests go from 152/33 to 162/23. The single remaining failure, testPreselectAgainstDeleteAhead, fails on trunk as well: a row deleted ahead of the cursor is still delivered from a frozen result list. It is a pre-existing PresortQuery defect on a different axis from this task — the branch's own AdaptivePresortQuery handles the same case correctly — so I propose a separate issue rather than widening this one. The test is new, so this is a newly discovered trunk defect rather than a known one.
Committed the fix to rev 16791.
#43 Updated by Artur Școlnic 28 days ago
- % Done changed from 90 to 100
- Status changed from WIP to Review
#44 Updated by Ovidiu Maxiniuc 27 days ago
- Status changed from Review to Internal Test
Artur,
I think the code changes are fine. But I guess the full set of tests needs to be redone.
A couple of minor things to mention:- in
Presorter.java, the methodscriteriaMatchAt()andsortsBefore()use two parallel iterators: enhancedforonsortHelpersandi. I think we discussed this and I understand that semantically these should be synchronized (for example, insortsBefore(),values[i]will not raise offset exception becausevalueswas constructed usingsortHelpers.size()), but this is not evident locally. I think a short note should be added to javadoc. - there is one thing I missed in previous review, when I was focused on code changes rather than on comments: in the header of
Presortable.java, the-T-column should be dropped, nut not the--JPRM--. Although that is deprecated now, it is still valuable information. As opposed toT, whose values were evident. Keep the max width to 110 chars.
#45 Updated by Alexandru Lungu 26 days ago
- Status changed from Internal Test to WIP
- % Done changed from 100 to 90
Review from ai:
- [CRITICAL] functional
AdaptiveQuery.retainResultsOnReset: The testhighLevelResults instanceof SimpleResultsalso matchesFilteredResults(class FilteredResults extends SimpleResults), whichPreselectQuery.createResultsreturns for every plainAdaptiveQuerywith a client-side where. PreviouslyresetResultscalledFilteredResults.cleanup()(rows.clear()+reset()); now the cleanup is skipped,resultsNeedBackingre-executes into the adapter,createResultshands back the sameFilteredResultsandsetResultsskips cleanup because the object is identical.FilteredResults.first()/next()serve the stale cached rows before falling through to the new delegate. Trigger:CompoundQuerycallsquery.reset(!resolveOnce, false)on each inner component every loop pass (CompoundQuery.java:2381, 4242), so inFOR EACH a, EACH b WHERE b.k = a.k AND <client-side expr>the inner query for outer row N returns the rows cached for outer row N-1, then its own. Key the exemption on the presort layer (a flag/override inAdaptivePresortQuery), not onSimpleResults.
I warned about something similar in my previous requirements:
plenty of code has instanceof AdaptiveQuery or similar.
I should have been more specific, but AQ and PQ have plenty of "hard-coded" checks to identify certain query states. Checking if the results are dynamic using results instanceof DynamicResults, instead of checking whether the query is in preselect/dynamic mode. As the Presort feature is being ported to this infrastructure, most of the instanceof require second pass to see if they still fit the logic.
- [MAJOR] functional
AdaptiveQuery.sessionEvent: TheSESSION_CLOSING_NORMALLYguard became!resultsCached && !(highLevelResults instanceof SimpleResults), which (same root cause) matchesFilteredResults. For a scrolling client-whereAdaptiveQuery(OPEN QUERY ... SCROLLING/ browse walked withGET NEXTacross a committed transaction),TRANSACTION_COMMITTINGalready breaks onisScrolling(), and now session close no longer stores reference rows nor callsresetResults(). The adapter stays connected with its pre-close delegate (resultsNeedBacking()is false), so the nextGET NEXTreads aScrollingResultsJDBC cursor over a session thatPersistencecloses or marks reclaimable, and the cursor is no longer released at session close. Apply the same presort-specific test as forretainResultsOnReset.
Same mention as above. Mentioning SESSION_CLOSING_NORMALLY may raise us a complete new perspective; are the presort query results invalidated on commit as they might be forward-only? If so, do we need caching BatchResults on top of the extracted FilteredResults?
- [MAJOR] functional
AdaptivePresortQuery.finishInitialize:setScrolling()is always called and the conversion change withholdssetNonScrollingforindexSatisfiableFOR EACH presorts. On UNDO, RETRY,TransactionManagercallsPreselectQuery.retry()(setsbetweenRows = true); the re-run body then callsnext(), butAdaptiveQuery.next()honorsbetweenRowsonly when!isScrolling()(~line 2441) and neitherAdaptiveQuerynorAdaptivePresortQueryoverridesretry, soresults.next()advances. Trigger:FOR EACH customer BREAK BY customer.name: UPDATE customer.credit-limit. IF ... THEN UNDO, RETRY. END.(index on name) skips the retried row and proceeds with the next one. Regression vs the previousPresortQuerypath, which was non-scrolling via the graftedsetNonScrollingand whosePreselectQuery.next()(line 3621) honorsbetweenRowsregardless.
To be investigated.
- [MAJOR] functional
Presorter.IncrementalResults.sessionEvent: On the firstTRANSACTION_COMMITTINGduring the walk, all remaining rows of the backing (theAdaptiveQueryadapter) are drained into aSimpleResults,resultsCached = trueis set and the listener deregisters; afterwardsloadRowreads only the frozen snapshot, so laterresetResults/consumeBackingReplacedhandling never reaches it. The patch edits this drain block but adds no guard for the adaptive backing. Trigger:FOR EACH t EXCLUSIVE-LOCK BREAK BY t.name:(indexedname) whose body updatest(one transaction per iteration) and later creates a rowname = "ZZZ"past the read-ahead band: OE delivers it (per the patch's own 12.8 note inbehindReadAhead: "CCX, DDX and ZZZ all are"), FWD never does. Change sensitivity ofAdaptivePresortQueryeffectively ends at the first commit, and the rest of the table is read eagerly at that point.
Same comment as above regarding results caching. Please create some performance tests in this idea to ensure that the caching is done in a performant way for presort queries (if this is the case).
- [MAJOR] functional
AdaptivePresortQuery.createHighLevelResults: The client-side where filter is applied after sorting (super.createHighLevelResults(presorted)→PreselectQuery.createResultswraps the presorted layer inFilteredResultswhenisClientWhere()), the reverse ofPresortQuery.createResults(filter first, thencreateIncrementalResults/createSortedResults).breakRowsare recorded against unfiltered row indexes whilePresorter.isFirstOfGroup/isLastOfBreakGroup/isLastlook upgetSortedResults().getRowNumber()from the filtered layer. Example: sorted rows A(si2=1, filtered out), B(si2=1), C(si2=2) givebreakRows= {0, 2}; after filtering B=0, C=1, so LAST-OF for B and FIRST-OF for C both return false. Trigger:FOR EACH t WHERE t.si1 = 1 AND <expr not executable server-side, e.g. a UDF call> BREAK BY t.si2with index (si1, si2);index_selectionruns beforewhere_clause_prep2(annotations.xml:676/681) soindex_satisfiable_sortignores client-side where. Apply the filter below the presort layer (mirroringPresortQuery) or exclude client-where queries fromindex_satisfiable_sort.
to be investigated.
- [MAJOR] performance
AdaptivePresortQuery.finishInitialize: Single-tableFOR EACH ... BREAK BYwith an index-satisfiable sort previously converted toPresortQuery+setNonScrolling(), i.e. a non-scrollingIncrementalResultsholding 3 rows withbreakRowstrimmed to the last 2 and no cursor. It now converts toAdaptivePresortQuerywithout the graft, sosetScrolling()stands:IncrementalResults.addRowappends every row'sLong[]ids,breakRowssets are never trimmed, and theCursorWrapperinstalled byAdaptiveQuery.setScrolling()records ids for every row. A forward-only report loop goes from O(1) to several O(n) structures. The rules comment acknowledges giving up the ForwardResults optimisation, but nothing bounds this for large reports; consider a forward-only mode for this shape.
to be investigated.
- [MINOR] functional
Presorter.IncrementalResults.behindReadAhead: The read-ahead band holds only the BREAK/BY criteria values, andsortsBeforetreats a full tie as "not behind". Trigger: index (name, seq),FOR EACH t BREAK BY t.name, cursor on ("BBB", x), read-ahead ("CCC", 5); the body creates ("CCC", 1), the query is invalidated, the replacement resumes after the cursor in (name, seq) order and yields ("CCC", 1), which is not inresuppliedand is appended after the cached ("CCC", 5) — delivered out of index order, whereas the patch's own model ("passes over anything behind the furthest row it has read") implies OE skips it. Break flags stay correct. The OE 12.8 observation quoted only covers the strictly-between BBX case; compare against the read-ahead row's full index key, or add an OE test for the tie case.
to be investigated.
- [MINOR] functional
AdaptivePresortQuery.resetResults:backingReplacedis raised unconditionally, including fromPreselectQuery.open(which callsresetResults()beforeexecute()) andreset, while neithercleanupnorcreatePresortedResultsclears it (its only consumer is Presorter.java:1715). Every open therefore builds a newIncrementalResultswith the flag already set, and the first genuine end of the backing (including an empty result) is taken for the "between delegates" case inIncrementalResults.loadRow, issuing one redundantnext()on the exhaustedResultsAdapter/ProgressiveResults(extramoveTo/bracket lookup) per open. Results stay correct. Clear the flag incleanupor aftercreatePresortedResults.
to be investigated.
There are also plenty of style issues you can regenerate I suppose using the AI FWD review skill.
#46 Updated by Artur Școlnic 26 days ago
- % Done changed from 90 to 100
I will try to find testcases for the technical issues.
#47 Updated by Artur Școlnic 26 days ago
- % Done changed from 100 to 90
#48 Updated by Artur Școlnic 26 days ago
| # | Finding | Status | Action |
|---|---|---|---|
| 7 | read-ahead tie loses rows | confirmed by test -- regression introduced in 16791 | fix first |
| 4 | change sensitivity dies at the first commit | confirmed by test | fix |
| 5 | break flags computed over unfiltered row numbers | confirmed by test | fix |
| 3 | UNDO, RETRY skips the retried row | confirmed in source (not runnable in batch) | fix |
| 1+2 | instanceof SimpleResults also catches FilteredResults |
confirmed in source, latent -- no trigger found | one fix, both sites |
| 6 | always-scrolling presort keeps O(n) structures | not measurable from 4GL | measure, then decide |
| 8 | backingReplaced raised on every open |
harmless as stated by review | one line, optional |
Most of the findings can be reproduced, working on extending the test suite and fixes.
#49 Updated by Artur Școlnic 25 days ago
I introduced ~ 50 more tests in the suite, there are 3 regressing tests, compared to trunk, the failure reasons are:
Over-delivery — break by si1 (tests 1 and 2). The branch decides "behind the read position" by comparing sort keys. readAheadBand holds the resolved values of the break criteria only — just si1 = CCC. The created row is also CCC, and sortsBefore treats a full tie as "not behind", so CCC/1 is let in. The comparison needs the total order (si1, si2, rowid); the band is two levels too coarse. Result: BBB/1,CCC/5,CCC/1,DDD/1, with the row out of index order.
Truncation — break by si1 by si2 (test 3). There the band happens to cover the whole key, so the row is correctly refused — and the walk then dies, losing DDD/1. Refusing a row inside Presorter.loadRow breaks the reconstruction: the row never enters the presort cache, so cursor.peekLastLoadedResult — the anchor the re-executed backing is repositioned on — is stale, the re-execution comes back empty, and the loop reads that as end-of-results (addRow(null) + cleanupBackingResults()).
One cause underneath both. The branch has no representation of the walk's read position. It approximates it with four derived pieces of state — resupplied, backingResultsOffset and readAheadBand in Presorter.IncrementalResults, plus peekLastLoadedResult in AdaptiveQuery — which agree only while every row the replacement stream produces is either handed back or appended. OE semantics requires refusing some rows, and that breaks the invariant.
#50 Updated by Artur Școlnic 25 days ago
I need to mention that for establishing an OE baseline, I use a self hosted database, client/server uses the message buffer which heavily skews the results. Since we are dealing with prefetched records and invalidation, another layer of caching/buffering is highly confusing and is a matter for future improvements.
#51 Updated by Artur Școlnic 22 days ago
- Status changed from WIP to Review
Changes in the branch:
16793 -- Keep a presorted result list alive across the resets beneath it
A presorted layer is built once per open and the caller walks it, so the parent must not empty or rebuild it when the backing underneath is replaced. Four places assumed otherwise.
AdaptiveQueryasked whether its results were materialised by testing forSimpleResults, which also matchesFilteredResults-- a filter over the delegate, not a materialised view, and one which must still be cleaned up. The question is now put to the subclass throughretainResultsOnReset.AdaptivePresortQueryanswers it, and clears the backing replacement flag when the open state is discarded.PreselectQueryno longer cleans up the previous results when they are materialised: a re-execution can reach that path from underneath a walk, and emptying the list leaves the caller walking nothing.Presorterdrains the delegate the backing already holds when a transaction commits, rather than asking the adapter for rows, which re-enteredprovideNewResultsand handed back a rewound delegate.
Two behaviours measured against OpenEdge 12.8 rather than inferred: a client-side where belongs below the presorted layer, since break rows are recorded against the layer's own row numbers while the break flags look the current row up in whatever the caller is walking; and re-delivery of the row before a dead end depends on how the walk is driven, not on the query -- a named query walked with GET NEXT hands that row back while an inline FOR EACH block ends the walk.
16794 -- Give a presorted walk a read position, and measure it in the whole sort phrase
A BREAK walk reads one row past its cursor to answer LAST-OF, and which side of that read position a changed row falls on decides whether the legacy system delivers it. Nothing represented that position: a replacement backing resumed at the cursor, so the rows between the two were offered again for the layer to recognise, which it could not do for a row whose key tied with one it was holding.
AdaptiveQuerygainedresumeAnchorIdsandresumeAnchorByScan, both defaulting to the previous behaviour, plusrowMatches; the replacement may now be advanced to the anchor rather than only tested against its first row.AdaptivePresortQueryanswers with the furthest row its layer has read, whichPresorterexposes asfurthestRowRead.Presorter.useSortPhraseForBandwidens the read-position band from theBREAKcriteria to the query's whole sort phrase, so rows tying onsi1are still ordered bysi2. The band's resolvables take the DMO proxy the criteria already held; aRecordBufferis also aDataModelObjectand would compile, but resolves its getters against no record. TherecidcomponentSortCriterion.parseappends is skipped.
Testing:
Measured against OpenEdge 12.8 self-service, 226 comparable tests: trunk 170, this branch 218, no regressions. The branch fixes 48 of trunk's 56 failures; the 8 remaining fail identically on trunk.
#52 Updated by Ovidiu Maxiniuc 20 days ago
Review of 10806a / r16794
I analysed the branch focusing on the last two revisions documented in #10806-51. For some reason, the changes have multiple formatting issues. I could blame a rebase error?
AdaptivePresortQuery.java:- line 125, 945: extra empty lines;
- lines 982 and 989. Usage of
integer.UNKNOWNis recommended. Doing so does not all any routine, does not create any new objects. - line 992: usage of
max(0, sorted.getNumberOfLoadedRows() - resultListDeletions). Is it possible to delete more records than loaded and get a negative number?
AdaptiveQuery.java:- lines 4569-4582:
privatemethod inprotectedarea ()should be moved down the file, toprivatesection; - lines 5960-5994: the javadocs are grouped together. Or
rowMatches()method with its javadoc was inserted betweenprovideNewResults()method and its javadoc; - line 6055, 6063: extra empty lines;
- lines 4569-4582:
Presorter.java:- line 137: the
RECORD_IDis hardcoded to"recid", but the correct approach would be to use the configurableSession.PKinstead; - line 141, 827, 1925: extra empty line;
- line 1765: missing javadoc for
furthestRowRead(). However, it looks like it can be found at line 1735. There is also another stray javadoc at line 1729 which looks like the original frombackingReplaced(), but this has a new javadoc;
- line 137: the
Presortable.java:- line 114: extra empty line.
That's a first pass. I will try a more in-depth analysis tomorrow, if needed.
#53 Updated by Artur Școlnic 19 days ago
Ovidiu Maxiniuc wrote:
Review of 10806a / r16794
I analysed the branch focusing on the last two revisions documented in #10806-51. For some reason, the changes have multiple formatting issues. I could blame a rebase error?
The formatting was broken after removing logging statements from the code with a regex, sorry for that. I addressed them in rev 16795.
Regarding max(0, sorted.getNumberOfLoadedRows() - resultListDeletions), it is more of a defensive guard than a crucial piece of logic.
#54 Updated by Alexandru Lungu 19 days ago
- Priority changed from Normal to Urgent
#55 Updated by Alexandru Lungu 19 days ago
Review on latest version:
- [MAJOR] functional
Presorter.IncrementalResults.loadRow: Break rows are recorded atbackingResults.getRowNumber()(the delegate's own row number), butisNewBreakGroup/isLastOfBreakGrouplook them up by the presorted layer's row number. After the backing is replaced (invalidate,resetResults, re-execute) the delegate row numbers restart, so FIRST-OF/LAST-OF and break accumulators are wrong for all later rows. Trigger:FOR EACH t NO-LOCK BREAK BY t.fwhere another session commits a change totwhile the cursor is past row 0. Record atbackingResultsOffset + getRowNumber()or at the layer's own index. - [MAJOR] functional
Presorter.IncrementalResults.loadRow:actualRow = backingResultsOffset + backingResults.getRowNumber()assumes every delegate row becomes a layer row, but rows dropped bybehindReadAhead/noRecordLoadedadvance the delegate withoutaddRow, andconsumeBackingReplacedonly compensates foralreadyResupplied. The read position drifts, the one-row lookahead is lost andisLast/isLastOfGroupreturn true early. Trigger: walk on BBB with CCC read ahead, another session creates BBX; BBX is dropped as behind the band, then DDD1 (with DDD2 following) reports LAST-OF. - [MAJOR] functional
Presorter.IncrementalResults.sessionEvent: The first TRANSACTION_COMMITTING during anAdaptivePresortQuerywalk drains the whole remaining delegate into aSimpleResults, setsresultsCachedand deregisters, freezing the result set so rows created by other sessions/the body are never delivered (OpenEdge delivers them), and loading all remaining rows into memory (N+1 if the delegate is aDynamicResults). Whether this happens depends on whether the query was invalidated in the same transaction. When the delegate is a connectedDynamicResults,spent.next()also walks the query's buffers withoutpushTempContexts. Trigger:FOR EACH t EXCLUSIVE-LOCK BREAK BY t.grpwith per-iteration transaction and a body that creates a row sorting later. - [MAJOR] performance
Presorter.IncrementalResults.loadRow: Every loaded row with break groups executesfurthestBand = resolveBand(), allocating aBaseDataType[]and re-resolving every sort criterion, though the value is only read inbackingReplaced()whenreadAheadOfCursor() > 0(onlyAdaptivePresortQuery).PresortQueryalso usescreateIncrementalResults, so every existingPresortQueryBREAK BY walk pays this on every row. Gate onbackingResumesAtCursor()or resolve lazily inbackingReplaced(). - [MAJOR] performance
Presorter.IncrementalResults.behindReadAhead: OncereadAheadBandis set it is never cleared, so every subsequent row allocates a band and resolves the sort phrase (in theresultsCachedpath also push/pop temp contexts and aquickLoadper component per row). Trigger:AdaptivePresortQueryBREAK BY walk where a transaction commits mid-walk. ClearreadAheadBandonce a row not behind it is appended. - [MINOR] performance
AdaptiveQuery.ResultsProvider.provideNewResults: The loopwhile (!found && resumeAnchorByScan() && adapter.next())is an unbounded linear scan of the replacement delegate; if the anchor row was deleted or re-keyed it fetches the entire remainder and thenadapter.reset()discards it. Bound byreadAheadOfCursoror stop once past the anchor's band. - [MINOR] functional
Presorter.sortsBefore: Rows are dropped by comparing withDataSorter(unknown sorts high ascending), which assumes the database returns 4GL index order.MariaDbLenientDialectdoes not honour nulls-last, so with a?key in the walk and a replacement backing, every non-unknown row after the unknown group is silently skipped viabehindReadAhead.
#56 Updated by Artur Școlnic 19 days ago
- Status changed from Review to WIP
- Priority changed from Urgent to Normal
Will confirm with testcases and fix if necessary.
#57 Updated by Artur Școlnic 19 days ago
- Priority changed from Normal to Urgent
#58 Updated by Artur Școlnic 14 days ago
- Status changed from WIP to Review
Fixed all the tests and performance issues. Alex, please review rev 16800.
#59 Updated by Alexandru Lungu 8 days ago
- reviewer Eduard Soltan added
Review of 10806a
Foreign commits¶
- Changes in
Sessionare risky. I understand what they do and they seem to do it correctly, but I fear:- Direct JDBC access won't honor this mechanism.
- The LinkedHashSet + Map implementation is basically replicating a ScopedDictionary as far as I can tell. I prefer to have it consistent and reuse the ScopedDictionary as a data structure. However, each time we consider such structure:
- consider retry scenarios
- consider iterate scenarios
- consider the obvious no-rollback and rollback scenarios (endScope).
- Changes in
SQLQueryare fragile as they useregionMatcheswith "alter sequence" and "analyze". Are these 2 the only SQL commands that are considered out from writing tables? I understand that the solution doesn't have to be complete and just sound, but I prefer the "hook" of noteWrite to be somewhere we can know for sure we are capturing only insert, update and delete.- again, direct JDBC access is to be considered
- there are also changes in AdaptiveQuery: foreign commits,
I think the review above is strictly only for tracking transactions that write and mostly unrelated to presort. I prefer to not include these changes now as they are too fragile currently and don't seem directly related to the presort rewritting part. Please correct me if I am wrong, but there foreign commits logic is an entire sub-system not 100% related to presorting, so we can defer this and design it a bit more, considering that AdaptiveQuery non-presort also needs this.
TL;DR the sensitivity to foreign commits is currently a gap in FWD for any query. This effort should not be scoped to the porting of PRESORT query. I want it to be a separate effort with proper harness, observations, performance considerations etc. It makes the presort porting hard to follow.
Not having cross-session invalidation for presort is not a priority now. We need to prioritize the #11879-6 which is about leveraging the intra-session invalidation.
Clear repo-cache¶
- ScrollingCursor / RepositionCache / ReleasedRowList: I can't understand the goal of releaseRowsBehind. From my understanding, this forces the REPOSITION to no longer find records that were already iterated. Do you have a small example for this that clears the repo-cache? This is a new feature I have not seen before.
Resume class memebers¶
- Can you document the "resumeFetcher" on Redmine? I am not sure if there is a proper requirement to have the paged results mechanism reused for
Presort. I am afraid of the 3 extra fields of AQ: resumeIndexValue, resumeFetcher and resumePaged. They seem oddly specific.- If "resume" is linked with the foreign commits, lets link it to it and defer.
APQ quirks¶
leaveWalkcalls are documented as "moved the walk elsewhere" and "starts a new walk". What is "walk" supposed to mean. The AQ can be used as an open query or within a query block, scorlling or non-scrolling. Considering such different cases, "walk" terminology may be confusing, especially what "leave walk" would mean.
I also see some integration with forgetDeadEnd, redelivered state and the new redeliverRowBeforeSkippedOffEnd API. From my understanding, this is a quirk discovered when integrating AdaptivePresortQuery. I am a bit skeptic of having such machinery in the highly used AdaptiveQuery as long as it is a quirk of AdaptivePresortQuery. I think touching all AQ API (next, prev, first, last, etc.) to route this quirk makes the AQ code hard to follow. Please see if you can move the quirk resolution in AdaptivePresortQuery.
I agree that we usually use such polymorphism to gather a true/false flag to control the behavior, but as long as AdaptivePresortQuery is extending AQ and the hooks of leaveWalk are right at the start of the next/prev/etc. API, I think it would be cleaner to scope it to AdaptivePresortQuery as much as possible. I prefer to have a minimal change set on AQ and most of the behavior integrated through AdaptivePresortQuery class.
Other¶
- I can't understand some of the new API and the goal:
- hydratesScrollingRows: what does this mean?
loadByValuesis it correct to callalignToRow. AFAIK, this API is meant only to load the buffers without changing the query state.- is it possible to have an
AdaptivePresortQueryas aCompoundQuerycomponent? I guess not?
- is it possible to have an
- Please avoid mentioning OpenEdge in the javadoc.
Extra reviewers¶
- Eduard, please review
IndexSelectionWorkerchanges and index_selection.rules - Ovidiu/Constantin, please review the conversion changes in database_access.rules
Suggested testing¶
- enhanced browse FWD ability. If the attached query is a AdaptivePResortQuery and you add new sorting, will this sorting be considered for BREAK? I ask this because I see override of
addDynamicSortCriterion. - INDEXED-REPOSITION phrase. This phase allows REPOSITION statement to force a RandomAccessCursor instead of a ScrollingCursor to the query and have a different way to retrieve data. Let me know if this is possible first and foremost.
#60 Updated by Alexandru Lungu 8 days ago
PS: it is a bit tricky to follow the 5k patch ATM to be fair. That is why I prefer the minimal amount of changes to port PRESORT queries as a subclass of AQ in order to fix #11879 and don't have regressions. Any other side-fixes no necessarily due to RPESORT, but rather quirks and limitations of AQ that were not in scope, should be deferred, either in other task branches or actual tasks. Anything bigger than 1k patches fills my brain context and I need compacting :)
#61 Updated by Artur Școlnic 8 days ago
Q: Are the foreign-commit changes a separate subsystem, not specific to presort?
Yes. Cross-session invalidation in AdaptiveQuery is switched off by a hardcoded forceGlobalInvalidation = false, so outside this branch no AdaptiveQuery sees other sessions' commits. The foreign-commit code is Session.noteWrite, the SQLQuery hook, the Persistence counters, the cap in PagedResults and resumeOnForeignCommit in AdaptiveQuery. I'll remove it and defer it to its own task. One catch: resumeAtReferenceRow() also runs on a normal session close (SESSION_CLOSING_NORMALLY), so that call stays or falls back to invalidate(true).
Q: What is the goal of releaseRowsBehind? Is there an example where it clears the repo-cache?
It only limits memory. AdaptivePresortQuery must scroll, while trunk's non-scrolling PresortQuery kept just the previous, current and next rows. It is enabled only for a standalone, unbrowsed FOR EACH (walksForwardOnly()). Every repositionByID comes through QueryWrapper, so REPOSITION can't reach that query, and no code reads the dropped rows. So there is no example of a visible clear. Named queries keep their rows, which they must: OpenEdge allows REPOSITION backwards on BREAK BY.
Q: Can you document resumeFetcher? Is "resume" linked to foreign commits?
resumeFetcher, resumeIndexValues and resumePaged are the paged form of resumeAtReferenceRow(), which runs on a foreign commit and on a normal session close. Without them, both paths switch to dynamic mode (row by row). They can be deferred with the foreign commits, at that cost on session close.
Q: What do "walk" and leaveWalk mean? Can the quirk move into AdaptivePresortQuery?
"Walk" means a run of consecutive next() calls. leaveWalk() resets the re-delivery guard and the RETRY row on first, last, prev and reposition. Both only apply to AdaptivePresortQuery:
- redeliverRowBeforeSkippedOffEnd() is false everywhere else.
- retryRow needs a standalone scrolling query, and only AdaptivePresortQuery is one.
I'll move them into AdaptivePresortQuery.
Q: What does hydratesScrollingRows mean?
The presorted layer takes the record the result set is already on, instead of loading it again with quickLoad by id. The scrolling backing returns only ids, so that reload would be one SELECT per row. That cost comes from reading the code; I haven't measured it.
Q: Is it correct for loadByValues to call alignToRow?
Yes. All seven callers pass the row the cursor is already on: first, last, next from the cache, previous, the RETRY row, and loadCurrentRow from QueryWrapper and BrowseWidget. FIRST-OF/LAST-OF read from the layer, so it must follow the cursor. When it's already there, the call changes nothing.
Q: Can an AdaptivePresortQuery be a CompoundQuery component?
No. It is emitted only when the statement's table_count is 1 (index_selection.rules), and dynamic queries go through the same rules.
Q: Enhanced browse: is added sorting considered for BREAK?
Yes, though I haven't run it in a browse. BrowseWidget.beforeQueryOpen applies the sort before each open. Every sort criterion is a break level, so the new one becomes level 1 and shifts the others: QUERY:FIRST-OF then means the new field. PresortQuery uses the same Presorter code. The query falls back to fully sorted results unless the new order still matches the index.
Q: Is INDEXED-REPOSITION possible?
- Static OPEN QUERY: no. OpenEdge rejects it at compile time with 14297.
- Dynamic QUERY-PREPARE: yes. OpenEdge accepts it and applies the option: after REPOSITION, CURRENT-RESULT-ROW is ? and NUM-RESULTS restarts.
- The branch has a regression here. After REPOSITION-TO-ROWID to g=3/v=1, GET-NEXT returns g=1 v=3. OpenEdge and trunk return g=3 v=2. The presorted layer isn't moved to the random-access position. This needs fixing.
Side finding, not caused by the branch. FIRST-OF on a group's first row reached by REPOSITION is no in OpenEdge and yes in FWD. Trunk does the same.
#62 Updated by Artur Școlnic 8 days ago
Regarding the scope of the issue, I can attempt to limit it, but in the development process I kept finding regressions compared to trunk which meant new sets of changes. I can try to limit the scope without causing regressions, is it ok if I move the 'essential' changes in 10806b?
#63 Updated by Eduard Soltan 8 days ago
Alexandru Lungu wrote:
- Eduard, please review
IndexSelectionWorkerchanges and index_selection.rules
I think the changes in IndexSelectionWorker@ and index_selection.rules are safe.
#64 Updated by Artur Școlnic 7 days ago
- Status changed from Review to WIP
Besides the above mention reposition regression, there is also one in chui tests, working on fixing them.
#65 Updated by Artur Școlnic 7 days ago
Artur Școlnic wrote:
there is also one in chui tests
Revision 16800 causes it.
When another session commits to the walked table, AdaptiveQuery.resumeAtReferenceRow now switches a FOR EACH walk to PagedResults, whose pages are read through a forward-only ForwardResults cursor. resumePageIndexValues only checks walksForwardOnly() and pagingShapeRejection; it does not reject a scrolling query, as the normal paging path does (AdaptiveQuery.java:4342). AdaptivePresortQuery is always scrolling, because finishInitialize calls setScrolling().
The FOR EACH is TransactionType.FULL, so each iteration commits. On every commit cachePagedResults -> PagedResults.cachePage -> cachePageCursor -> PreselectQuery.cacheResults takes the scrolling branch: it calls target.reset() and re-reads from the start. ForwardResults.reset() cannot rewind; it only logs Cannot call reset in FORWARD_ONLY mode. The cached page therefore holds only the rows after the cursor, but is positioned at the cursor's old row number, so rows are skipped. The page then looks shorter than its limit, and the walk ends early.
#66 Updated by Alexandru Lungu 7 days ago
Yes. Cross-session invalidation in AdaptiveQuery is switched off by a hardcoded forceGlobalInvalidation = false, so outside this branch no AdaptiveQuery sees other sessions' commits. The foreign-commit code is Session.noteWrite, the SQLQuery hook, the Persistence counters, the cap in PagedResults and resumeOnForeignCommit in AdaptiveQuery. I'll remove it and defer it to its own task. One catch: resumeAtReferenceRow() also runs on a normal session close (SESSION_CLOSING_NORMALLY), so that call stays or falls back to invalidate(true).
Please do defer.
It only limits memory. AdaptivePresortQuery must scroll, while trunk's non-scrolling PresortQuery kept just the previous, current and next rows. It is enabled only for a standalone, unbrowsed FOR EACH (walksForwardOnly()). Every repositionByID comes through QueryWrapper, so REPOSITION can't reach that query, and no code reads the dropped rows. So there is no example of a visible clear. Named queries keep their rows, which they must: OpenEdge allows REPOSITION backwards on BREAK BY.
The performance improvement is marginal. There was effort to move cursors from Record[] to Long[]. Thus, there may be millions of records stored safely in such cursor without much fuss. We will consider such performance improvement in the future, but today it is not worth it. The memory problem is already there in trunk.
resumeFetcher, resumeIndexValues and resumePaged are the paged form of resumeAtReferenceRow(), which runs on a foreign commit and on a normal session close. Without them, both paths switch to dynamic mode (row by row). They can be deferred with the foreign commits, at that cost on session close.
Please defer.
I'll move them into AdaptivePresortQuery.
Perfect.
The presorted layer takes the record the result set is already on, instead of loading it again with quickLoad by id. The scrolling backing returns only ids, so that reload would be one SELECT per row.
This needs consideration. The scrolling backing returns only ids if the query is share/exclusive lock. This is because we always want the last image of the record before loading in the buffer. This is implemented in AQ already and it may be leveraged by the presort implementation.
When you say that the presorted layer takes the record the rs is already on, do you mean is re-hydrating from result-set? If so, then hydrate is first looking in the session cache providing the latest cached image. If the record was deleted in the mean-time, mind that AQ fetch is raising MissingRecordException and continues the next or whatever API was triggering it. If load by values or reposition is used, then other resolutions are employed. It is very important to avoid bringing back to life records from rs that were deleted in the mean-time, especially if the locking level was share/exclusive.
Q: Can an AdaptivePresortQuery be a CompoundQuery component?
So this means that PresortCompoundQuery is still preselecting results and not leveraging the dynamic behavior, right? I don't want to scope this topic on this task, but we should account that if we will ever make PresortCompoundQuery non-preselecting, then:
- individual components can become
AdaptivePresortQuery. - optimizer may join several
AdaptivePresortQueryin a multi-tableAdaptivePresortQuery, which its own invalidation/revalidation rules.
Q: Enhanced browse: is added sorting considered for BREAK?
This will need testing. Each time a sort column is added to the query, its state may change (from adaptive, to preselect; so from adaptive presort to simply presort and back on) - dependning if the BY matches an index prefix at runtime.
Q: Is INDEXED-REPOSITION possible?
This will be a PITA. Anyway, we will figure it out.
Regarding the scope of the issue, I can attempt to limit it, but in the development process I kept finding regressions compared to trunk which meant new sets of changes. I can try to limit the scope without causing regressions, is it ok if I move the 'essential' changes in 10806b?
Can you exemplify? I find it hard to understand how a test could regress that required foreignCommit, considering that neither the trunk PresortQuery not AQ doesn't honor it.
#67 Updated by Alexandru Lungu 7 days ago
Artur, do you manage to port the changes into a 10806b only with some of the essential behavioral changes? If you encounter regressions comparing with trunk, please post them here in order to assess.
I prepared in parallel a test suite for acceptance testing 10806b. If the task branch will pass them without regressions comparing with trunk, I will approve the merge to ameliorate #11879
#68 Updated by Artur Școlnic 7 days ago
I managed to get the branch in a somewhat stable condition, there is only one known regression compared to trunk at this point.
The regression is an endless loop in a BREAK BY walk. It happens when the walk opens while the same session has an uncommitted change to the field the walk sorts on.
The test that shows it (TestUncommittedCreateAndOrdering.testPresortOrdersByPendingSortValue):
1. Table dsNonunique has two rows with si1 = "S1": P1 with si2 = BBB, and P2 with si2 = CCC.
2. Inside a transaction, buffer b-ds changes P1's si2 to ZZZ. Nothing is committed or flushed: the database still holds BBB, and only the session knows ZZZ.
3. A procedure then walks FOR EACH q-ds WHERE si1 = "S1" EXCLUSIVE-LOCK BREAK BY q-ds.si2.
OpenEdge returns CCC, then ZZZ, and stops. Trunk returns the same. The branch returns CCC, ZZZ, CCC, ZZZ, … forever, and the test never finishes.
Why trunk gets it right: trunk runs this walk as a preselect PresortQuery. It reads both rows once, then sorts the records in memory using the values the session sees. P1 is ZZZ in the session, so the order is CCC, ZZZ, and the list is fixed, so the walk ends.
What the branch does instead, step by step:
1. The walk is now an AdaptivePresortQuery, a kind of AdaptiveQuery. At the open, AdaptiveQuery notices that the session has uncommitted changes to this table. A preselect would read stored database values and miss them, so it switches to dynamic mode right away.
2. In dynamic mode, rows come from a RandomAccessQuery worker, one at a time, the way FIND NEXT works. It finds "the row after the one I'm on" by comparing sort keys.
3. The worker starts and finds CCC. Then it looks for the row after CCC; its search sees the session's pending value, so P1 counts as ZZZ, which sorts after CCC. It returns P1. Correct so far.
4. Next it looks for the row after P1. To do that it needs P1's sort key, and it takes it from its reference record, an internal copy (RecordState{COPY}) that holds the stored database value BBB, not the pending ZZZ. The row after BBB is CCC, so it returns CCC again.
5. From CCC it finds P1 again, from P1 it goes back to CCC, and so on. The presort layer above trusts whatever order it gets, so it delivers CCC, ZZZ, CCC, ZZZ, … and the FOR EACH never reaches its end.
Why it's a regression, and whose defect it is: the inconsistency in steps 3–4 (search with the pending value, position from the stored one) belongs to AdaptiveQuery's dynamic mode, not to the presort code. Trunk has it too: the same scenario with plain BY si2 instead of BREAK BY is an AdaptiveQuery on trunk, and it loops there (CCC, BBB, CCC, BBB, …). Trunk's BREAK BY only escaped because it was a preselect that never entered dynamic mode. The branch turns BREAK BY into an AdaptiveQuery, which is what makes it change-sensitive, so it now takes the same broken path.
#69 Updated by Alexandru Lungu 7 days ago
Why it's a regression, and whose defect it is: the inconsistency in steps 3–4 (search with the pending value, position from the stored one) belongs to AdaptiveQuery's dynamic mode, not to the presort code. Trunk has it too: the same scenario with plain BY si2 instead of BREAK BY is an AdaptiveQuery on trunk, and it loops there (CCC, BBB, CCC, BBB, …). Trunk's BREAK BY only escaped because it was a preselect that never entered dynamic mode. The branch turns BREAK BY into an AdaptiveQuery, which is what makes it change-sensitive, so it now takes the same broken path.
This is acceptable. This is a known bug in AQ. In fact, it is a potential bug in 4GL as well if changing records inside the for each that move the record later without proper guarding. So:
- trunk has wrongly preselecting presort and bugged AQ
- branch has AQ and APQ leverage the same bugger engine. But this engine is the actual dirty-share implementation which will be rather replaced with #8388 soon enough.
- as long as the infinite loop was not a problem yet through AQ, I think having it in PAQ as well acceptable. It is exploiting an existing issue that is under fixing now.
#70 Updated by Alexandru Lungu 7 days ago
Eric, can you provide some feedback on #10806-68 and -69? The goal of this task is achieved to move PresortQuery to a dynamic model, but it now surfaces AQ bugs. We fix the preselecting incorrect philosophy of presort and replace it with a buggy dynamic implementation (that is already used by AQ) which uses dirty-share. The same failing test on the brand new AdaptivePresortQuery was also failing on AdaptiveQuery in trunk. The PresortQuery from trunk doesn't fail as it is (incorrectly) preselecting.
#71 Updated by Alexandru Lungu 7 days ago
Alexandru Lungu wrote:
Eric, can you provide some feedback on #10806-68 and -69? The goal of this task is achieved to move PresortQuery to a dynamic model, but it now surfaces AQ bugs. We fix the preselecting incorrect philosophy of presort and replace it with a buggy dynamic implementation (that is already used by AQ) which uses dirty-share. The same failing test on the brand new
AdaptivePresortQuerywas also failing onAdaptiveQueryin trunk. ThePresortQueryfrom trunk doesn't fail as it is (incorrectly) preselecting.
If we have the green light on this, please wrap up the changes in the b branch and let me review + test.
Please prepare cross-customer testing as far as we can ASAP.
#72 Updated by Artur Școlnic 7 days ago
10806b contains the new AdaptivePresortQuery and the core set of changes necessary to support it, the reposition regression fix and the chui regression fix. It causes one known regression in the single user test suite. Alex, please review.
#73 Updated by Alexandru Lungu 7 days ago
- Status changed from WIP to Review
#74 Updated by Alexandru Lungu 7 days ago
Please start testing whatever you have in hand and ask team around to fast track this.
#75 Updated by Artur Școlnic 7 days ago
10806a passed the multi tenant app testing and chui had one regression which is fixed. I will start with etf and maybe find someone to test the large gui app with 10806b.
#76 Updated by Alexandru Lungu 7 days ago
Artur Școlnic wrote:
10806a passed the multi tenant app testing and chui had one regression which is fixed. I will start with etf and maybe find someone to test the large gui app with 10806b.
Can you reuse conversion done with 10806a to test 10806b alone?
Mind that etf and other large gui app will need reconversion.
#77 Updated by Artur Școlnic 7 days ago
Alexandru Lungu wrote:
Can you reuse conversion done with 10806a to test 10806b alone?
For the multi tenant app and chui, yes. etf conversion + runtime takes 3.5 hours.
#78 Updated by Eric Faulhaber 7 days ago
Alexandru Lungu wrote:
Eric, can you provide some feedback on #10806-68 and -69? The goal of this task is achieved to move PresortQuery to a dynamic model, but it now surfaces AQ bugs. We fix the preselecting incorrect philosophy of presort and replace it with a buggy dynamic implementation (that is already used by AQ) which uses dirty-share. The same failing test on the brand new
AdaptivePresortQuerywas also failing onAdaptiveQueryin trunk. ThePresortQueryfrom trunk doesn't fail as it is (incorrectly) preselecting.
My main concerns are that we are:
- fixing the core problem in the customer application which was worked around by adding the buffer release;
- not introducing new regressions;
- not surfacing old latent bugs which make the runtime fail in new ways.
In other words, if we're fixing the core "missing buffer release" problem without making the overall behavior worse, I'm good with scoping back the fix as you suggest.
However, notes 68 and 69 seem to contradict each other. 68 says the new branch causes a regression not shown by trunk. 69 says it's an existing problem. If we're not exposing it in trunk (or the custom build line where the bug was reported), but we are with this branch, isn't that effectively a new regression?
#79 Updated by Artur Școlnic 7 days ago
AdaptivePresortQuery brings some of the issues from AdaptiveQuery to new 4gl query patterns in 10806b.
#80 Updated by Eric Faulhaber 7 days ago
Artur Școlnic wrote:
AdaptivePresortQuery brings some of the issues from AdaptiveQuery to new 4gl query patterns in 10806b.
I'm not sure what is meant by "new" patterns. For the same 4GL code base, we don't want to see net new failures.
So, what does your statement mean in practical terms? Will users see new failures where the runtime was working correctly with those 4GL query patterns before? Or were those query patterns failing already before the changes (perhaps in a different way)? We shouldn't introduce new failures where there were none before.
#81 Updated by Artur Școlnic 7 days ago
The customer might see new failures in presort queries.
#82 Updated by Artur Școlnic 7 days ago
The multi tenant app and chui (ran only the previously failing tests) seem fine with 10806b. Alex, have you had a chance to run your suites?
#83 Updated by Alexandru Lungu 7 days ago
Artur Școlnic wrote:
The multi tenant app and chui (ran only the previously failing tests) seem fine with 10806b. Alex, have you had a chance to run your suites?
This is WIP. Managed to confirm they pass in OE and run them with a baseline in FWD. Running them now with 10806b (4.7k files).
#84 Updated by Alexandru Lungu 7 days ago
So, what does your statement mean in practical terms? Will users see new failures where the runtime was working correctly with those 4GL query patterns before? Or were those query patterns failing already before the changes (perhaps in a different way)? We shouldn't introduce new failures where there were none before.
A scenario that was always working in a customer application will potentially regress now under specific contexts. This is a trade-off from a trivial implementation of PresortQuery that we have in trunk vs the complex one in 10806b. We are aware of limitations on dirty-share, even intra-session. But in order to make PresortQuery work in a dynamic fashion, we have to leverage that (nasty) dirty-share implementation. This is the requirement of #11879 - it needs the partial dirty-share implementation in order to work with the risk for some cases to hit the bugs from dirty-share.
Artur's test unveil a dirty-share bug that also holds for non-PresortQuery, but which is also visible now in PresortQuery.
In order to overcome this, we need more dirty-share fixes - #8388 to start with.
However, notes 68 and 69 seem to contradict each other. 68 says the new branch causes a regression not shown by trunk. 69 says it's an existing problem. If we're not exposing it in trunk (or the custom build line where the bug was reported), but we are with this branch, isn't that effectively a new regression?
It is a new regression, yes. We trade a problem (lack of dynamism of PresortQuery) for another (the lack of completeness for dynamism in FWD). My request of feedback for you Eric was if this is acceptable. Because if it is not, #10806 is going to Pend for other dirty-share tasks - #8388 being one of them (and the only one for the moment).
#85 Updated by Alexandru Lungu 7 days ago
| Suite | Type | Ring | Tests | OE pass/fail/err | Baseline pass/fail/err | Task pass/fail/err | Regressed | Fixed | Still failing |
|---|---|---|---|---|---|---|---|---|---|
| bb-change-sens | ablunit | target | 41 | 39 / 0 / 2 | 32 / 9 / 0 | 40 / 0 / 1 | 0 | 8 | 1 |
| bb-idx-group | ablunit | target | 6 | 6 / 0 / 0 | 4 / 2 / 0 | 4 / 2 / 0 | 0 | 0 | 2 |
| bb-idx-nav | ablunit | target | 5 | 5 / 0 / 0 | 5 / 0 / 0 | 5 / 0 / 0 | 0 | 0 | 0 |
| bb-group | ablunit | neighbour | 63 | 60 / 0 / 3 | 62 / 0 / 1 | 62 / 0 / 1 | 0 | 0 | 0 |
| bb-nav | ablunit | neighbour | 45 | 36 / 0 / 9 | 39 / 0 / 6 | 39 / 0 / 6 | 0 | 0 | 5 |
| aq-client-where | ablunit | neighbour | 4 | 2 / 0 / 2 | 2 / 0 / 2 | 2 / 0 / 2 | 0 | 0 | 2 |
| ir-shape | ablunit | neighbour | 44 | 44 / 0 / 0 | 41 / 3 / 0 | 41 / 3 / 0 | 0 | 0 | 3 |
| fv-shape | ablunit | neighbour | 152 | 133 / 19 / 0 | 98 / 54 / 0 | 127 / 25 / 0 | 0 | 29 | 6 |
| query-top | ablunit | neighbour | 619 | 507 / 41 / 71 | 371 / 202 / 46 | 371 / 202 / 46 | 0 | 0 | 174 |
| ir-rest | ablunit | neighbour | 876 | 580 / 0 / 296 | 757 / 105 / 14 | 757 / 105 / 14 | 0 | 0 | 76 |
| aq-progressive | ablunit | neighbour | 579 | 566 / 0 / 13 | 518 / 52 / 9 | 520 / 50 / 9 | 0 | 2 | 46 |
| aq-pt | ablunit | neighbour | 345 | 342 / 2 / 1 | 283 / 61 / 1 | 283 / 61 / 1 | 0 | 0 | 59 |
| ps-late-change | ablunit | target | 104 | 104 / 0 / 0 | 102 / 0 / 2 | 102 / 0 / 2 | 0 | 0 | 2 |
| bb-foreign-commit | harness | target | 18 | 17 / 1 / 0 | 18 / 0 / 0 | 17 / 1 / 0 | 1 | 0 | 0 |
| ps-grouping | ablunit | neighbour | 346 | 346 / 0 / 0 | 260 / 76 / 10 | 259 / 77 / 10 | 2 | 1 | 85 |
| ps-navigation | ablunit | neighbour | 390 | 390 / 0 / 0 | 278 / 97 / 15 | 343 / 29 / 18 | 4 | 69 | 43 |
| ps-accum | ablunit | neighbour | 145 | 145 / 0 / 0 | 122 / 17 / 6 | 122 / 17 / 6 | 0 | 0 | 23 |
| ps-expr-reuse | ablunit | neighbour | 24 | 24 / 0 / 0 | 24 / 0 / 0 | 24 / 0 / 0 | 0 | 0 | 0 |
| ps-id-only | ablunit | neighbour | 48 | 48 / 0 / 0 | 48 / 0 / 0 | 48 / 0 / 0 | 0 | 0 | 0 |
| first-of-last-of | ablunit | neighbour | 94 | 94 / 0 / 0 | 84 / 0 / 10 | 84 / 0 / 10 | 0 | 0 | 10 |
| fv-pending | ablunit | neighbour | 98 | 98 / 0 / 0 | 85 / 13 / 0 | 96 / 2 / 0 | 0 | 11 | 2 |
| fe-client-sort | ablunit | neighbour | 55 | 55 / 0 / 0 | 29 / 26 / 0 | 29 / 26 / 0 | 0 | 0 | 26 |
| aq58-commit | ablunit | neighbour | 159 | 158 / 1 / 0 | 147 / 12 / 0 | 147 / 12 / 0 | 0 | 0 | 11 |
| aq58-compound | ablunit | neighbour | 86 | 86 / 0 / 0 | 86 / 0 / 0 | 86 / 0 / 0 | 0 | 0 | 0 |
| aq58-find | ablunit | neighbour | 181 | 181 / 0 / 0 | 163 / 0 / 18 | 163 / 0 / 18 | 0 | 0 | 18 |
| aq58-joins | ablunit | neighbour | 189 | 187 / 1 / 1 | 169 / 17 / 3 | 169 / 17 / 3 | 0 | 0 | 18 |
| aq58-mode | ablunit | neighbour | 772 | 627 / 0 / 145 | 649 / 0 / 123 | 649 / 0 / 123 | 0 | 0 | 11 |
| aq58-navigation | ablunit | neighbour | 474 | 474 / 0 / 0 | 471 / 0 / 3 | 471 / 0 / 3 | 0 | 0 | 3 |
| aq58-non-abend | ablunit | neighbour | 70 | 70 / 0 / 0 | 52 / 17 / 1 | 52 / 17 / 1 | 0 | 0 | 18 |
| aq58-unflushed | ablunit | neighbour | 147 | 147 / 0 / 0 | 139 / 3 / 5 | 139 / 3 / 5 | 0 | 0 | 8 |
| Total | 6179 | 5571 / 65 / 543 | 5138 / 766 / 275 | 5251 / 649 / 279 | 7 | 120 | 652 |
#86 Updated by Alexandru Lungu 6 days ago
Regarding regressions:
1. Record visibility: a row created ahead of the cursor is now delivered (1 test)¶
tests/persistence/sort/break_by_grouping/break_flags_under_retry_and_client_where/RetryWithCreateAhead.cls#checkRetryWithCreateAhead
FOR EACH tt BREAK BY tt.nm. On rowAAAthe body createsBBX, which sorts ahead of the cursor; the first attempt doesUNDO, RETRY.- OE and the baseline: trace
AAA,BBB,CCC. The walk never deliversBBX, although it is in the table afterwards (count 4). - Task: trace
AAA,BBB,BBX,CCC.
Expected: trace= AAA,BBB,CCC|count= 4|row= AAA 1|row= BBB 2|row= BBX 9|row= CCC 3| but was: trace= AAA,BBB,BBX,CCC|count= 4|row= AAA 1|row= BBB 2|row= BBX 9|row= CCC 3|
The new change-sensitive AdaptivePresortQuery picks up a row that the loop body created ahead of the cursor, in a create-ahead plus UNDO, RETRY shape where OE's BREAK BY walk does not. This is close to the core of #10806, so it needs a decision on which create-ahead shapes OE actually delivers.
2. Dynamic BREAK BY queries are treated as forward-only (5 tests, one probable cause)¶
Probable cause: the QueryWrapper change "An unbrowsed BREAK BY query navigates as a forward-only one: GET LAST/PREV ..." is too broad. On a dynamic, non-browsed BREAK BY query, GET-FIRST after a forward walk and GET-LAST now raise the forward-only errors. OE 11.6.3 allows both.
| Test | Scenario | OE / baseline | Task |
|---|---|---|---|
break_by_grouping/LinkQueryPrepareT02PrepareTwoLevelsAndBadLevel.cls#T02PrepareTwoLevelsAndBadLevel |
QUERY-PREPARE("for each room no-lock break by room.floor by room.type-of-room"), walk forward, then GET-FIRST |
allowed | error 12394 "Cannot move Query back to FIRST when FORWARD-ONLY or -noautoreslist enabled" at hQry.unwrapQuery().getFirst() |
break_by_query_navigation/Ticket7496FromReviewBreakByGetFirstAfterNext.cls#breakByGetFirstAfterNext |
dynamic break by tt.grp: GET-FIRST, GET-NEXT, GET-FIRST |
error 0 | fails (Expected: first alone: 10) |
break_by_query_navigation/QueryForwardOnlyGetprevGetlastRejectedT08WithoutNoErrorRaises.cls#t08WithoutNoErrorRaises |
dynamic break by room.floor, GET-LAST |
allowed, error 0 | raises, "not reached" never written. Passes alone, fails in the suite session |
break_by_query_navigation/break_by_get_last_no_projection/ForEachBreakByLastOfControl.cls#forEachBreakByLastOfPerGroup |
FOR EACH ... BREAK BY with LAST-OF; the test starts with Assert:IsFalse(ERROR-STATUS:ERROR or ERROR-STATUS:NUM-MESSAGES > 0) |
passes | Expected: FALSE but was: yes. Passes alone, fails in the suite session |
break_by_query_navigation/break_by_get_last_no_projection/GetLastControlByWithoutBreak.cls#getLastWithByWithoutBreakReturnsFullRecord |
static OPEN QUERY ... BY (no BREAK), GET LAST; same ERROR-STATUS check at the start |
passes | Expected: FALSE but was: yes. Passes alone, fails in the suite session |
- The first two fail alone and in the suite. The other three pass alone and fail in the suite session in 2 of 2 runs, while the baseline passes them in the same session. So they are order-dependent, not flaky.
ForEachBreakByLastOfControlandGetLastControlByWithoutBreakdo not fail on their own logic.ERROR-STATUScarries over between tests in one session, so an earlier test in the suite most likely now leaves a 12393/12394 behind. Not proven: I did not trace which earlier test leaves it.t08WithoutNoErrorRaisesfailing only in the session suggests that the forward-only decision also depends on state left by earlier queries in the session, not only on the query's own shape.
Fixing the forward-only decision for dynamic BREAK BY queries would probably clear all five.
3. Harness BW05: visibility now matches OE (OE-red regression)¶
tests/concurrency/break_by_foreign_commit/TestBreakByForeignCommitReader.cls#BW05CommitAheadControl (two-session harness)
- Another session commits a row ahead of the reader's BREAK BY cursor.
- OE delivers it (
Expected: 0 but was: 1). The baseline did not deliver it and passed. The task delivers it and fails with OE's own message, reproduced on rerun. - FWD now behaves like OE. The test's control expectation (
AHEAD_ROW_NOT_DELIVERED_PREFETCHED) is wrong and should be corrected to OE's behaviour.
Related findings that are not regressions¶
- New hang on the task build:
tests/query/break_by/change_sensitivity/indexed_presort_walk/TestForwardOnlyBreakByPostOpenCreate.cls#testForwardOnlyWalkPostOpenCreateDeliveryloops forever. On the baseline it failed (Expected: 4 but was: 3), so by the rules it is "still failing", but it now hangs instead of failing, and it blocked the whole target-suite session until the 20-minute cap. Its siblingtestDefaultBreakByDeliversPostOpenCreateis fixed. - Fixed by the branch: 120 tests.
ps-navigation69,fv-shape29,fv-pending11,bb-change-sens8 (post-open create/move visibility in indexed BREAK BY walks),aq-progressive2,ps-grouping1.
#88 Updated by Alexandru Lungu 5 days ago
- Status changed from Review to WIP
Artur, please keep testing and invest most of the time on this ticket.
Assess #10806-86 and other bugs you may find that are issues due to the port of PQ to APQ.
#89 Updated by Artur Școlnic 5 days ago
Almost all known regressions solved in 10806b.
r16819QueryWrapper.canGetFirst:GET FIRSTafter a move raises 12394 only for an index-walked BREAK BY (AdaptivePresortQuery). A materialised list can restart.GET LAST/PREVstill raise 12393 for every BREAK BY query (OE-verified tests inbreak_by_query_navigation).- Dropped
hydratesScrollingRows;ProgressiveResultsis back to trunk. database_access.rules: secondAdaptivePresortQueryemission site reverted toPresortQuery.
- Create-ahead after a batch
UNDO, RETRY(runs as NEXT). Measured on OE 12.8 (TestCreateAheadRetryDepth, 14 cases): rows created by that iteration stay hidden until the walk reaches the second existing row after the undone one. TransactionManager.isRetriedAsNext()tells the iteration's listeners that the RETRY ran as NEXT.BufferManager/RecordBufferreport the keys of created records to a watching query.AdaptivePresortQuery/Presorterpass over those rows.
Testing
- New tests checked on OE 12.8 first, expected values from OE: 27 cases, all pass on FWD.
- FWD:
break_by_query_navigation,break_by_grouping,query_shape_uncommitted_visibilityplus the new classes, 950 tests, compared with a clean r16817 (trunk).
| Build | Failing | vs trunk |
|---|---|---|
| trunk r16817 | 274 | - |
| 10806b r16820 | 171 | 106 fixed / 3 regressed |
- Fixed today:
RetryWithCreateAhead,TestCreateAheadVariants#betweenRetry,LinkQueryPrepareT02,Ticket7496FromReviewBreakByGetFirstAfterNext,T8100...ModifyInLoopThenRead, 3 x T6459 off-end tests. Nothing newly broken. - #10806-68 test (
TestPresortOrdersByPendingSortValue) passes. - Still regressed (3):
T08WithoutNoErrorRaises,ForEachBreakByLastOfControl,GetLastControlByWithoutBreak. FWD records a shown, uncaught dynamic 12393 inERROR-STATUS; OE does not. This is inErrorManager; IMO out of scope here.
Alex, I could not run your 6k tests suite, could you please run it?
#90 Updated by Alexandru Lungu 5 days ago
Alex, I could not run your 6k tests suite, could you please run it?
Please let me know what was the blocker. I am trying to refine the testing process so that any developer can use it.
#91 Updated by Artur Școlnic 4 days ago
1. Test sources that are not valid 4GL
- Unexpanded generator placeholders left in the source (e.g.
do n = 1 to $4:). The files do not parse. - References to procedures or classes that do not exist in the repository (e.g.
procWalkin theCoreAq*tests). - Tests written to check a compile-time rejection (e.g.
GroupSynonymForBreakRejected*). They cannot be converted, so they cannot sit in an ABLUnit run.
2. Tests using constructs the converter does not support
- ACCUM nested inside ACCUM (e.g.
accum total (accum maximum by f1 f2) * 2), inbreak_by_accumulators. - Embedded SQL statements in
.phelpers. - REPOSITION with a parameter type the expression converter rejects.
- About 18 tests whose generated Java does not compile.
These need either a converter fix or moving out of the ABLUnit suites into a separate conversion-gap set.
TestSuite aggregators
- The
TestSuite.clsaggregators reference the broken tests above, so they fail to convert as well. - The FWD test engine reaches sub-packages only through these aggregators. Without them, most of the suite is not discovered.
- Several aggregators also miss classes in their folder (e.g.
TestCreateAheadVariants,TestCreateAheadRetryDepth), which are then never run in a package run.
Tests missing from the repository
- The targeted
bb-*suites (tests/query/break_by: change sensitivity, indexed group, indexed navigation) are not committed. bb-foreign-commitis a two-session harness battery and does not run throughunittest.sh.
In the end I got claude to run 1,225 tests, no regressions on those.