This is an automated email from the ASF dual-hosted git repository.
JingsongLi pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/paimon.git
The following commit(s) were added to refs/heads/master by this push:
new 23da742275 [core] Enforce LIMIT at the table-read level for
primary-key tables (#8116)
23da742275 is described below
commit 23da742275ba10d5e4dabecaba68bb5a45fc1113
Author: wangwj <[email protected]>
AuthorDate: Mon Jul 13 12:06:20 2026 +0800
[core] Enforce LIMIT at the table-read level for primary-key tables (#8116)
---
.../apache/paimon/reader/LimitRecordReader.java | 92 +++++++++
.../paimon/table/format/FormatTableRead.java | 48 +----
.../paimon/table/source/KeyValueTableRead.java | 15 +-
.../paimon/table/PrimaryKeySimpleTableTest.java | 229 +++++++++++++++++++++
.../paimon/flink/KeyValueTableReadLimitITCase.java | 110 ++++++++++
5 files changed, 444 insertions(+), 50 deletions(-)
diff --git
a/paimon-common/src/main/java/org/apache/paimon/reader/LimitRecordReader.java
b/paimon-common/src/main/java/org/apache/paimon/reader/LimitRecordReader.java
new file mode 100644
index 0000000000..59e1a767b8
--- /dev/null
+++
b/paimon-common/src/main/java/org/apache/paimon/reader/LimitRecordReader.java
@@ -0,0 +1,92 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.paimon.reader;
+
+import javax.annotation.Nullable;
+
+import java.io.IOException;
+import java.util.concurrent.atomic.AtomicLong;
+
+import static org.apache.paimon.utils.Preconditions.checkArgument;
+
+/** A {@link RecordReader} that stops after reading a given number of records.
*/
+public final class LimitRecordReader<T> implements RecordReader<T> {
+
+ private final RecordReader<T> reader;
+ private final long limit;
+ private final AtomicLong recordCount = new AtomicLong(0);
+
+ public LimitRecordReader(RecordReader<T> reader, long limit) {
+ checkArgument(limit > 0, "Limit must be positive.");
+ this.reader = reader;
+ this.limit = limit;
+ }
+
+ public static <T> RecordReader<T> limit(RecordReader<T> reader, @Nullable
Integer limit) {
+ if (limit == null || limit <= 0) {
+ return reader;
+ }
+ return new LimitRecordReader<>(reader, limit);
+ }
+
+ @Override
+ @Nullable
+ public RecordIterator<T> readBatch() throws IOException {
+ if (recordCount.get() >= limit) {
+ return null;
+ }
+ RecordIterator<T> iterator = reader.readBatch();
+ if (iterator == null) {
+ return null;
+ }
+ return new LimitRecordIterator<>(iterator);
+ }
+
+ @Override
+ public void close() throws IOException {
+ reader.close();
+ }
+
+ private class LimitRecordIterator<T> implements RecordIterator<T> {
+
+ private final RecordIterator<T> iterator;
+
+ private LimitRecordIterator(RecordIterator<T> iterator) {
+ this.iterator = iterator;
+ }
+
+ @Override
+ @Nullable
+ public T next() throws IOException {
+ if (recordCount.get() >= limit) {
+ return null;
+ }
+ T next = iterator.next();
+ if (next != null) {
+ recordCount.incrementAndGet();
+ }
+ return next;
+ }
+
+ @Override
+ public void releaseBatch() {
+ iterator.releaseBatch();
+ }
+ }
+}
diff --git
a/paimon-core/src/main/java/org/apache/paimon/table/format/FormatTableRead.java
b/paimon-core/src/main/java/org/apache/paimon/table/format/FormatTableRead.java
index 765cfd2c9a..9939b1b750 100644
---
a/paimon-core/src/main/java/org/apache/paimon/table/format/FormatTableRead.java
+++
b/paimon-core/src/main/java/org/apache/paimon/table/format/FormatTableRead.java
@@ -23,6 +23,7 @@ import org.apache.paimon.disk.IOManager;
import org.apache.paimon.metrics.MetricRegistry;
import org.apache.paimon.predicate.Predicate;
import org.apache.paimon.predicate.PredicateProjectionConverter;
+import org.apache.paimon.reader.LimitRecordReader;
import org.apache.paimon.reader.RecordReader;
import org.apache.paimon.table.FormatTable;
import org.apache.paimon.table.source.Split;
@@ -31,7 +32,6 @@ import org.apache.paimon.types.RowType;
import java.io.IOException;
import java.util.Optional;
-import java.util.concurrent.atomic.AtomicLong;
/** A {@link TableRead} implementation for {@link FormatTable}. */
public class FormatTableRead implements TableRead {
@@ -80,11 +80,7 @@ public class FormatTableRead implements TableRead {
if (executeFilter) {
reader = executeFilter(reader);
}
- if (limit != null && limit > 0) {
- reader = applyLimit(reader, limit);
- }
-
- return reader;
+ return LimitRecordReader.limit(reader, limit);
}
private RecordReader<InternalRow> executeFilter(RecordReader<InternalRow>
reader) {
@@ -106,44 +102,4 @@ public class FormatTableRead implements TableRead {
Predicate finalFilter = predicate;
return reader.filter(finalFilter::test);
}
-
- private RecordReader<InternalRow> applyLimit(RecordReader<InternalRow>
reader, int limit) {
- return new RecordReader<InternalRow>() {
- private final AtomicLong recordCount = new AtomicLong(0);
-
- @Override
- public RecordIterator<InternalRow> readBatch() throws IOException {
- if (recordCount.get() >= limit) {
- return null;
- }
- RecordIterator<InternalRow> iterator = reader.readBatch();
- if (iterator == null) {
- return null;
- }
- return new RecordIterator<InternalRow>() {
- @Override
- public InternalRow next() throws IOException {
- if (recordCount.get() >= limit) {
- return null;
- }
- InternalRow next = iterator.next();
- if (next != null) {
- recordCount.incrementAndGet();
- }
- return next;
- }
-
- @Override
- public void releaseBatch() {
- iterator.releaseBatch();
- }
- };
- }
-
- @Override
- public void close() throws IOException {
- reader.close();
- }
- };
- }
}
diff --git
a/paimon-core/src/main/java/org/apache/paimon/table/source/KeyValueTableRead.java
b/paimon-core/src/main/java/org/apache/paimon/table/source/KeyValueTableRead.java
index 48b08df0c1..0b19df5c0c 100644
---
a/paimon-core/src/main/java/org/apache/paimon/table/source/KeyValueTableRead.java
+++
b/paimon-core/src/main/java/org/apache/paimon/table/source/KeyValueTableRead.java
@@ -28,6 +28,7 @@ import org.apache.paimon.operation.RawFileSplitRead;
import org.apache.paimon.operation.SplitRead;
import org.apache.paimon.predicate.Predicate;
import org.apache.paimon.predicate.TopN;
+import org.apache.paimon.reader.LimitRecordReader;
import org.apache.paimon.reader.RecordReader;
import org.apache.paimon.schema.TableSchema;
import
org.apache.paimon.table.source.splitread.IncrementalChangelogReadProvider;
@@ -96,9 +97,6 @@ public final class KeyValueTableRead extends
AbstractDataTableRead {
if (topN != null) {
read = read.withTopN(topN);
}
- if (limit != null) {
- read = read.withLimit(limit);
- }
read.withFilter(predicate).withIOManager(ioManager);
}
@@ -131,11 +129,20 @@ public final class KeyValueTableRead extends
AbstractDataTableRead {
@Override
public InnerTableRead withLimit(int limit) {
- initialized().forEach(r -> r.withLimit(limit));
this.limit = limit;
return this;
}
+ @Override
+ public RecordReader<InternalRow> createReader(List<Split> splits) throws
IOException {
+ return LimitRecordReader.limit(super.createReader(splits), limit);
+ }
+
+ @Override
+ public RecordReader<InternalRow> createReader(Split split) throws
IOException {
+ return LimitRecordReader.limit(super.createReader(split), limit);
+ }
+
@Override
public TableRead withIOManager(IOManager ioManager) {
initialized().forEach(r -> r.withIOManager(ioManager));
diff --git
a/paimon-core/src/test/java/org/apache/paimon/table/PrimaryKeySimpleTableTest.java
b/paimon-core/src/test/java/org/apache/paimon/table/PrimaryKeySimpleTableTest.java
index 245eb6ccf0..dd3a21b86c 100644
---
a/paimon-core/src/test/java/org/apache/paimon/table/PrimaryKeySimpleTableTest.java
+++
b/paimon-core/src/test/java/org/apache/paimon/table/PrimaryKeySimpleTableTest.java
@@ -22,6 +22,7 @@ import org.apache.paimon.CoreOptions;
import org.apache.paimon.CoreOptions.ChangelogProducer;
import org.apache.paimon.KeyValue;
import org.apache.paimon.Snapshot;
+import org.apache.paimon.catalog.TableQueryAuthResult;
import org.apache.paimon.data.BinaryString;
import org.apache.paimon.data.GenericRow;
import org.apache.paimon.data.InternalRow;
@@ -64,6 +65,7 @@ import org.apache.paimon.table.sink.TableWriteImpl;
import org.apache.paimon.table.sink.WriteSelector;
import org.apache.paimon.table.source.DataSplit;
import org.apache.paimon.table.source.InnerTableRead;
+import org.apache.paimon.table.source.QueryAuthSplit;
import org.apache.paimon.table.source.ReadBuilder;
import org.apache.paimon.table.source.ScanMode;
import org.apache.paimon.table.source.Split;
@@ -80,6 +82,7 @@ import org.apache.paimon.types.DataTypes;
import org.apache.paimon.types.RowKind;
import org.apache.paimon.types.RowType;
import org.apache.paimon.utils.ChangelogManager;
+import org.apache.paimon.utils.JsonSerdeUtil;
import org.apache.paimon.utils.Pair;
import org.apache.paimon.utils.RoaringBitmap32;
@@ -3365,4 +3368,230 @@ public class PrimaryKeySimpleTableTest extends
SimpleTableTestBase {
write.close();
commit.close();
}
+
+ @Test
+ public void testReadWithLimitOnEmptyPlan() throws Exception {
+ FileStoreTable table = createFileStoreTable();
+ TableScan.Plan plan = table.newScan().plan();
+ assertThat(plan.splits()).isEmpty();
+
+ RecordReader<InternalRow> reader =
+ table.newRead().withLimit(10).createReader(plan.splits());
+ AtomicInteger cnt = new AtomicInteger(0);
+ reader.forEachRemaining(row -> cnt.incrementAndGet());
+ assertThat(cnt.get()).isEqualTo(0);
+ reader.close();
+ }
+
+ @Test
+ public void testReadWithLimitThroughTableReadPath() throws Exception {
+ FileStoreTable table = createFileStoreTable();
+ StreamTableWrite write = table.newWrite(commitUser);
+ StreamTableCommit commit = table.newCommit(commitUser);
+
+ // Write multiple times to create overlapping level-0 files in the
same bucket.
+ int numRecords = 100;
+ for (int i = 0; i < numRecords; i++) {
+ write.write(rowData(1, i, (long) i));
+ }
+ commit.commit(0, write.prepareCommit(true, 0));
+
+ for (int i = 0; i < numRecords; i++) {
+ write.write(rowData(1, i, (long) (i + 1)));
+ }
+ commit.commit(1, write.prepareCommit(true, 1));
+ write.close();
+ commit.close();
+
+ // Verify that withLimit takes effect through the table read path.
+ int limit = 10;
+ TableScan.Plan plan = table.newScan().plan();
+ RecordReader<InternalRow> reader =
+ table.newRead().withLimit(limit).createReader(plan.splits());
+ AtomicInteger cnt = new AtomicInteger(0);
+ reader.forEachRemaining(row -> cnt.incrementAndGet());
+ assertThat(cnt.get()).isEqualTo(limit);
+ reader.close();
+ }
+
+ @Test
+ public void testReadWithLimitThroughTableReadPathMultiSplit() throws
Exception {
+ FileStoreTable table = createFileStoreTable(conf -> conf.set(BUCKET,
4));
+ StreamTableWrite write = table.newWrite(commitUser);
+ StreamTableCommit commit = table.newCommit(commitUser);
+
+ int numRecords = 50;
+ for (int i = 0; i < numRecords; i++) {
+ write.write(rowData(1, i, (long) i));
+ }
+ commit.commit(0, write.prepareCommit(true, 0));
+
+ for (int i = 0; i < numRecords; i++) {
+ write.write(rowData(1, i, (long) (i + 1)));
+ }
+ commit.commit(1, write.prepareCommit(true, 1));
+ write.close();
+ commit.close();
+
+ TableScan.Plan plan = table.newScan().plan();
+ assertThat(plan.splits().size()).isGreaterThan(1);
+
+ int limit = 10;
+ RecordReader<InternalRow> reader =
+ table.newRead().withLimit(limit).createReader(plan.splits());
+ AtomicInteger cnt = new AtomicInteger(0);
+ reader.forEachRemaining(row -> cnt.incrementAndGet());
+ assertThat(cnt.get()).isEqualTo(limit);
+ reader.close();
+ }
+
+ @Test
+ public void testReadWithLimitThroughTableReadPathWithNonPrimaryKeyFilter()
throws Exception {
+ FileStoreTable table = createFileStoreTable();
+ StreamTableWrite write = table.newWrite(commitUser);
+ StreamTableCommit commit = table.newCommit(commitUser);
+
+ int numRecords = 100;
+ for (int i = 0; i < numRecords; i++) {
+ write.write(rowData(1, i, (long) i));
+ }
+ commit.commit(0, write.prepareCommit(true, 0));
+
+ for (int i = 0; i < numRecords; i++) {
+ write.write(rowData(1, i, (long) (i + 1)));
+ }
+ commit.commit(1, write.prepareCommit(true, 1));
+ write.close();
+ commit.close();
+
+ int limit = 10;
+ Predicate nonPrimaryKeyFilter = new
PredicateBuilder(ROW_TYPE).isNotNull(2);
+ TableScan.Plan plan =
table.newScan().withFilter(nonPrimaryKeyFilter).plan();
+ assertThat(plan.splits()).hasSize(1);
+
+ RecordReader<InternalRow> reader =
+ table.newRead()
+ .withFilter(nonPrimaryKeyFilter)
+ .withLimit(limit)
+ .createReader(plan.splits());
+ AtomicInteger cnt = new AtomicInteger(0);
+ reader.forEachRemaining(row -> cnt.incrementAndGet());
+ assertThat(cnt.get()).isEqualTo(limit);
+ reader.close();
+ }
+
+ @Test
+ public void testReadWithLimitThroughTableReadPathWithQueryAuthFilter()
throws Exception {
+ FileStoreTable table = createFileStoreTable();
+ writeOverlappingL0Records(table, 100);
+
+ int limit = 10;
+ Predicate authFilter = new
PredicateBuilder(ROW_TYPE).greaterOrEqual(1, 90);
+ TableQueryAuthResult authResult = authResultWithFilter(authFilter);
+
+ TableScan.Plan plan = table.newScan().plan();
+ assertThat(plan.splits()).hasSize(1);
+ List<Split> authSplits = wrapWithAuth(plan.splits(), authResult);
+
+ RecordReader<InternalRow> reader =
+ table.newRead().withLimit(limit).createReader(authSplits);
+ AtomicInteger cnt = new AtomicInteger(0);
+ reader.forEachRemaining(row -> cnt.incrementAndGet());
+ assertThat(cnt.get()).isEqualTo(limit);
+ reader.close();
+ }
+
+ @Test
+ public void
testReadWithLimitThroughSingleSplitCreateReaderWithQueryAuthFilter()
+ throws Exception {
+ FileStoreTable table = createFileStoreTable();
+ writeOverlappingL0Records(table, 100);
+
+ int limit = 10;
+ Predicate authFilter = new
PredicateBuilder(ROW_TYPE).greaterOrEqual(1, 50);
+ TableQueryAuthResult authResult = authResultWithFilter(authFilter);
+
+ TableScan.Plan plan = table.newScan().plan();
+ assertThat(plan.splits()).hasSize(1);
+ Split authSplit = wrapWithAuth(plan.splits(), authResult).get(0);
+
+ RecordReader<InternalRow> reader =
table.newRead().withLimit(limit).createReader(authSplit);
+ AtomicInteger cnt = new AtomicInteger(0);
+ reader.forEachRemaining(row -> cnt.incrementAndGet());
+ assertThat(cnt.get()).isEqualTo(limit);
+ reader.close();
+ }
+
+ @Test
+ public void
testReadWithLimitThroughSingleSplitCreateReaderWithNonPrimaryKeyFilter()
+ throws Exception {
+ FileStoreTable table = createFileStoreTable();
+ writeOverlappingL0Records(table, 100);
+
+ int limit = 10;
+ Predicate nonPrimaryKeyFilter = new
PredicateBuilder(ROW_TYPE).isNotNull(2);
+ TableScan.Plan plan =
table.newScan().withFilter(nonPrimaryKeyFilter).plan();
+ assertThat(plan.splits()).hasSize(1);
+ Split split = plan.splits().get(0);
+
+ RecordReader<InternalRow> reader =
+ table.newRead()
+ .withFilter(nonPrimaryKeyFilter)
+ .withLimit(limit)
+ .createReader(split);
+ AtomicInteger cnt = new AtomicInteger(0);
+ reader.forEachRemaining(row -> cnt.incrementAndGet());
+ assertThat(cnt.get()).isEqualTo(limit);
+ reader.close();
+ }
+
+ @Test
+ public void
testReadWithLimitThroughTableReadPathMultiSplitWithQueryAuthFilter()
+ throws Exception {
+ FileStoreTable table = createFileStoreTable(conf -> conf.set(BUCKET,
4));
+ writeOverlappingL0Records(table, 50);
+
+ int limit = 10;
+ Predicate authFilter = new
PredicateBuilder(ROW_TYPE).greaterOrEqual(1, 40);
+ TableQueryAuthResult authResult = authResultWithFilter(authFilter);
+
+ TableScan.Plan plan = table.newScan().plan();
+ assertThat(plan.splits().size()).isGreaterThan(1);
+ List<Split> authSplits = wrapWithAuth(plan.splits(), authResult);
+
+ RecordReader<InternalRow> reader =
+ table.newRead().withLimit(limit).createReader(authSplits);
+ AtomicInteger cnt = new AtomicInteger(0);
+ reader.forEachRemaining(row -> cnt.incrementAndGet());
+ assertThat(cnt.get()).isEqualTo(limit);
+ reader.close();
+ }
+
+ private void writeOverlappingL0Records(FileStoreTable table, int
numRecords) throws Exception {
+ StreamTableWrite write = table.newWrite(commitUser);
+ StreamTableCommit commit = table.newCommit(commitUser);
+
+ for (int i = 0; i < numRecords; i++) {
+ write.write(rowData(1, i, (long) i));
+ }
+ commit.commit(0, write.prepareCommit(true, 0));
+
+ for (int i = 0; i < numRecords; i++) {
+ write.write(rowData(1, i, (long) (i + 1)));
+ }
+ commit.commit(1, write.prepareCommit(true, 1));
+ write.close();
+ commit.close();
+ }
+
+ private TableQueryAuthResult authResultWithFilter(Predicate predicate) {
+ return new TableQueryAuthResult(
+
Collections.singletonList(JsonSerdeUtil.toFlatJson(predicate)), null);
+ }
+
+ private List<Split> wrapWithAuth(List<Split> splits, TableQueryAuthResult
authResult) {
+ return splits.stream()
+ .map(split -> new QueryAuthSplit(split, authResult))
+ .collect(Collectors.toList());
+ }
}
diff --git
a/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/KeyValueTableReadLimitITCase.java
b/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/KeyValueTableReadLimitITCase.java
new file mode 100644
index 0000000000..6428394c67
--- /dev/null
+++
b/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/KeyValueTableReadLimitITCase.java
@@ -0,0 +1,110 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.paimon.flink;
+
+import org.apache.flink.types.Row;
+import org.junit.jupiter.api.Test;
+
+import javax.annotation.Nullable;
+
+import java.util.List;
+import java.util.stream.IntStream;
+
+import static java.util.Collections.singletonList;
+import static org.assertj.core.api.Assertions.assertThat;
+
+/** Batch IT for TableRead LIMIT on primary key tables with multiple level-0
files. */
+public class KeyValueTableReadLimitITCase extends CatalogITCaseBase {
+
+ private static final int TOTAL_ROWS = 100;
+ private static final int COMMITS = 5;
+ private static final int LIMIT = 10;
+
+ @Override
+ protected List<String> ddl() {
+ return singletonList(
+ "CREATE TABLE IF NOT EXISTS pk_l0_limit ("
+ + "id INT, v INT, PRIMARY KEY (id) NOT ENFORCED) "
+ + "WITH ("
+ + "'bucket' = '1', "
+ + "'write-only' = 'true', "
+ + "'changelog-producer' = 'none', "
+ + "'merge-engine' = 'deduplicate')");
+ }
+
+ @Nullable
+ @Override
+ protected Boolean sqlSyncMode() {
+ return true;
+ }
+
+ @Test
+ public void testLimitOnMultiLevel0PrimaryKeyTable() {
+ writeMultipleCommits();
+
+ long level0FileCount =
+ batchSql("SELECT file_path FROM `pk_l0_limit$files` WHERE
level = 0").size();
+ assertThat(level0FileCount)
+ .as("Need multiple level-0 files to exercise merge read path")
+ .isGreaterThan(1);
+
+ List<Row> limited =
+ batchSql(
+ String.format(
+ "SELECT * FROM pk_l0_limit /*+
OPTIONS('scan.infer-parallelism'='false', 'scan.parallelism'='1') */ LIMIT %d",
+ LIMIT));
+ assertThat(limited).hasSize(LIMIT);
+
+ List<Row> all = batchSql("SELECT * FROM pk_l0_limit");
+ assertThat(all).hasSize(TOTAL_ROWS);
+ }
+
+ @Test
+ public void testLimitWithValueFilterReturnsCorrectRows() {
+ writeMultipleCommits();
+
+ List<Row> filtered =
+ batchSql(
+ String.format(
+ "SELECT * FROM pk_l0_limit /*+
OPTIONS('scan.infer-parallelism'='false', 'scan.parallelism'='1') */ "
+ + "WHERE v > %d LIMIT %d",
+ TOTAL_ROWS / 2, LIMIT));
+ assertThat(filtered).hasSize(LIMIT);
+ for (Row row : filtered) {
+ assertThat((int) row.getField(1)).isGreaterThan(TOTAL_ROWS / 2);
+ }
+ }
+
+ private void writeMultipleCommits() {
+ int rowsPerCommit = TOTAL_ROWS / COMMITS;
+ IntStream.range(0, COMMITS)
+ .forEach(
+ commit -> {
+ StringBuilder values = new StringBuilder();
+ for (int i = 0; i < rowsPerCommit; i++) {
+ int id = commit * rowsPerCommit + i;
+ if (i > 0) {
+ values.append(", ");
+ }
+ values.append(String.format("(%d, %d)", id,
id));
+ }
+ batchSql("INSERT INTO pk_l0_limit VALUES " +
values);
+ });
+ }
+}