Skip to content

IGNITE-28724 Calcite engine. Support SELECT ... FOR UPDATE query - #13366

Open
vldpyatkov wants to merge 10 commits into
apache:masterfrom
vldpyatkov:igite-28724
Open

IGNITE-28724 Calcite engine. Support SELECT ... FOR UPDATE query#13366
vldpyatkov wants to merge 10 commits into
apache:masterfrom
vldpyatkov:igite-28724

Conversation

@vldpyatkov

Copy link
Copy Markdown
Contributor

Comment on lines +831 to +861
List<SqlNode> ofCols = null;
SqlIdentifier col;
Long waitSeconds = null;
String waitValue;
}
{
qry = OrderedQueryOrExpr(ExprContext.ACCEPT_QUERY) { s = span(); }
[
LOOKAHEAD(<FOR> <UPDATE>)
<FOR> <UPDATE>
[
LOOKAHEAD(<OF>)
<OF>
{
ofCols = new ArrayList<SqlNode>();
}
col = CompoundIdentifier() { ofCols.add(col); }
(
<COMMA> col = CompoundIdentifier() { ofCols.add(col); }
)*
{ ofList = new SqlNodeList(ofCols, s.pos()); }
]
[
LOOKAHEAD(<WAIT>)
<WAIT> <UNSIGNED_INTEGER_LITERAL>
{
waitValue = token.image;

try {
waitSeconds = Long.parseLong(waitValue);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
List<SqlNode> ofCols = null;
SqlIdentifier col;
Long waitSeconds = null;
String waitValue;
}
{
qry = OrderedQueryOrExpr(ExprContext.ACCEPT_QUERY) { s = span(); }
[
LOOKAHEAD(<FOR> <UPDATE>)
<FOR> <UPDATE>
[
LOOKAHEAD(<OF>)
<OF>
{
ofCols = new ArrayList<SqlNode>();
}
col = CompoundIdentifier() { ofCols.add(col); }
(
<COMMA> col = CompoundIdentifier() { ofCols.add(col); }
)*
{ ofList = new SqlNodeList(ofCols, s.pos()); }
]
[
LOOKAHEAD(<WAIT>)
<WAIT> <UNSIGNED_INTEGER_LITERAL>
{
waitValue = token.image;
try {
waitSeconds = Long.parseLong(waitValue);
}
SqlIdentifier col;
Long waitSeconds = null;
}
{
qry = OrderedQueryOrExpr(ExprContext.ACCEPT_QUERY) { s = span(); }
[
LOOKAHEAD(<FOR> <UPDATE>)
<FOR> <UPDATE>
[
LOOKAHEAD(<OF>)
<OF>
{
List<SqlNode> columns = new ArrayList<SqlNode>();
}
col = CompoundIdentifier() { columns.add(col); }
(
<COMMA> col = CompoundIdentifier() { columns.add(col); }
)*
{ ofList = new SqlNodeList(columns, s.pos()); }
]
[
LOOKAHEAD(<WAIT>)
<WAIT> <UNSIGNED_INTEGER_LITERAL>
{
String waitValue = token.image;
try {
waitSeconds = Long.parseLong(waitValue);
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The previous variant is fit for existing code, but I can rewrite this close as you wish)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought again.
I guess I won't be able to move the variable declarations inside of the Java code here. Because it is a common approach in this file, and I don't want to be the first who violates it.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

)))

{
final Span s;
SqlNode qry;
SqlNodeList ofList = null;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rename it plz

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The same, but I see you want to put this in order. Ok, I will try.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

still the same ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed variable names and made the Java code slightly simpler.

IgniteResource.INSTANCE.selectForUpdateRequiresPessimisticTx().str(),
IgniteQueryErrorCode.UNSUPPORTED_OPERATION);

// waitSeconds: null = use tx remaining time (0), 0 = NOWAIT (-1), positive = ms.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(0) and (-1) in braces are confused, what is the purpose of such a comment ? plz extend it or remove

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I translate it right now. If you get in waitSeconds after parsing the query string, null it, which means you have to pass wait milliseconds 0, which means wait until the transaction is alive. If you get 0 (in waitSeconds) you have to pass -1L (or any negative digit) with means nowait, or if you get a digit, you have to convert seconds to milliseconds and pass the result.
I will extend the comment here.

waitMs = waitSeconds * 1000L;

// Zero means that retries are limited only by the transaction or query timeout.
long deadline = waitMs > 0

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
long deadline = waitMs > 0
long attemptsTimeout = waitMs > 0

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do not think there is a good change because the deadline is not a timeout; it is a time after which the query execution has to be finished.


if (userTx == null || !userTx.pessimistic())
throw new IgniteSQLException(
IgniteResource.INSTANCE.selectForUpdateRequiresPessimisticTx().str(),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

seems we need to align this 'strange' as for me message with :

org.apache.ignite.internal.processors.cache.GridCacheAdapter#lockTxEntriesAsync -> 
new IgniteCheckedException("Failed to acquire transactional lock in optimistic transaction."));

or ignore this check here and it will be raized a bit later, need to make sure that SqlException will be raized.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The message is not only for optomistic transaction is used. It will be appear when anyone try to liunch SELECT FOR UPDATE without transaction at all. For this reason is has a general statement.


IgniteTable igniteTable = (IgniteTable)schemaPlus.getTable(target.tableName());

if (igniteTable == null)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is it sys view case ? if so - it need to be handled more informative i suppose

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added a dedicated test for the system table. It fills in the other code fragment.
This exception is rather for completeness than for the specific case. I think we can be there in case where table removed concurrently.

IgniteInternalFuture<?> lockFut = GridTestUtils.runAsync(() -> {
try (Transaction tx = ignite0.transactions().txStart(PESSIMISTIC, READ_COMMITTED)) {
assertRows(
sql("SELECT p.id FROM Person p JOIN Dept d ON p.deptId = d.id WHERE p.id = 1 FOR UPDATE"),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why you avoid to inherit from AbstractBasicIntegrationTest ? and use all infrastructure like :

                assertQuery("SELECT p.id FROM Person p JOIN Dept d ON p.deptId = d.id WHERE p.id = 1 FOR UPDATE")
                    .returns(1)
                    .check();

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These tests do not read the transaction in SQL.
So I can try, but it leads to fixing the base class.

/**
* Integration tests for {@code SELECT ... FOR UPDATE} syntax.
*/
public class SelectForUpdateIntegrationTest extends GridCommonAbstractTest {

@zstan zstan Aug 4, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. sys view tests are absent
  2. virtual tables are absent table(system_range(1, 4)
  3. non PK involved tests are absent, or i miss ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. Added a test
  2. Added a test
  3. There are a lot of tests without condition on PK.

*
* @return Result cursor when locking succeeds, or {@code null} when the SELECT must be executed again.
*/
@Nullable private FieldsQueryCursor<List<?>> tryExecuteForUpdate(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The method is a bit hard to read; could you split the different parts into separate methods?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I split the method into three parts:

  1. Collect lock batches,
  2. Locks acquiring,
  3. Prepare result.

}

/** */
private void validateVersionColumnDmlTarget(SqlIdentifier id) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think other system fields should be modifiable during an update either. Could you check that? If so, we need to fix it for all of them preferably in a separate ticket, ideally one addressed before this one, to avoid bloating the changes here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the other system fields, the circumstances are more complicated.
For example, field _val can be used if it is the only field for a simple type like String or Integer.
A _key field is available to use in the INSERT command.

@Override public boolean isUpdateAllowed(RelOptTable tbl, int colIdx) {
final CacheColumnDescriptor desc = descriptors[colIdx];

if (QueryUtils.VER_FIELD_NAME.equals(desc.name()))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we could introduce a boolean field or something similar to avoid checking strings?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I fied it as desc instanceof SystemDescriptor, it not clear because we call _key and _val also system fileds, but they are instances of the other class (KeyValDescriptor).
Anyway is is correct right now.

// Both WHEN MATCHED and WHEN NOT MATCHED clauses in MERGE.
assert rowColumnsCnt == descriptors.length * 2 + updateColList.size() : "Unexpected columns count: " +
rowColumnsCnt;
assert rowColumnsCnt == 2 * descriptors.length + updateColList.size() : "Unexpected columns count: "

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like you haven't changed anything here other than repositioning the multiplication operator.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I changed it many times and finally returned to the origin.

protected List<List<?>> sql(IgniteEx ignite, String sql, Object... params) {
// {@code sql} can contain more than one query.
List<FieldsQueryCursor<List<?>>> allCurs = queryProcessor(ignite).query(queryContext(), "PUBLIC", sql, params);
Transaction tx = ignite.transactions().tx();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest not keeping these changes in this class but moving them to the appropriate ones, or creating a subclass of AbstractBasicIntegrationTest and defining this logic there.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like this change. Moreover, I think finally we understand that the AbstractBasicIntegrationTest.java is not the best base class.
Probably, we will make a refactoring in the future in order to it uses a public APY.


/** */
@Test
public void testSystemColumnsAreHiddenFromSelectStar() throws Exception {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Column name validation is missing.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This check is based on a column count.
The person table has fields id, name, and age, so this assertEquals(3, rows.get(0).size()) passes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants