-
Notifications
You must be signed in to change notification settings - Fork 1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[YSQL] Positional and Directional FETCH Not Yet Supported #6514
Comments
Summary: CURSOR feature is turned ON with the following notes. (1) Fixed a couple of crashes in "src/postgres/contrib/yb_pg_metrics/yb_pg_metrics.c" This module uses global variables to control the states of a PORTAL execution. This design is very flawed, and this work needs a redo. For now, workaround is added. - Introduced separate state-variables for block and statement initializations. - Used "querydesc" attribute as indicator for logging metrics because global state variables cannot be used when there are nested executions. (2) A number bugs on CURSOR are not fixed at this time due to its complexity. - Issue #6514 - Issue #6541 - Issue #6627 - Issue #6629 Test Plan: Add a few new test suites. More will be added later on. Reviewers: alex, mihnea Reviewed By: mihnea Subscribers: yql Differential Revision: https://phabricator.dev.yugabyte.com/D10135
Summary: CURSOR feature is turned ON with the following notes. (1) Fixed a couple of crashes in "src/postgres/contrib/yb_pg_metrics/yb_pg_metrics.c" This module uses global variables to control the states of a PORTAL execution. This design is very flawed, and this work needs a redo. For now, workaround is added. - Introduced separate state-variables for block and statement initializations. - Used "querydesc" attribute as indicator for logging metrics because global state variables cannot be used when there are nested executions. (2) A number bugs on CURSOR are not fixed at this time due to its complexity. - Issue #6514 - Issue #6541 - Issue #6627 - Issue #6629 Test Plan: Add a few new test suites. More will be added later on. Reviewers: alex, mihnea Subscribers: yql Differential Revision: https://phabricator.dev.yugabyte.com/D10327
On the use of "ysql_suppress_unsupported_error=true"All of the tests that are described in these comments:
and that are implemented in the attached .zip files were run after starting my YB cluster with this YSQL configuration parameter setting:
The default for this parameter is false. And when the default setting is used, every 0A000 occurrence, that in my tests is a warning and that I suppress by setting client_min_messages to error, is a genuine error—meaning that any statement that causes 0A000 rolls back and has no effect. issue-6514.zip — Bllewell summaryNOTE: the list that starts this report is wrong. Here are the statements that don't draw a warning (when client_min_messages is set to warning):
where :N is an integer that's greater than zero. This issue needs some testcases. In particular, it doesn't mention limitations in PL/pgSQL cursor functionality that stem from the underlying SQL functionality limitations. I just tested everything using YB-2.17.0.0. My tests are attached. It actually turns out that every operation that's listed above as "not yet supported" runs quietly without error and produces the expected result as long as you do this:
This suggests that, with another round of careful testing, this warning, and others like it for other variants of fetch, can simply be removed:
Similarly, move works fine when warnings are suppressed:
Notice that, when client_min_messages is warning, even create function when the body has a move statement draws the warning:
You get it again at runtime. But you also get the right result. Create the testcase objectsThe following shows the testcase concept. However, the real testcase (attached) uses a fine-grained control over client_min_messages to set it to error only when this is needed. Connect as an ordinary user to a database upon which it has the create privilege. First create and populate a suitable test table.
Next, create a procedure that declares a session-duration cursor. (There's no explicit PL/pgSQL statements that allow this. But dynamic SQL manages it without a problem.) The advantage of using a session-duration cursor for these tests is simply that you can create it once and forget about it without having to do everything within an explicitly transaction. (An an explicitly transaction makes ad hoc queries tedious because even a trivial typo breaks the txn so that you have to start again from scratch.)
Next, create a function that encapsulates the various fetch operations, using (according to the lang formal argument) either raw SQL (via the execute statement) or the dedicated PL/pgSQL fetch statement variants.
Next, create a function that encapsulates the various PL/pgSQL move statement variants, matching the variants one-to-one with the fetch variants.
Finally, create a driver table function that exercises all the variants according to its lang formal argument. When lang is sql, it exercises all of the SQL fetch variants but, of course, there's no "move". When lang is plpgsql, it exercises all of the PL/pgSQL fetch variants and all of the PL/pgSQL move variants.
Run the testcaseRun it first in YB and then in vanilla PG. Spool the output (using the "\o" meta-command), using suitable names like yb.txt and pg.txt, so that you can diff them. First, create the session-duration cursor and execute the few SQL fetch variants that get many rows each. (These cannot be used from PL/pgSQL.)
Now invoke the s.cur_rows_sql() encapsulation using both its sql mode and its plpgsql mode:
Diffing the two spool files shows that they are identical. The attached testcaseThis is presented as a single file, issue-6514.sql inside the attached issue-6514.zip. It's totally self-contained and you can simply start it at the ysqlsh or the psql prompt—with one caveat: it starts with this:
So, of course, you'll have to improvise a bit if the database that you connect to happens already to have a schema called s that's owned by a role other than the one as which you connect. Notice that the script uses this device to spool to a file that reflects whether you run the test in YB or PG:
The attached issue-6514.zip also contains the spool files yb.txt and pg.txt that were generated just before making the .zip. Here, the code decorates each output line with an asterisk if it required warnings to be suppressed. This means that diff will indeed shown differences because, of course, you don't get any warnings in PG and so there's no need to suppress them. However, it's very easy to read over these asterisk as see that the actual returned values are identical in PB and PG. |
What's expected when you scroll forwards and backwards in a cursor? And what's expected when the population of the result set that it defines is changed during the cursor's lifetime?What follows provides some background for understanding how things are supposed to work. Introductionnocaway asked me a question in a YB-internal Slack exchange:
He didn't ask a second question. But he should have:
I can't find explicit answers to these questions in the PG doc. But the answers are implied by a general understanding of how query execution works and of how SQL statements define outcomes in the face of concurrent activity (where the effect of one session's changes affects the set of rows another session's SQL statement sees). Mental model for move and fetch, forwards and backwards, for a non-holdable cursorHere's a simple mental model for what it means to move and fetch, forwards and backwards at will in or from a cursor. Use this table as the target for the cursor's defining subquery:
Now inspect the seventh row in the result set for an unrestricted subquery with no order by against s.t. (Assume that you can be sure that no other sessions can change the contents of s.t during the test.) It doesn't matter that, as the rules of SQL promise, you cannot in general predict which row it will be. Even so, within the tiny timespans that matter in this test, repeating this query, time and again, will always get the same result—within a particular database in a particular PG or YB cluster.
The random() function always generates the same sequence of values in successive invocations that follow invoking setseed() with the same actual argument. This is the result (using YB):
(It's different, of course, in PG because of the different physical storage systems that YB and PG use.) Now declare a cursor and also "snapshot" the result set of its defining subquery into the table pg_temp.cursor_simulation:
Once again, we can be sure that the subquery that defines both the cursor and the content of the pg_temp.cursor_simulation table will produce the same result set each of the two times that it's executed. Inspect the seventh row here too:
The result is the same for both methods—and is the same as the free-standing subquery got immediately before start transaction. Finally, traverse the entire result set, in both directions, using first the cursor and then the pg_temp.cursor_simulation table:
and:
The results are the same for both approaches, thus:
and:
In summary, this experiment informs a simple mental model:
Mental model for the immunity of the result set that a cursor defines to committed table changes from other concurrent sessionsEnsure that you issue rollback to finish the previous test. Set up for the next test as for the previous one. But make the choice of isolation level explicit.
Now do this variant on move and fetch from the previous test:
This is the result:
So far, make sure that the session where you do this is the only session that's connected to the present database. Then connect to the same database as the same user in a second session and do this:
Make sure that, in this second session, the plain \set metacommand shows this usual setting:
Back in the first session, repeat the move and fetch from the cursor:
If you do this test using PG, then you'll see that the results haven't changed—even though a second session has committed changes to the table that the cursor's subquery references and the first session is in flight in a read committed transaction. Emphasize the point by doing this (still within the same ongoing transaction):
This does show the committed changes that the second session made, consistent with the proper semantics of the read committed isolation level.
So far, the results are identical, when you do the steps in this exact order, in both PG and in YB. However (because of a YB bug—see below) you'll see the intended behavior only in PG if you do the second session's update statement before the first move and fetch in the first session. This is the intention:
Now stress test this rule by changing the order of the steps, thus:
When you do this in PG, the outcome is exactly the same as when you use the first order. But when you do in in YB, the move and fetch get a different outcome, thus:
Now repeat the update statement in the second session and come back to the first session and repeat the move and fetch. Now the results are unchanged. In summary:
We must regard the PG behavior as tautologically correct. And we must therefore conclude this:
Of course, for the use cases for which YB is intended, concurrent data changes within the relevant tables is the common case. How are move and fetch implemented, for a non-holdable cursor, when the execution plan cannot be run backwards?The execution plan for the cursor's defining subquery can sometimes be executed correctly in both the forwards and the backwards directions. It's easy to picture this:
Here, the scheme that supports a forwards sequential scan over the table's rows can just as well support a backwards scan. Many execution plans are reversible in this way. (Experts on the plpgsql-general email list have suggested that this is the common case.) However, not all execution plans are reversible. Here is a simple but compelling example:
Now do this at the psql (or ysqlsh) prompt:
It finishes silently and without error. Notice that the function s.f() is opaque to the planner. It simply returns its result set in the order that each next row is computed. And it's impossible for the planner to execute it any other way. This means that, in order to support the operations like fetch prior that declaring the cursor as scrollable allows, the results (at least the unfiltered results from the generate_series() function) must be cached. I can't find this detail in the PG doc. But PG expert Laurenz Albe mentions it in his blog post WITH HOLD cursors and transactions in PostgreSQL. Start with this to see the tests that are made on the output of generate_series() to restrict it to the result set that the cursor's subquery defines:
The move absolute 0 statement produces no SQL output and no raise info output. (This is because its meaning, "go to just before where the data starts", is fixed. The fetch all statement produces this SQL output:
And raise info reports this (the <-- annotations were added manually):
Now do this:
It produces no SQL output. And raise info reports this:
Notice that this time, we do not see series() invoked in the raise info output. This, and the results from the previous move and fetch, together tell us that the unfiltered output from generate_series() is cached—and now it's held in a structure that has frozen the order (whatever it was) in which the rows were produced. And it tells us that the where clause is applied anew to the cached raw data on every fetch or move invocation. If you create this function:
and invoke it in the select list, then you'll see that it, too, is applied after the fact to the cached raw data. Given these facts (that just the raw data is cached and that select list and where clause functions are applied to the cached data at fetch time, I cannot see how to design an experiment that would show whether or not ordinary table data is, or is not cached on the first fetch. This detail seems to be undocumented and also seems to be pointlessly inefficient. (Contrast it with what happens, see below, when the cursor is declared as holdable.) I asked about it on the pgsql-general list HERE. But I didn't get a useful reply. How are move and fetch implemented—for a holdable cursor?Here, it doesn't matter whether or not the execution plan is reversible because, by definition of holdable, the result set is materialized at cursor declaration time. Rollback to finish the example from the previous section and do this:
We see no SQL output. But we do see raise info output already now, thus:
Now use the same move and fetch that we used in the previous section:
It produces the same SQL result as in the without hold text, as expected:
But we see no raise info output. This tells us that—in contrast to how it's done for a without hold cursor—here (for a with hold cursor), it's the already-filtered result set that's cached (at the moment that the cursor is declared). If you create the function select_list_fn(i in int) and include this in the cursor's subquery, then you'll see that this, too, is taken account of at cursor declaration time to define the values that are cached. There seems to be a very strange and arbitrary difference between how caching is done for the with hold cursor and the without hold cursor. There seems to be no rationale for deferring the use of select list and where clause functions at caching time and then applying them after the fact to the cached data. This difference in approach relatively harms the performance of the without hold case but makes no useful semantic difference. Apparently, the without hold cursor is more commonly used than is the with hold cursor. (Of course, you would see differences in output if the filter() function or the select_list_fn) function were not immutable. But using such a function in a SQL statement is anyway unsafe.) Finally, because the entire result set for the cursor's subquery is materialized at declaration time, it is bound to be properly immune to the effects of changes made to the contents of the tables that the subquery references during the cursor's lifetime. |
issue-6514-2.zip — Bllewell summaryThe attached issue-6514-2.zip contains a self-contained testcase that explores the degrees of freedom suggested by:
This led to a complicated testcase:
This brings a total of thirty-five different tests. Moreover, some of the tests required interleaving table updates from a second session. So (without adding the complication of, say, a Python driver program) this implies following steps manually in two concurrent sessions. Here are the high-level conclusions:
NOTE: nocaway also proposed testing with cursor subqueries that have no order by. Such cursor declarations are included in the kit but are not used in the tests that are described. You can try them yourself by hand. But you can't mechanically compare the YB results with the PG results because differences in the storage mechanisms bring, with no ordering, different orderings in the two envs. Therefore, when the tests sample just the first few, the middle few, and the last few rows, all the values that you see are different. Even so, it's easy to conclude, by manual inspection:
Without tracing what's going on in the underlying implementation, these results seem to be bizarrely random. It's impossible to tell, therefore, if yet more tests would reveal yet more errors. However, the high-level conclusion is clear. Until the root causes are identified and fixed:
And even this conclusion is subject to the caveat that YB is vulnerable to DML from other sessions in the race condition window between declaring a cursor and first fetching from it—while PG is not vulnerable in this way. Testcase designTo run the testcase (for the case that no concurrent table changes are made) simply define the psql variables db and u:
and invoke 0.sql at the psql or ysqlsh prompt. You'll have to read this whole comment section to learn how to run the testcase in all of its variants. issue-6514-2.zip contains these files:
1-cr-tables.sqlThis has the two create table statements for the tables k__v and h_k__v shown above and these create procedure statements to populate and to update them:
2-cr-proc-open-cursor.sqlThis creates a procedure to declare the cursor cur in nine different ways:
It also creates the trivial fmt() helper function:
And it creates a trivial function to display the columns is_holdable and statement for the cursor cur from the pg_cursors catalog view:
This is used to annotate the spooled testcase output. Here is its output for the seven modes that the mechanical test uses. (Whitespace has been added by hand.)
3-cr-fn-rows-from-cursor.sqlThis creates the table function s.rows_from_cursor(). It performs these operations:
The return value after each is compared with the expected value. The expected value is defined, tautologically, as what the test produces in vanilla PG. There, the set of results that the schedule of fetches produces is the same in all thirty_five cases (each of the seven cursor declarations that the mechanical test uses combined with each of the five concurrent update variants). Sometimes, in YB, the fetch finds no row—because of the bugginess that the testcase reveals. This is detected as a null return value. And this is transformed, using coalesce(), into <no row> for readability. Only when the return value differs from the expected value does the table function return it. This makes the buggy cases very easy to spot. 4-cr-rule-off.sqlThis creates the trivial function s.rule_off() to add captions like this to the spooled output. Here's an example that shows a buggy outcome:
5-run-test.sqlHere it is:
Notice the psql variables whose values are accessed using :without_hold, :open_cursor_mode, :update_before_first_cursor_use, and :update_after_first_cursor_use. Three of the concurrent update scenarios can be run mechanically. In these cases for concurrent updates:
the testing can be done simply by invoking 5-run-test.sql from the driver script 6-run-all-tests.sql (below) and by setting update_before_first_cursor_use and update_after_first_cursor_use before each run. But the remaining two cases for concurrent updates:
must be stepped through manually by setting the psql variable open_cursor_mode by hand, switching to the second session at the right moment to issue call s.update_tables(true) and then switching back to the first session to finish the sequence. Notice that you must choose manually which leg of the \if :without_hold meta-command test to run to match the value that you set for open_cursor_mode. Values 2, 4, 5, 6, and 7 call for setting without_hold to true. And values 8 and 9 call for setting it to false. 6-run-all-tests.sqlHere it is:
ResultsHere they are:
For all tests except for this:
"Buggy" means that the cursor sees the effect of updates that are made to the table it queries when these are made, either in the same session or in a second session, between the invocation of open_cursor() and the first invocation of rows_from_cursor(). And for the tests for this exceptional holdability and subquery, plain "Buggy" means that any fetch after scrolling backwards sees the wrong row or no row at all; and "Double buggy" means what plain "Buggy" means together with seeing the effect of changes made to the underlying table during the cursor's lifetime. Notice the here, and only here, this departure from the proper behavior (MVCC as of the moment that the cursor is opened) occurs in one extra case: when the updates are done in a second session even after having already invoked rows_from_cursor() a few times. Manual testsIt's useful to look at move at fetch outcomes for the problem cursor definition. Before starting, make sure that you're not in an ongoing transaction:
(If client_min_messages happens to be set to error, then rollback will always finish silently, whether or not you're not in an ongoing transaction, and leave you with no ongoing transaction.) Now set up like this. (Only basic, top-level, SQL statements, with the exception of one anonymous block, are used here in this section.)
First, make sure that none of these statements draws a warning and produces correct results:
Here are its results to date:
All other fetch variants, and all of the move variants draw the 42601 warning, for example:
Moreover, it gets the wrong answer, thus:
And simply repeating it (which you might think would get the same wrong answer), gets a new wrong answer:
Now suppress the warnings and try some other examples:
Here are the three results, in order:
The first two results are correct; but the third is wrong. This means that the fact that fetch prior draws just warning is (arguably) itself a bug. It should (arguably) cause a straight error—and produce no result; You can repeat this sequence of three fetch statements, still in the same ongoing transaction, as many times as you want. The results are always the same. This seems to suggest that fetch first always gets the right result and that it's therefore a bug that it draws the warning:
Now use the popular technique that, in PG, famously gets you the correct count and correct rows, at the moment that the cursor is declared, even in the presence of heavy concurrent changes to the table from other concurrent sessions:
It finishes silently, time and again. This seems to suggest that move absolute 0 always gets the right result and that it's therefore a bug that it draws the (wrongly worded) warning:
It's then followed by this:
Now look at the effect of concurrent table changes from another session:
It produces these results. First:
And then, after updating the table in the second session:
So this test has shown the unique "double-buggy" outcome with just a few top-level fetch statements and without the possible complication of invoking the move and fetch operations using PL/pgSQL. Notice, however, the manifestation of the final wrong value for v, 1091 instead of 1001, is sensitive to what seem to be irrelevant changes. It goes away if you make any one of these changes, by itself, with respect to the code as presented:
Notice that you can remove "order by k" altogether. Because of the table's definition and how YB works under the covers, this doesn't change the ordering of the results. This change preserves the buggy behavior! Notice that all of these tests produce only correct results using vanilla PG. DiscussionCursor functionality in YB, as of this comment's timestamp, is seen to be massively buggy when you suppress warnings and test the unsupported operations. But when you avoid these, you lose almost all of the functionality that makes the feature interesting. The PG community regards the functionality as very useful. For example, PG expert Laurenz Albe has published these two recent blog posts on the topic:
Both of these posts use operations that draw the "unsupported" warning in YB and that, in certain tests, produce wrong results. He also uses the functionality in code that he's published for migrating data from foreign sources into PG. See HERE. The fact that my tests show many cases of bizarre, but reproducible, wrong behavior makes me suspect that the must be some gross bugs in YB's implementation—so gross that the causes ought to be identifiable by single-stepping through the internal implementation for these tests. I have a hunch that the reversibility property for the candidate plans for the subquery that defines a cursor might be one of the culprits. And maybe the code, adopted from PG, that intends to determine the reversibility property for a plan hasn't been properly modified to accommodate the fact that YB's storage model is so very different from PG's. This hunch is strengthened by the fact that the result set is always cached for a with hold cursor and that, here, the random navigation that you do with move and positional fetch never gets wrong results. (The fact that the MVCC moment is advanced in YB, w.r.t. PG, from the moment that declare is executed seems to be a separable problem.) It would be interesting to intervene in the reversibility test so that it always says "not reversible" and then to try all my testcases. One further testLook at the one row in the table of results above that has cells that say "Double buggy". This is the full select that defines the cursor for that row:
I added a new version of the procedure s.open_cursor() to examine this more closely. I put it in this file:
This is its content:
These, then, are the defining select statements for the two modes:
and:
I added a corresponding new version of the script to run the tests to focus on just the two modes 4 and 99 in this file:
This is its content:
I didn't bother with the other combinations for update_before_first_cursor_use and update_after_first_cursor_use. Simply cd to the investigating-effect-of-selecting-row-number directory (on the directory where you unzipped issue-6514-2.zip) and create the two files 2s-cr-proc-open-cursor.sql and 6s-run-all-tests.sql there using the content shown above. Then create a directory output as a peer to these two scripts and start 6s-run-all-tests.sql first using PG and then using YB. This is what I saw:
This observation strengthens the theory that the YB bugginess stems from this:
Maybe the plan could be executed in the backwards direction, but this is simply implemented wrong. Or maybe it can't be reversed, and so the code that detects reversibility needs to detect such cases. As I reason it, the plan for a select that has row_number() in the select list item can't be reversed because the successive values for row_number() can only emerge as the rows are produced one-by-one. And when the plan starts to produce rows, it has no idea how many there'll be. So it can't count downwards from the total number of rows. I'm guessing that we still use the code that we inherited from PG that detects when row_number() over() is present and returns "non-reversible". Because this causes the result set (in the forwards direction) to be cached, the net effect is bound to be correct for backwards operations. |
issue-6514-3.zip — Bllewell summaryThis account describes the overall design of some tests that explore if/when the result set that a cursor defines is cached and how the possible caching affects the actual values that move and fetch produce when volatile functions are used in the subquery that defines that cursor. It also draws some conclusions from the results. The attached issue-6514-3.zip contains these three .sql scripts:
The 0-*.sql script is the master script. It invokes the 1-*.sql and 2-*.sql scripts. The scripts produce both raise info output—from the functions s.f1(), s.f2(), and s.series()—and ordinary .sql output. Commands like this, at the O/S prompt (connecting both to YB and to PG) are used to produce spool files that interleave the raise info output with the SQL output:
Reference copies of the two spool files, yb-0.txt and pg-0.txt, are also included in issue-6514-3.zip. Overall designThese tests use three helper functions, thus:
Each of these functions uses raise info to report when it's invoked. The test declares a cursor in various different ways to explore these degrees of freedom:
These five binary degrees of freedom bring 32 different combinations. But, here, only six of these are included in the test to keep it manageably small. This is enough to show the general pattern:
How to run the testcaseFirst step through the testcase by hand, observing how the raise info output interleaves with the SQL output. It's enough to do this using just YB. Then do this, at the O/S prompt, to get both the raise info output and the SQL output into the same spool file:
And then diff yb.txt and pg.txt. You'll see that they are identical. The attached issue-6514-3.zip contains reference copies of these as yb-0.txt and pg-0.txt. 1-set-up.sqlRead the script to see the detail. This is the outline.
The s.f1() test gets this result on the first invocation of the select:
And you see "INFO: f1() invoked" five times. Successive repeats of the same select return ever-increasing values from f1() so that repeating the select for the second time produces this:
The s.f2() test gets this result on the first invocation of the select. And you see "INFO: f1() invoked" five times.
The first repeat of the same select returns this:
And so it goes on. The between ranges in f2() were chosen so that it will return the same value for each row when it's called thus:
2-fetch-and-move.sqlThe script precedes each substantive fetch or move statement with s.caption() to help the readability of the spooled output, thus:
The six actual testsNotice that the very same \ir 2-fetch-and-move.sql is used in all tests. Test 1
These are the SQL results:
Look at one of the two identical spool files to see how the raise info output interleaves with the SQL output. It always comes before the SQL output.
The outcomes are consistent with an implementation where the results are delivered simply by executing the plan explicitly in the forward or backward direction as requested, including therefore evaluating s.f1(), for every single fetch or move execution. NOTE: These observations are also consistent with an implementation that caches the raw result set from scanning the table and that invokes f1() whenever the cache is accessed. The test cannot distinguish between the two alternative hypotheses. Test 2This is identical to Test 1 except for the difference that this:
In other words, without hold is replaced by with hold and the explicitly started transaction that declares the cursor is immediately committed. Then each fetch is done in ordinary autocommit mode. And then the cursor is explicitly closed with close cur. Here, we see INFO: f1() invoked exactly five times immediately following commit. And here, we don't see any more raise info output. These are the SQL results:
The values of v are the same as in Test 1. But the values reported for f1() are different. Now they are neatly paired with the values for v. These results are consistent with an implementation where the entire result set is cached, when the transaction that creates the with hold cursor issues commit. Then all cursor accesses using fetch or move access the cached result set. Notice that (in contrast to what Test 3 shows) the result of invoking f1() in the select list is also cached. Test 3
In other words, it's is identical to Test 1 except for the single difference that this:
is replaced with this:
The thinking is that, because the immutable user-defined function s.series() is opaque, the execution plan cannot be run backwards—and so at least the raw results must be cached. (The order by is dispensed with in Test 2 because the results from s.series() are guaranteed to be produced in the order that they are computed.) The fact that s.series() starts with raise info 'series() invoked' allows this hypothesis to be tested. The SQL output for Test 3 is identical to that for Test 1 (as it must be for correctness). But the raise info output differs in just one way. Test 3's output starts with "INFO: series() invoked" while Test 1's output, of course, does not. This message is never seen again. (The "INFO: f1() invoked" output is identical in both tests.) This tells us that, at least in this test, the raw results from executing s.series() are cached while the function f1) is invoked when a result is fetched from the cache. Test 4This is identical to Test 1 except that it extends the select list with row_number(). The idea is that the plan for such a query cannot be reversible.
These are the SQL results:
These results are, for the select list items s.f1() and v, identical to those for Test 2. And the results for row_number() line up with those for v, just as the order by demands. When you look at the spool file, you'll see that the INFO: f1() invoked output is seen only when a row that has not so far been fetched or moved over is fetched or moved over for the first time. These observations are consistent with the hypothesis that a row is added to the result cache on demand when it is first produced. Moreover, the cached row value includes the effect of the select list function so that it is not re-invoked when the cache is accessed. Test 5This is the same as _Test 1 except that it removes s.f1() from the select list and adds the restriction where v = s.f2().
These are the SQL results:
When you look at the spool file, you'll see that the INFO: f2() invoked output is seen five times before the very first fetch first result is produced. Recall that f2() returns the value 1 for each of its first five invocations—so, for one point-of-view, just a single invocation of f2() is sufficient to produce the first result. The fact that f2() is invoked five times before the first result is seen is consistent with the hypothesis that analysis of the select statement or of its plan produced the decision that the entire already restricted result set should be cached when the very first fetch or move is done. Of course, then, the result set has just the single row for v = 1. Any subsequent fetch will either get that row—or get no row at all if the current position is that of the single result. Test 6This is the same as Test 5 except that it uses from s.series() without order by in stead of accessing the table s.t using an order by.
These are the SQL results:
Notably, they're different from those from Test 5 because of caching differences. When you look at the spool file, you'll see that first INFO: series() invoked is seen (and never seen again). Then the INFO: f2() invoked output is seen a single time and then the first SQL result is seen. Then the INFO: f2() invoked output is seen a single time again and then the second SQL result is seen. The second SQL result happens to be the same as the first SQL result because s.f2() produces the same result on its second invocation as on its first invocation. And so it goes on. The critical point here is that, though the row source delivers the same values for v in Test 6 and Test 5, the final SQL results are different. These results are consistent with the hypothesis that analysis of the select statement or of its plan produced the decision that the raw row source results should be cached when the very first fetch or move is done and that, thereafter, every fetch or move applies the where v = s.f2() restriction to the results that are retrieved from the cache. The deliberately contrived pattern of f2()'s volatile production of return values explains the rest of what the spool file shows. ConclusionBecause f1() and f2() were designed deliberately to be volatile, the different ways that caching is done lead to genuine semantic differences in the results. The PG doc (in the Cursors subsection of the PL/pgSQL chapter HERE) has this caution:
This caveat, together with the observed results, lead to this conclusion:
This, in turn, suggests that YB need not try to mimic PG's whimsical caching rules. On the other hand, if this outcome always, or even just sometimes, emerges from the direct re-use of the PG code, then this cannot be considered to be harmful. |
Yet more anomalies: functionality that causes the 0A000 "not supported" error in top-level SQL works fine in PL/pgSQLTry this in top-level SQL just as revision:
It causes the 0A000 error:
Replace fetch first from cur; with fetch last from cur; — the outcome is the same error but now with this message:
This is the hint for both:
Now try the same thing using a PL/pgSQL function:
It all works without error and produces this result:
This inconsistency in behavior is beyond belief. |
Jira Link: DB-4854
The following FETCH options are not yet supported.
The text was updated successfully, but these errors were encountered: