From bbf0694023adce62046964df981397cfa7e175aa Mon Sep 17 00:00:00 2001 From: Sergei Pustovykh Date: Tue, 12 May 2026 11:29:31 +0100 Subject: [PATCH 01/12] Prepare statements to RecordLayerSchemaTemplate and RecordMetaData --- .../foundationdb/record/RecordMetaData.java | 11 ++++++ .../record/RecordMetaDataBuilder.java | 15 +++++++- .../src/main/proto/record_metadata.proto | 1 + .../metadata/RecordLayerSchemaTemplate.java | 35 +++++++++++++++++-- .../serde/RecordMetadataDeserializer.java | 1 + .../serde/RecordMetadataSerializer.java | 4 +++ 6 files changed, 63 insertions(+), 4 deletions(-) diff --git a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/RecordMetaData.java b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/RecordMetaData.java index 0f62f05e4f..1b48764cbd 100644 --- a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/RecordMetaData.java +++ b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/RecordMetaData.java @@ -87,6 +87,8 @@ public class RecordMetaData implements RecordMetaDataProvider { @Nonnull private final Map viewMap; @Nonnull + private final List prepareStatements; + @Nonnull private final Map indexes; @Nonnull private final Map universalIndexes; @@ -118,6 +120,7 @@ protected RecordMetaData(@Nonnull RecordMetaData orig) { Collections.unmodifiableList(orig.formerIndexes), Collections.unmodifiableMap(orig.userDefinedFunctionMap), Collections.unmodifiableMap(orig.viewMap), + Collections.unmodifiableList(orig.prepareStatements), orig.splitLongRecords, orig.storeRecordVersions, orig.version, @@ -139,6 +142,7 @@ protected RecordMetaData(@Nonnull Descriptors.FileDescriptor recordsDescriptor, @Nonnull List formerIndexes, @Nonnull Map userDefinedFunctionMap, @Nonnull Map viewMap, + @Nonnull List prepareStatements, boolean splitLongRecords, boolean storeRecordVersions, int version, @@ -157,6 +161,7 @@ protected RecordMetaData(@Nonnull Descriptors.FileDescriptor recordsDescriptor, this.formerIndexes = formerIndexes; this.userDefinedFunctionMap = userDefinedFunctionMap; this.viewMap = viewMap; + this.prepareStatements = prepareStatements; this.splitLongRecords = splitLongRecords; this.storeRecordVersions = storeRecordVersions; this.version = version; @@ -704,6 +709,7 @@ public RecordMetaDataProto.MetaData toProto(@Nullable Descriptors.FileDescriptor builder.addAllUserDefinedFunctions(userDefinedFunctionMap.values().stream().map(UserDefinedFunction::toProto).collect(Collectors.toList())); builder.addAllViews(viewMap.values().stream().map(View::toProto).collect(Collectors.toList())); + builder.addAllPrepareStatements(prepareStatements); builder.setSplitLongRecords(splitLongRecords); builder.setStoreRecordVersions(storeRecordVersions); builder.setVersion(version); @@ -728,6 +734,11 @@ public Map getViewMap() { return viewMap; } + @Nonnull + public List getPrepareStatements() { + return prepareStatements; + } + @Nonnull public Type.Record getPlannerType(@Nonnull String recordTypeName) { final RecordType recordType = getRecordType(recordTypeName); diff --git a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/RecordMetaDataBuilder.java b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/RecordMetaDataBuilder.java index 206e73c01e..ebd551afff 100644 --- a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/RecordMetaDataBuilder.java +++ b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/RecordMetaDataBuilder.java @@ -116,6 +116,8 @@ public class RecordMetaDataBuilder implements RecordMetaDataProvider { @Nonnull private final Map viewMap; @Nonnull + private final List prepareStatements; + @Nonnull private final Map indexes; @Nonnull private final Map universalIndexes; @@ -152,6 +154,7 @@ public class RecordMetaDataBuilder implements RecordMetaDataProvider { syntheticRecordTypes = new HashMap<>(); userDefinedFunctionMap = new HashMap<>(); viewMap = new HashMap<>(); + prepareStatements = new ArrayList<>(); } private void processSchemaOptions(boolean processExtensionOptions) { @@ -238,6 +241,7 @@ private void loadProtoExceptRecords(@Nonnull RecordMetaDataProto.MetaData metaDa final View view = View.fromProto(viewProto); viewMap.put(view.getName(), view); } + prepareStatements.addAll(metaDataProto.getPrepareStatementsList()); if (metaDataProto.hasSplitLongRecords()) { splitLongRecords = metaDataProto.getSplitLongRecords(); } @@ -1215,6 +1219,15 @@ public void addView(@Nonnull View view) { viewMap.put(view.getName(), view); } + @Nonnull + public List getPrepareStatements() { + return prepareStatements; + } + + public void addPrepareStatement(@Nonnull String prepareStatement) { + prepareStatements.add(prepareStatement); + } + public boolean isSplitLongRecords() { return splitLongRecords; } @@ -1456,7 +1469,7 @@ public RecordMetaData build(boolean validate) { Map> recordTypeKeyToSyntheticRecordTypeMap = Maps.newHashMapWithExpectedSize(syntheticRecordTypes.size()); RecordMetaData metaData = new RecordMetaData(recordsDescriptor, getUnionDescriptor(), unionFields, builtRecordTypes, builtSyntheticRecordTypes, recordTypeKeyToSyntheticRecordTypeMap, - indexes, universalIndexes, formerIndexes, userDefinedFunctionMap, viewMap, + indexes, universalIndexes, formerIndexes, userDefinedFunctionMap, viewMap, prepareStatements, splitLongRecords, storeRecordVersions, version, subspaceKeyCounter, usesSubspaceKeyCounter, recordCountKey, localFileDescriptor != null); for (RecordTypeBuilder recordTypeBuilder : recordTypes.values()) { KeyExpression primaryKey = recordTypeBuilder.getPrimaryKey(); diff --git a/fdb-record-layer-core/src/main/proto/record_metadata.proto b/fdb-record-layer-core/src/main/proto/record_metadata.proto index 303dc6d3d8..51d2933fdd 100644 --- a/fdb-record-layer-core/src/main/proto/record_metadata.proto +++ b/fdb-record-layer-core/src/main/proto/record_metadata.proto @@ -208,6 +208,7 @@ message MetaData { repeated UnnestedRecordType unnested_record_types = 13; repeated PUserDefinedFunction user_defined_functions = 14; repeated PView views = 15; + repeated string prepare_statements = 16; extensions 1000 to 2000; } diff --git a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/metadata/RecordLayerSchemaTemplate.java b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/metadata/RecordLayerSchemaTemplate.java index e9c6d72b0e..88937e2e8b 100644 --- a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/metadata/RecordLayerSchemaTemplate.java +++ b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/metadata/RecordLayerSchemaTemplate.java @@ -80,6 +80,9 @@ public final class RecordLayerSchemaTemplate implements SchemaTemplate { @Nonnull private final Set views; + @Nonnull + private final List prepareStatements; + private final int version; private final boolean enableLongRows; @@ -107,6 +110,7 @@ private RecordLayerSchemaTemplate(@Nonnull final String name, @Nonnull final Set tables, @Nonnull final Set invokedRoutines, @Nonnull final Set views, + @Nonnull final List prepareStatements, int version, boolean enableLongRows, boolean storeRowVersions, @@ -115,6 +119,7 @@ private RecordLayerSchemaTemplate(@Nonnull final String name, this.tables = ImmutableSet.copyOf(tables); this.invokedRoutines = ImmutableSet.copyOf(invokedRoutines); this.views = ImmutableSet.copyOf(views); + this.prepareStatements = ImmutableList.copyOf(prepareStatements); this.version = version; this.enableLongRows = enableLongRows; this.storeRowVersions = storeRowVersions; @@ -130,6 +135,7 @@ private RecordLayerSchemaTemplate(@Nonnull final String name, @Nonnull final Set tables, @Nonnull final Set invokedRoutines, @Nonnull final Set views, + @Nonnull final List prepareStatements, int version, boolean enableLongRows, boolean storeRowVersions, @@ -140,6 +146,7 @@ private RecordLayerSchemaTemplate(@Nonnull final String name, this.tables = ImmutableSet.copyOf(tables); this.invokedRoutines = ImmutableSet.copyOf(invokedRoutines); this.views = ImmutableSet.copyOf(views); + this.prepareStatements = ImmutableList.copyOf(prepareStatements); this.enableLongRows = enableLongRows; this.storeRowVersions = storeRowVersions; this.intermingleTables = intermingleTables; @@ -338,6 +345,11 @@ public Set getViews() { return views; } + @Nonnull + public List getPrepareStatements() { + return prepareStatements; + } + @Nonnull @Override public Optional findViewByName(@Nonnull final String viewName) { @@ -414,6 +426,9 @@ public static final class Builder { @Nonnull private final Map views; + @Nonnull + private final List prepareStatements; + private RecordMetaData cachedMetadata; @@ -422,6 +437,7 @@ private Builder() { auxiliaryTypes = new LinkedHashMap<>(); invokedRoutines = new LinkedHashMap<>(); views = new LinkedHashMap<>(); + prepareStatements = new ArrayList<>(); // enable long rows is TRUE by default enableLongRows = true; } @@ -540,6 +556,18 @@ public Builder addViews(@Nonnull final Collection views) { return this; } + @Nonnull + public Builder addPrepareStatement(@Nonnull final String prepareStatement) { + prepareStatements.add(prepareStatement); + return this; + } + + @Nonnull + public Builder addPrepareStatements(@Nonnull final Collection prepareStatements) { + this.prepareStatements.addAll(prepareStatements); + return this; + } + /** * Adds an auxiliary type, an auxiliary type is a type that is merely created, so it can be referenced later on * in a table definition. Any {@link DataType.Named} data type can be added as an auxiliary type such as {@code enum}s @@ -632,10 +660,10 @@ public RecordLayerSchemaTemplate build() { if (cachedMetadata != null) { return new RecordLayerSchemaTemplate(name, new LinkedHashSet<>(tables.values()), - new LinkedHashSet<>(invokedRoutines.values()), new LinkedHashSet<>(views.values()), version, enableLongRows, storeRowVersions, intermingleTables, cachedMetadata); + new LinkedHashSet<>(invokedRoutines.values()), new LinkedHashSet<>(views.values()), prepareStatements, version, enableLongRows, storeRowVersions, intermingleTables, cachedMetadata); } else { return new RecordLayerSchemaTemplate(name, new LinkedHashSet<>(tables.values()), - new LinkedHashSet<>(invokedRoutines.values()), new LinkedHashSet<>(views.values()), version, enableLongRows, storeRowVersions, intermingleTables); + new LinkedHashSet<>(invokedRoutines.values()), new LinkedHashSet<>(views.values()), prepareStatements, version, enableLongRows, storeRowVersions, intermingleTables); } } @@ -763,6 +791,7 @@ public Builder toBuilder() { .setIntermingleTables(intermingleTables) .addTables(getTables()) .addInvokedRoutines(getInvokedRoutines()) - .addViews(getViews()); + .addViews(getViews()) + .addPrepareStatements(getPrepareStatements()); } } diff --git a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/metadata/serde/RecordMetadataDeserializer.java b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/metadata/serde/RecordMetadataDeserializer.java index 68b4c15bdc..c563c6ced0 100644 --- a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/metadata/serde/RecordMetadataDeserializer.java +++ b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/metadata/serde/RecordMetadataDeserializer.java @@ -125,6 +125,7 @@ private static RecordLayerSchemaTemplate.Builder deserializeRecordMetaData(@Nonn schemaTemplateBuilder.addView(generateViewBuilder(metadataProvider, view.getKey(), view.getValue().getDefinition()).build()); } } + schemaTemplateBuilder.addPrepareStatements(recordMetaData.getPrepareStatements()); schemaTemplateBuilder.setCachedMetadata(recordMetaData); return schemaTemplateBuilder; } diff --git a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/metadata/serde/RecordMetadataSerializer.java b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/metadata/serde/RecordMetadataSerializer.java index 11b80a071c..43b605747d 100644 --- a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/metadata/serde/RecordMetadataSerializer.java +++ b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/metadata/serde/RecordMetadataSerializer.java @@ -107,9 +107,13 @@ public void visit(@Nonnull final View view) { @Override public void visit(@Nonnull SchemaTemplate schemaTemplate) { Assert.thatUnchecked(schemaTemplate instanceof RecordLayerSchemaTemplate); + final var recLayerSchemaTemplate = (RecordLayerSchemaTemplate) schemaTemplate; getBuilder().setSplitLongRecords(schemaTemplate.isEnableLongRows()); getBuilder().setStoreRecordVersions(schemaTemplate.isStoreRowVersions()); getBuilder().setVersion(schemaTemplate.getVersion()); + for (final var prepareStatement : recLayerSchemaTemplate.getPrepareStatements()) { + getBuilder().addPrepareStatement(prepareStatement); + } } @Nonnull From eee583b816615dbce497b697de227667e668958b Mon Sep 17 00:00:00 2001 From: Sergei Pustovykh Date: Wed, 13 May 2026 14:17:01 +0100 Subject: [PATCH 02/12] Prepare statements parse as DDL --- .../foundationdb/record/RecordMetaData.java | 10 +- .../record/RecordMetaDataBuilder.java | 12 +- .../src/main/proto/record_metadata.proto | 2 +- .../api/metadata/SchemaTemplate.java | 9 ++ .../src/main/antlr/RelationalParser.g4 | 1 + .../metadata/NoOpSchemaTemplate.java | 8 ++ .../metadata/RecordLayerSchemaTemplate.java | 24 ++-- .../serde/RecordMetadataSerializer.java | 4 +- .../query/visitors/DdlVisitor.java | 11 ++ .../query/SchemaTemplatePrepareTest.java | 103 ++++++++++++++++++ .../src/test/java/YamlIntegrationTests.java | 5 + .../src/test/resources/standard-tests.yamsql | 4 + 12 files changed, 167 insertions(+), 26 deletions(-) create mode 100644 fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/query/SchemaTemplatePrepareTest.java diff --git a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/RecordMetaData.java b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/RecordMetaData.java index 1b48764cbd..762338b5ac 100644 --- a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/RecordMetaData.java +++ b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/RecordMetaData.java @@ -87,7 +87,7 @@ public class RecordMetaData implements RecordMetaDataProvider { @Nonnull private final Map viewMap; @Nonnull - private final List prepareStatements; + private final Map prepareStatements; @Nonnull private final Map indexes; @Nonnull @@ -120,7 +120,7 @@ protected RecordMetaData(@Nonnull RecordMetaData orig) { Collections.unmodifiableList(orig.formerIndexes), Collections.unmodifiableMap(orig.userDefinedFunctionMap), Collections.unmodifiableMap(orig.viewMap), - Collections.unmodifiableList(orig.prepareStatements), + Collections.unmodifiableMap(orig.prepareStatements), orig.splitLongRecords, orig.storeRecordVersions, orig.version, @@ -142,7 +142,7 @@ protected RecordMetaData(@Nonnull Descriptors.FileDescriptor recordsDescriptor, @Nonnull List formerIndexes, @Nonnull Map userDefinedFunctionMap, @Nonnull Map viewMap, - @Nonnull List prepareStatements, + @Nonnull Map prepareStatements, boolean splitLongRecords, boolean storeRecordVersions, int version, @@ -709,7 +709,7 @@ public RecordMetaDataProto.MetaData toProto(@Nullable Descriptors.FileDescriptor builder.addAllUserDefinedFunctions(userDefinedFunctionMap.values().stream().map(UserDefinedFunction::toProto).collect(Collectors.toList())); builder.addAllViews(viewMap.values().stream().map(View::toProto).collect(Collectors.toList())); - builder.addAllPrepareStatements(prepareStatements); + builder.putAllPrepareStatements(prepareStatements); builder.setSplitLongRecords(splitLongRecords); builder.setStoreRecordVersions(storeRecordVersions); builder.setVersion(version); @@ -735,7 +735,7 @@ public Map getViewMap() { } @Nonnull - public List getPrepareStatements() { + public Map getPrepareStatements() { return prepareStatements; } diff --git a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/RecordMetaDataBuilder.java b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/RecordMetaDataBuilder.java index ebd551afff..9ed0d0bfb0 100644 --- a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/RecordMetaDataBuilder.java +++ b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/RecordMetaDataBuilder.java @@ -116,7 +116,7 @@ public class RecordMetaDataBuilder implements RecordMetaDataProvider { @Nonnull private final Map viewMap; @Nonnull - private final List prepareStatements; + private final Map prepareStatements; @Nonnull private final Map indexes; @Nonnull @@ -154,7 +154,7 @@ public class RecordMetaDataBuilder implements RecordMetaDataProvider { syntheticRecordTypes = new HashMap<>(); userDefinedFunctionMap = new HashMap<>(); viewMap = new HashMap<>(); - prepareStatements = new ArrayList<>(); + prepareStatements = new HashMap<>(); } private void processSchemaOptions(boolean processExtensionOptions) { @@ -241,7 +241,7 @@ private void loadProtoExceptRecords(@Nonnull RecordMetaDataProto.MetaData metaDa final View view = View.fromProto(viewProto); viewMap.put(view.getName(), view); } - prepareStatements.addAll(metaDataProto.getPrepareStatementsList()); + prepareStatements.putAll(metaDataProto.getPrepareStatementsMap()); if (metaDataProto.hasSplitLongRecords()) { splitLongRecords = metaDataProto.getSplitLongRecords(); } @@ -1220,12 +1220,12 @@ public void addView(@Nonnull View view) { } @Nonnull - public List getPrepareStatements() { + public Map getPrepareStatements() { return prepareStatements; } - public void addPrepareStatement(@Nonnull String prepareStatement) { - prepareStatements.add(prepareStatement); + public void addPrepareStatement(@Nonnull String name, @Nonnull String prepareStatement) { + prepareStatements.put(name, prepareStatement); } public boolean isSplitLongRecords() { diff --git a/fdb-record-layer-core/src/main/proto/record_metadata.proto b/fdb-record-layer-core/src/main/proto/record_metadata.proto index 51d2933fdd..b9b6da2694 100644 --- a/fdb-record-layer-core/src/main/proto/record_metadata.proto +++ b/fdb-record-layer-core/src/main/proto/record_metadata.proto @@ -208,7 +208,7 @@ message MetaData { repeated UnnestedRecordType unnested_record_types = 13; repeated PUserDefinedFunction user_defined_functions = 14; repeated PView views = 15; - repeated string prepare_statements = 16; + map prepare_statements = 16; extensions 1000 to 2000; } diff --git a/fdb-relational-api/src/main/java/com/apple/foundationdb/relational/api/metadata/SchemaTemplate.java b/fdb-relational-api/src/main/java/com/apple/foundationdb/relational/api/metadata/SchemaTemplate.java index 29cd7ab878..0df40f200e 100644 --- a/fdb-relational-api/src/main/java/com/apple/foundationdb/relational/api/metadata/SchemaTemplate.java +++ b/fdb-relational-api/src/main/java/com/apple/foundationdb/relational/api/metadata/SchemaTemplate.java @@ -27,6 +27,7 @@ import javax.annotation.Nonnull; import java.util.BitSet; import java.util.Collection; +import java.util.Map; import java.util.Optional; import java.util.Set; @@ -130,6 +131,14 @@ public interface SchemaTemplate extends Metadata { @Nonnull Collection getTemporaryInvokedRoutines() throws RelationalException; + /** + * Returns the prepare statements defined in this schema template. + * + * @return A map of prepare statement names to their SQL strings. + */ + @Nonnull + Map getPrepareStatements(); + @Nonnull String getTransactionBoundMetadataAsString() throws RelationalException; diff --git a/fdb-relational-core/src/main/antlr/RelationalParser.g4 b/fdb-relational-core/src/main/antlr/RelationalParser.g4 index 604d2c56b9..68576b4728 100644 --- a/fdb-relational-core/src/main/antlr/RelationalParser.g4 +++ b/fdb-relational-core/src/main/antlr/RelationalParser.g4 @@ -92,6 +92,7 @@ utilityStatement templateClause : CREATE ( structDefinition | tableDefinition | enumDefinition | indexDefinition | sqlInvokedFunction | viewDefinition ) + | prepareStatement ; createStatement diff --git a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/metadata/NoOpSchemaTemplate.java b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/metadata/NoOpSchemaTemplate.java index 2a053aabb2..e1de98f4cd 100644 --- a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/metadata/NoOpSchemaTemplate.java +++ b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/metadata/NoOpSchemaTemplate.java @@ -35,6 +35,8 @@ import javax.annotation.Nonnull; import java.util.BitSet; import java.util.Collection; +import java.util.Collections; +import java.util.Map; import java.util.Optional; import java.util.Set; @@ -139,6 +141,12 @@ public Collection getTemporaryInvokedRoutines() throws Relationa throw new RelationalException("NoOpSchemaTemplate doesn't have temporary invoked routines!", ErrorCode.INVALID_PARAMETER); } + @Nonnull + @Override + public Map getPrepareStatements() { + return Collections.emptyMap(); + } + @Nonnull @Override public String getTransactionBoundMetadataAsString() throws RelationalException { diff --git a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/metadata/RecordLayerSchemaTemplate.java b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/metadata/RecordLayerSchemaTemplate.java index 88937e2e8b..25e3d931b3 100644 --- a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/metadata/RecordLayerSchemaTemplate.java +++ b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/metadata/RecordLayerSchemaTemplate.java @@ -81,7 +81,7 @@ public final class RecordLayerSchemaTemplate implements SchemaTemplate { private final Set views; @Nonnull - private final List prepareStatements; + private final Map prepareStatements; private final int version; @@ -110,7 +110,7 @@ private RecordLayerSchemaTemplate(@Nonnull final String name, @Nonnull final Set tables, @Nonnull final Set invokedRoutines, @Nonnull final Set views, - @Nonnull final List prepareStatements, + @Nonnull final Map prepareStatements, int version, boolean enableLongRows, boolean storeRowVersions, @@ -119,7 +119,7 @@ private RecordLayerSchemaTemplate(@Nonnull final String name, this.tables = ImmutableSet.copyOf(tables); this.invokedRoutines = ImmutableSet.copyOf(invokedRoutines); this.views = ImmutableSet.copyOf(views); - this.prepareStatements = ImmutableList.copyOf(prepareStatements); + this.prepareStatements = ImmutableMap.copyOf(prepareStatements); this.version = version; this.enableLongRows = enableLongRows; this.storeRowVersions = storeRowVersions; @@ -135,7 +135,7 @@ private RecordLayerSchemaTemplate(@Nonnull final String name, @Nonnull final Set tables, @Nonnull final Set invokedRoutines, @Nonnull final Set views, - @Nonnull final List prepareStatements, + @Nonnull final Map prepareStatements, int version, boolean enableLongRows, boolean storeRowVersions, @@ -146,7 +146,7 @@ private RecordLayerSchemaTemplate(@Nonnull final String name, this.tables = ImmutableSet.copyOf(tables); this.invokedRoutines = ImmutableSet.copyOf(invokedRoutines); this.views = ImmutableSet.copyOf(views); - this.prepareStatements = ImmutableList.copyOf(prepareStatements); + this.prepareStatements = ImmutableMap.copyOf(prepareStatements); this.enableLongRows = enableLongRows; this.storeRowVersions = storeRowVersions; this.intermingleTables = intermingleTables; @@ -346,7 +346,7 @@ public Set getViews() { } @Nonnull - public List getPrepareStatements() { + public Map getPrepareStatements() { return prepareStatements; } @@ -427,7 +427,7 @@ public static final class Builder { private final Map views; @Nonnull - private final List prepareStatements; + private final Map prepareStatements; private RecordMetaData cachedMetadata; @@ -437,7 +437,7 @@ private Builder() { auxiliaryTypes = new LinkedHashMap<>(); invokedRoutines = new LinkedHashMap<>(); views = new LinkedHashMap<>(); - prepareStatements = new ArrayList<>(); + prepareStatements = new LinkedHashMap<>(); // enable long rows is TRUE by default enableLongRows = true; } @@ -557,14 +557,14 @@ public Builder addViews(@Nonnull final Collection views) { } @Nonnull - public Builder addPrepareStatement(@Nonnull final String prepareStatement) { - prepareStatements.add(prepareStatement); + public Builder addPrepareStatement(@Nonnull final String name, @Nonnull final String prepareStatement) { + prepareStatements.put(name, prepareStatement); return this; } @Nonnull - public Builder addPrepareStatements(@Nonnull final Collection prepareStatements) { - this.prepareStatements.addAll(prepareStatements); + public Builder addPrepareStatements(@Nonnull final Map prepareStatements) { + this.prepareStatements.putAll(prepareStatements); return this; } diff --git a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/metadata/serde/RecordMetadataSerializer.java b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/metadata/serde/RecordMetadataSerializer.java index 43b605747d..63a2c0a4eb 100644 --- a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/metadata/serde/RecordMetadataSerializer.java +++ b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/metadata/serde/RecordMetadataSerializer.java @@ -111,8 +111,8 @@ public void visit(@Nonnull SchemaTemplate schemaTemplate) { getBuilder().setSplitLongRecords(schemaTemplate.isEnableLongRows()); getBuilder().setStoreRecordVersions(schemaTemplate.isStoreRowVersions()); getBuilder().setVersion(schemaTemplate.getVersion()); - for (final var prepareStatement : recLayerSchemaTemplate.getPrepareStatements()) { - getBuilder().addPrepareStatement(prepareStatement); + for (final var entry : recLayerSchemaTemplate.getPrepareStatements().entrySet()) { + getBuilder().addPrepareStatement(entry.getKey(), entry.getValue()); } } diff --git a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/visitors/DdlVisitor.java b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/visitors/DdlVisitor.java index 17483c5c64..2bd4908f3c 100644 --- a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/visitors/DdlVisitor.java +++ b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/visitors/DdlVisitor.java @@ -414,6 +414,17 @@ public ProceduralPlan visitCreateSchemaTemplateStatement(@Nonnull RelationalPars sqlInvokedFunctionClauses.add(templateClause.sqlInvokedFunction()); } else if (templateClause.viewDefinition() != null) { viewClauses.add(templateClause.viewDefinition()); + } else if (templateClause.prepareStatement() != null) { + final var prepareCtx = templateClause.prepareStatement(); + final var name = visitUid(prepareCtx.uid()).getName(); + final String queryString; + if (prepareCtx.queryString != null) { + final var text = prepareCtx.queryString.getText(); + queryString = text.substring(1, text.length() - 1); + } else { + queryString = prepareCtx.variable.getText(); + } + metadataBuilder.addPrepareStatement(name, queryString); } else { Assert.thatUnchecked(templateClause.indexDefinition() != null); indexClauses.add(templateClause.indexDefinition()); diff --git a/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/query/SchemaTemplatePrepareTest.java b/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/query/SchemaTemplatePrepareTest.java new file mode 100644 index 0000000000..a45c98110c --- /dev/null +++ b/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/query/SchemaTemplatePrepareTest.java @@ -0,0 +1,103 @@ +/* + * SchemaTemplatePrepareTest.java + * + * This source file is part of the FoundationDB open source project + * + * Copyright 2021-2026 Apple Inc. and the FoundationDB project authors + * + * Licensed 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 com.apple.foundationdb.relational.recordlayer.query; + +import com.apple.foundationdb.relational.api.RelationalResultSet; +import com.apple.foundationdb.relational.recordlayer.EmbeddedRelationalConnection; +import com.apple.foundationdb.relational.recordlayer.EmbeddedRelationalExtension; +import com.apple.foundationdb.relational.recordlayer.metadata.RecordLayerSchemaTemplate; +import com.apple.foundationdb.relational.utils.Ddl; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import java.net.URI; + +public class SchemaTemplatePrepareTest { + private static final String SCHEMA_TEMPLATE = + "CREATE TABLE t1(id bigint, col1 bigint, col2 bigint, PRIMARY KEY(id))" + + " CREATE INDEX i1 AS SELECT col1 FROM t1" + + " PREPARE by_col1 FROM 'select * from t1 where col1 = 1'" + + " PREPARE by_id FROM 'select * from t1 where id = 1'"; + + @RegisterExtension + @Order(0) + public final EmbeddedRelationalExtension relationalExtension = new EmbeddedRelationalExtension(); + + @Test + void prepareStatementsStoredInTemplate() throws Exception { + try (var ddl = Ddl.builder() + .database(URI.create("/TEST/PREPARE_DB")) + .relationalExtension(relationalExtension) + .schemaTemplate(SCHEMA_TEMPLATE) + .build()) { + final var connection = ddl.setSchemaAndGetConnection(); + final var embeddedConnection = connection.unwrap(EmbeddedRelationalConnection.class); + embeddedConnection.setAutoCommit(false); + embeddedConnection.createNewTransaction(); + final var schemaTemplate = embeddedConnection.getSchemaTemplate().unwrap(RecordLayerSchemaTemplate.class); + embeddedConnection.rollback(); + embeddedConnection.setAutoCommit(true); + final var prepareStatements = schemaTemplate.getPrepareStatements(); + Assertions.assertEquals(2, prepareStatements.size()); + Assertions.assertEquals("select * from t1 where col1 = 1", prepareStatements.get("BY_COL1")); + Assertions.assertEquals("select * from t1 where id = 1", prepareStatements.get("BY_ID")); + } + } + + @Test + void queryWithPrepareStatements() throws Exception { + try (var ddl = Ddl.builder() + .database(URI.create("/TEST/PREPARE_DB2")) + .relationalExtension(relationalExtension) + .schemaTemplate(SCHEMA_TEMPLATE) + .build()) { + final var connection = ddl.setSchemaAndGetConnection(); + + try (var stmt = connection.createStatement()) { + stmt.execute("INSERT INTO T1 VALUES (1, 10, 1), (2, 10, 2), (3, 20, 3), (4, 20, 4), (5, 30, 5)"); + } + + try (var ps = connection.prepareStatement("SELECT * FROM T1 WHERE col1 = ?")) { + ps.setLong(1, 10); + try (RelationalResultSet rs = ps.executeQuery()) { + Assertions.assertTrue(rs.next()); + Assertions.assertEquals(1, rs.getLong("ID")); + Assertions.assertTrue(rs.next()); + Assertions.assertEquals(2, rs.getLong("ID")); + Assertions.assertFalse(rs.next()); + } + } + + try (var ps = connection.prepareStatement("SELECT * FROM T1 WHERE id = ?")) { + ps.setLong(1, 3); + try (RelationalResultSet rs = ps.executeQuery()) { + Assertions.assertTrue(rs.next()); + Assertions.assertEquals(3, rs.getLong("ID")); + Assertions.assertEquals(20, rs.getLong("COL1")); + Assertions.assertEquals(3, rs.getLong("COL2")); + Assertions.assertFalse(rs.next()); + } + } + } + } +} diff --git a/yaml-tests/src/test/java/YamlIntegrationTests.java b/yaml-tests/src/test/java/YamlIntegrationTests.java index 9ae0be9584..fccd2fc1b3 100644 --- a/yaml-tests/src/test/java/YamlIntegrationTests.java +++ b/yaml-tests/src/test/java/YamlIntegrationTests.java @@ -357,6 +357,11 @@ public void standardTests(YamlTest.Runner runner) throws Exception { runner.runYamsql("standard-tests.yamsql"); } + @TestTemplate + public void schemaTemplatePrepare(YamlTest.Runner runner) throws Exception { + runner.runYamsql("schema-template-prepare.yamsql"); + } + @TestTemplate public void standardTestsWithMetaData(YamlTest.Runner runner) throws Exception { runner.runYamsql("standard-tests-metadata.yamsql"); diff --git a/yaml-tests/src/test/resources/standard-tests.yamsql b/yaml-tests/src/test/resources/standard-tests.yamsql index 3e2c395d41..459b731774 100644 --- a/yaml-tests/src/test/resources/standard-tests.yamsql +++ b/yaml-tests/src/test/resources/standard-tests.yamsql @@ -43,6 +43,10 @@ test_block: name: standard-tests tests: - +# - query: select id from T1 where col1=10 + - query: select col2 from T1 where col1=10 + - explain: "COVERING(I1 [EQUALS promote(@c7 AS LONG)] -> [COL1: KEY:[0], ID: KEY:[2]]) | MAP (_.ID AS ID)" + - - query: select id, case when col1 = 10 then 100 when col2 in (6,7,8,9) then 200 else 300 end as NEWCOL From 7fef8b40ac1270d8b7b2fd5b50cc3921b4868751 Mon Sep 17 00:00:00 2001 From: Sergei Pustovykh Date: Thu, 14 May 2026 12:33:32 +0100 Subject: [PATCH 03/12] Prepare statements at first record store open --- .../recordlayer/query/PlanGenerator.java | 31 ++++- .../query/cache/RelationalPlanCache.java | 13 ++ .../query/SchemaTemplatePrepareTest.java | 128 +++++++++++++++--- 3 files changed, 149 insertions(+), 23 deletions(-) diff --git a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/PlanGenerator.java b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/PlanGenerator.java index 6066385e16..7e4397f0aa 100644 --- a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/PlanGenerator.java +++ b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/PlanGenerator.java @@ -140,6 +140,33 @@ public Plan getPlan(@Nonnull final String query) throws RelationalException { return plan; } + /** + * Pre-generates and caches plans for the prepare statements defined in the schema template. + * This method is idempotent per template name and version — subsequent calls for the same + * template are no-ops. + */ + public void prepareStatements() { + if (cache.isEmpty()) { + return; + } + final var schemaTemplate = planContext.getSchemaTemplate(); + if (schemaTemplate.getPrepareStatements().isEmpty()) { + return; + } + final var templateKey = schemaTemplate.getName() + ":" + schemaTemplate.getVersion(); + if (cache.get().isPrepared(templateKey)) { + return; + } + for (final var entry : schemaTemplate.getPrepareStatements().entrySet()) { + try { + getPlan(entry.getValue()); + } catch (RelationalException e) { + logger.warn("Failed to prepare statement '{}': {}", entry.getKey(), e.getMessage()); + } + } + cache.get().markPrepared(templateKey); + } + private boolean isCaseSensitive() { return options.getOption(Options.Name.CASE_SENSITIVE_IDENTIFIERS); } @@ -480,7 +507,9 @@ public static PlanGenerator create(@Nonnull final Optional @Nonnull final Options options) throws RelationalException { final var planner = new CascadesPlanner(metaData, recordStoreState, matchCandidateRegistry); planner.setConfiguration(planContext.getRecordQueryPlannerConfiguration()); - return new PlanGenerator(cache, planContext, planner, options); + final var planGenerator = new PlanGenerator(cache, planContext, planner, options); + planGenerator.prepareStatements(); + return planGenerator; } /** diff --git a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/cache/RelationalPlanCache.java b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/cache/RelationalPlanCache.java index 13e8192d8a..02f8872c0f 100644 --- a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/cache/RelationalPlanCache.java +++ b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/cache/RelationalPlanCache.java @@ -27,6 +27,8 @@ import javax.annotation.Nonnull; import javax.annotation.Nullable; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Executor; import java.util.concurrent.TimeUnit; @@ -45,6 +47,9 @@ public final class RelationalPlanCache extends MultiStageCache preparedTemplates = ConcurrentHashMap.newKeySet(); + private RelationalPlanCache(int size, int secondarySize, int tertiarySize, @@ -102,4 +107,12 @@ public static RelationalPlanCache buildWithDefaults() { return newRelationalCacheBuilder().build(); } + public boolean isPrepared(@Nonnull String templateKey) { + return preparedTemplates.contains(templateKey); + } + + public void markPrepared(@Nonnull String templateKey) { + preparedTemplates.add(templateKey); + } + } diff --git a/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/query/SchemaTemplatePrepareTest.java b/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/query/SchemaTemplatePrepareTest.java index a45c98110c..3e1c3a6e9b 100644 --- a/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/query/SchemaTemplatePrepareTest.java +++ b/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/query/SchemaTemplatePrepareTest.java @@ -20,10 +20,13 @@ package com.apple.foundationdb.relational.recordlayer.query; +import com.apple.foundationdb.relational.api.RelationalConnection; import com.apple.foundationdb.relational.api.RelationalResultSet; import com.apple.foundationdb.relational.recordlayer.EmbeddedRelationalConnection; import com.apple.foundationdb.relational.recordlayer.EmbeddedRelationalExtension; import com.apple.foundationdb.relational.recordlayer.metadata.RecordLayerSchemaTemplate; +import com.apple.foundationdb.relational.recordlayer.query.cache.QueryCacheKey; +import com.apple.foundationdb.relational.recordlayer.query.cache.RelationalPlanCache; import com.apple.foundationdb.relational.utils.Ddl; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Order; @@ -31,18 +34,51 @@ import org.junit.jupiter.api.extension.RegisterExtension; import java.net.URI; +import java.sql.SQLException; public class SchemaTemplatePrepareTest { + private static final String SCHEMA_TEMPLATE = "CREATE TABLE t1(id bigint, col1 bigint, col2 bigint, PRIMARY KEY(id))" + " CREATE INDEX i1 AS SELECT col1 FROM t1" + - " PREPARE by_col1 FROM 'select * from t1 where col1 = 1'" + + " PREPARE by_col1 FROM 'select * from t1 where col1 = 10'" + " PREPARE by_id FROM 'select * from t1 where id = 1'"; @RegisterExtension @Order(0) public final EmbeddedRelationalExtension relationalExtension = new EmbeddedRelationalExtension(); + private long countCachedPlans(RelationalConnection connection, String templateName) throws SQLException { + final var embeddedConnection = connection.unwrap(EmbeddedRelationalConnection.class); + final RelationalPlanCache cache = embeddedConnection.getRecordLayerDatabase().getPlanCache(); + if (cache == null) { + return 0; + } + final Long count = cache.getStats().numSecondaryEntries(templateName); + return count != null ? count : 0; + } + + private void showCache(RelationalConnection connection) throws SQLException { + final var embeddedConnection = connection.unwrap(EmbeddedRelationalConnection.class); + final RelationalPlanCache cache = embeddedConnection.getRecordLayerDatabase().getPlanCache(); + if (cache == null) { + System.out.println("[CACHE] no plan cache"); + return; + } + for (String key : cache.getStats().getAllKeys()) { + System.out.println("[CACHE] template: " + key); + for (QueryCacheKey secondaryKey : cache.getStats().getAllSecondaryKeys(key)) { + System.out.println("[CACHE] query: " + secondaryKey.getCanonicalQueryString() + + " (version=" + secondaryKey.getSchemaTemplateVersion() + + ", userVersion=" + secondaryKey.getUserVersion() + ")"); + var tertiaryMappings = cache.getStats().getAllTertiaryMappings(key, secondaryKey); + for (var entry : tertiaryMappings.entrySet()) { + System.out.println("[CACHE] plan: " + entry.getValue().explain()); + } + } + } + } + @Test void prepareStatementsStoredInTemplate() throws Exception { try (var ddl = Ddl.builder() @@ -59,45 +95,93 @@ void prepareStatementsStoredInTemplate() throws Exception { embeddedConnection.setAutoCommit(true); final var prepareStatements = schemaTemplate.getPrepareStatements(); Assertions.assertEquals(2, prepareStatements.size()); - Assertions.assertEquals("select * from t1 where col1 = 1", prepareStatements.get("BY_COL1")); + Assertions.assertEquals("select * from t1 where col1 = 10", prepareStatements.get("BY_COL1")); Assertions.assertEquals("select * from t1 where id = 1", prepareStatements.get("BY_ID")); + Assertions.assertEquals(0, countCachedPlans(connection, ddl.getSchemaTemplateName())); // this is not good } } @Test - void queryWithPrepareStatements() throws Exception { + void prepareStatementsAfterFirstQuery() throws Exception { + try (var ddl = Ddl.builder() + .database(URI.create("/TEST/PREPARE_DB3")) + .relationalExtension(relationalExtension) + .schemaTemplate(SCHEMA_TEMPLATE) + .build()) { + final var connection = ddl.setSchemaAndGetConnection(); + Assertions.assertEquals(0, countCachedPlans(connection, ddl.getSchemaTemplateName())); + + try (var stmt = connection.createStatement()) { + stmt.execute("INSERT INTO T1 VALUES (1, 10, 1)"); + } + Assertions.assertEquals(2, countCachedPlans(connection, ddl.getSchemaTemplateName())); + } + } + + @Test + void prepareStatementsUsage() throws Exception { try (var ddl = Ddl.builder() .database(URI.create("/TEST/PREPARE_DB2")) .relationalExtension(relationalExtension) .schemaTemplate(SCHEMA_TEMPLATE) .build()) { final var connection = ddl.setSchemaAndGetConnection(); + Assertions.assertEquals(0, countCachedPlans(connection, ddl.getSchemaTemplateName())); try (var stmt = connection.createStatement()) { - stmt.execute("INSERT INTO T1 VALUES (1, 10, 1), (2, 10, 2), (3, 20, 3), (4, 20, 4), (5, 30, 5)"); + stmt.execute("INSERT INTO T1 VALUES (1, 10, 1)"); } + Assertions.assertEquals(2, countCachedPlans(connection, ddl.getSchemaTemplateName())); - try (var ps = connection.prepareStatement("SELECT * FROM T1 WHERE col1 = ?")) { - ps.setLong(1, 10); - try (RelationalResultSet rs = ps.executeQuery()) { - Assertions.assertTrue(rs.next()); - Assertions.assertEquals(1, rs.getLong("ID")); - Assertions.assertTrue(rs.next()); - Assertions.assertEquals(2, rs.getLong("ID")); - Assertions.assertFalse(rs.next()); - } + try (var stmt = connection.createStatement(); RelationalResultSet rs = stmt.executeQuery("select * from t1 where col1 = 10")) { + Assertions.assertTrue(rs.next()); + Assertions.assertEquals(1, rs.getLong("ID")); + Assertions.assertFalse(rs.next()); } + Assertions.assertEquals(2, countCachedPlans(connection, ddl.getSchemaTemplateName())); - try (var ps = connection.prepareStatement("SELECT * FROM T1 WHERE id = ?")) { - ps.setLong(1, 3); - try (RelationalResultSet rs = ps.executeQuery()) { - Assertions.assertTrue(rs.next()); - Assertions.assertEquals(3, rs.getLong("ID")); - Assertions.assertEquals(20, rs.getLong("COL1")); - Assertions.assertEquals(3, rs.getLong("COL2")); - Assertions.assertFalse(rs.next()); - } + try (var stmt = connection.createStatement(); RelationalResultSet rs = stmt.executeQuery("select * from t1 where id = 1")) { + Assertions.assertTrue(rs.next()); + Assertions.assertEquals(1, rs.getLong("ID")); + Assertions.assertEquals(10, rs.getLong("COL1")); + Assertions.assertEquals(1, rs.getLong("COL2")); + Assertions.assertFalse(rs.next()); + } + Assertions.assertEquals(2, countCachedPlans(connection, ddl.getSchemaTemplateName())); + } + } + + @Test + void prepareStatementsUsageParams() throws Exception { + try (var ddl = Ddl.builder() + .database(URI.create("/TEST/PREPARE_DB2")) + .relationalExtension(relationalExtension) + .schemaTemplate(SCHEMA_TEMPLATE) + .build()) { + final var connection = ddl.setSchemaAndGetConnection(); + Assertions.assertEquals(0, countCachedPlans(connection, ddl.getSchemaTemplateName())); + + try (var stmt = connection.createStatement()) { + stmt.execute("INSERT INTO T1 VALUES (1, 10, 1)"); + stmt.execute("INSERT INTO T1 VALUES (2, 20, 2)"); + } + Assertions.assertEquals(2, countCachedPlans(connection, ddl.getSchemaTemplateName())); + + try (var stmt = connection.createStatement(); RelationalResultSet rs = stmt.executeQuery("select * from t1 where col1 = 20")) { + Assertions.assertTrue(rs.next()); + Assertions.assertEquals(2, rs.getLong("ID")); + Assertions.assertFalse(rs.next()); + } + Assertions.assertEquals(2, countCachedPlans(connection, ddl.getSchemaTemplateName())); + + try (var stmt = connection.createStatement(); RelationalResultSet rs = stmt.executeQuery("select * from t1 where id = 2")) { + Assertions.assertTrue(rs.next()); + Assertions.assertEquals(2, rs.getLong("ID")); + Assertions.assertEquals(20, rs.getLong("COL1")); + Assertions.assertEquals(2, rs.getLong("COL2")); + Assertions.assertFalse(rs.next()); } + Assertions.assertEquals(2, countCachedPlans(connection, ddl.getSchemaTemplateName())); } } } From 1f5e51e2b49654fa83f227eb25eee6155be0b3e6 Mon Sep 17 00:00:00 2001 From: Sergei Pustovykh Date: Thu, 14 May 2026 13:54:04 +0100 Subject: [PATCH 04/12] fix test validateMetaDataCoverage --- .../provider/foundationdb/MetaDataProtoEditorUnitTest.java | 1 + 1 file changed, 1 insertion(+) diff --git a/fdb-record-layer-core/src/test/java/com/apple/foundationdb/record/provider/foundationdb/MetaDataProtoEditorUnitTest.java b/fdb-record-layer-core/src/test/java/com/apple/foundationdb/record/provider/foundationdb/MetaDataProtoEditorUnitTest.java index 95d4dfb6ff..bb40904566 100644 --- a/fdb-record-layer-core/src/test/java/com/apple/foundationdb/record/provider/foundationdb/MetaDataProtoEditorUnitTest.java +++ b/fdb-record-layer-core/src/test/java/com/apple/foundationdb/record/provider/foundationdb/MetaDataProtoEditorUnitTest.java @@ -587,6 +587,7 @@ void validateMetaDataCoverage() { assertEquals(Set.of( "split_long_records", "version", "former_indexes", "record_count_key", "store_record_versions", "dependencies", "subspace_key_counter", "uses_subspace_key_counter", + "prepare_statements", // the below reference record types "records", "indexes", "record_types", "joined_record_types", "unnested_record_types", "user_defined_functions", "views"), From 9cde93f4f35406c22328d96af532e151d31dab1f Mon Sep 17 00:00:00 2001 From: Sergei Pustovykh Date: Thu, 14 May 2026 14:12:27 +0100 Subject: [PATCH 05/12] fix style --- .../recordlayer/metadata/RecordLayerSchemaTemplate.java | 1 + .../relational/recordlayer/query/PlanGenerator.java | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/metadata/RecordLayerSchemaTemplate.java b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/metadata/RecordLayerSchemaTemplate.java index 25e3d931b3..3969032863 100644 --- a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/metadata/RecordLayerSchemaTemplate.java +++ b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/metadata/RecordLayerSchemaTemplate.java @@ -346,6 +346,7 @@ public Set getViews() { } @Nonnull + @Override public Map getPrepareStatements() { return prepareStatements; } diff --git a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/PlanGenerator.java b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/PlanGenerator.java index 7e4397f0aa..e4a3746c84 100644 --- a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/PlanGenerator.java +++ b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/PlanGenerator.java @@ -26,6 +26,7 @@ import com.apple.foundationdb.record.RecordMetaData; import com.apple.foundationdb.record.RecordStoreState; import com.apple.foundationdb.record.logging.KeyValueLogMessage; +import com.apple.foundationdb.record.logging.LogMessageKeys; import com.apple.foundationdb.record.metadata.MetaDataException; import com.apple.foundationdb.record.provider.foundationdb.FDBRecordStoreBase; import com.apple.foundationdb.record.provider.foundationdb.IndexMatchCandidateRegistry; @@ -161,7 +162,7 @@ public void prepareStatements() { try { getPlan(entry.getValue()); } catch (RelationalException e) { - logger.warn("Failed to prepare statement '{}': {}", entry.getKey(), e.getMessage()); + logger.error(KeyValueLogMessage.of("prepare statement", LogMessageKeys.QUERY, entry.getValue()), e); } } cache.get().markPrepared(templateKey); From cf30cd3476c21052cd4409680279c19c59e1a81c Mon Sep 17 00:00:00 2001 From: Sergei Pustovykh Date: Fri, 15 May 2026 11:10:04 +0100 Subject: [PATCH 06/12] schema-template-prepare yamsql test --- .../resources/schema-template-prepare.yamsql | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 yaml-tests/src/test/resources/schema-template-prepare.yamsql diff --git a/yaml-tests/src/test/resources/schema-template-prepare.yamsql b/yaml-tests/src/test/resources/schema-template-prepare.yamsql new file mode 100644 index 0000000000..4c8553a995 --- /dev/null +++ b/yaml-tests/src/test/resources/schema-template-prepare.yamsql @@ -0,0 +1,45 @@ +# +# schema-template-prepare.yamsql +# +# This source file is part of the FoundationDB open source project +# +# Copyright 2021-2026 Apple Inc. and the FoundationDB project authors +# +# Licensed 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. + +--- +schema_template: + create table t1(id bigint, col1 bigint, col2 bigint, primary key(id)) + create index i1 as select col1 from t1 + prepare by_col1 from 'select * from t1 where col1 = 1' + prepare by_id from 'select * from t1 where id = 1' +--- +setup: + steps: + - query: INSERT INTO T1 + VALUES (1, 10, 1), + (2, 10, 2), + (3, 20, 3), + (4, 20, 4), + (5, 30, 5) +--- +test_block: + name: schema-template-prepare-tests + tests: + - + - query: select * from T1 where col1 = 10 + - result: [{ID: 1, COL1: 10, COL2: 1}, + {ID: 2, COL1: 10, COL2: 2}] + - + - query: select * from T1 where id = 3 + - result: [{ID: 3, COL1: 20, COL2: 3}] \ No newline at end of file From b80dccbb6705b5ba3d9e5197abd48587a9be2753 Mon Sep 17 00:00:00 2001 From: Sergei Pustovykh Date: Fri, 15 May 2026 11:17:03 +0100 Subject: [PATCH 07/12] logger fix --- .../relational/recordlayer/query/PlanGenerator.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/PlanGenerator.java b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/PlanGenerator.java index e4a3746c84..41bab939e5 100644 --- a/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/PlanGenerator.java +++ b/fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/PlanGenerator.java @@ -162,7 +162,9 @@ public void prepareStatements() { try { getPlan(entry.getValue()); } catch (RelationalException e) { - logger.error(KeyValueLogMessage.of("prepare statement", LogMessageKeys.QUERY, entry.getValue()), e); + if (logger.isErrorEnabled()) { + logger.error(KeyValueLogMessage.of("prepare statement", LogMessageKeys.QUERY, entry.getValue()), e); + } } } cache.get().markPrepared(templateKey); From 550aaf5a38c446378bc93d0d03666760622e8df6 Mon Sep 17 00:00:00 2001 From: Sergei Pustovykh Date: Fri, 15 May 2026 11:23:46 +0100 Subject: [PATCH 08/12] test jdbc prepare --- .../query/SchemaTemplatePrepareTest.java | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/query/SchemaTemplatePrepareTest.java b/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/query/SchemaTemplatePrepareTest.java index 3e1c3a6e9b..61802e44df 100644 --- a/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/query/SchemaTemplatePrepareTest.java +++ b/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/query/SchemaTemplatePrepareTest.java @@ -184,4 +184,43 @@ void prepareStatementsUsageParams() throws Exception { Assertions.assertEquals(2, countCachedPlans(connection, ddl.getSchemaTemplateName())); } } + + @Test + void prepareStatementsUsageJdbcPrepare() throws Exception { + try (var ddl = Ddl.builder() + .database(URI.create("/TEST/PREPARE_DB4")) + .relationalExtension(relationalExtension) + .schemaTemplate(SCHEMA_TEMPLATE) + .build()) { + final var connection = ddl.setSchemaAndGetConnection(); + + try (var stmt = connection.createStatement()) { + stmt.execute("INSERT INTO T1 VALUES (1, 10, 1)"); + stmt.execute("INSERT INTO T1 VALUES (2, 20, 2)"); + } + Assertions.assertEquals(2, countCachedPlans(connection, ddl.getSchemaTemplateName())); + + try (var ps = connection.prepareStatement("select * from t1 where col1 = ?")) { + ps.setLong(1, 20); + try (RelationalResultSet rs = ps.executeQuery()) { + Assertions.assertTrue(rs.next()); + Assertions.assertEquals(2, rs.getLong("ID")); + Assertions.assertFalse(rs.next()); + } + } + Assertions.assertEquals(2, countCachedPlans(connection, ddl.getSchemaTemplateName())); + + try (var ps = connection.prepareStatement("select * from t1 where id = ?")) { + ps.setLong(1, 2); + try (RelationalResultSet rs = ps.executeQuery()) { + Assertions.assertTrue(rs.next()); + Assertions.assertEquals(2, rs.getLong("ID")); + Assertions.assertEquals(20, rs.getLong("COL1")); + Assertions.assertEquals(2, rs.getLong("COL2")); + Assertions.assertFalse(rs.next()); + } + } + Assertions.assertEquals(2, countCachedPlans(connection, ddl.getSchemaTemplateName())); + } + } } From 9d72bc54cfcc0704be8935babc23e7af7d40d1ca Mon Sep 17 00:00:00 2001 From: Sergei Pustovykh Date: Fri, 15 May 2026 12:14:47 +0100 Subject: [PATCH 09/12] Scheme-template-prepare test based on build version --- .../test/resources/schema-template-prepare.yamsql | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/yaml-tests/src/test/resources/schema-template-prepare.yamsql b/yaml-tests/src/test/resources/schema-template-prepare.yamsql index 4c8553a995..56749b692a 100644 --- a/yaml-tests/src/test/resources/schema-template-prepare.yamsql +++ b/yaml-tests/src/test/resources/schema-template-prepare.yamsql @@ -19,10 +19,16 @@ --- schema_template: - create table t1(id bigint, col1 bigint, col2 bigint, primary key(id)) - create index i1 as select col1 from t1 - prepare by_col1 from 'select * from t1 where col1 = 1' - prepare by_id from 'select * from t1 where id = 1' + - initialVersionLessThan: !current_version + definition: | + create table t1(id bigint, col1 bigint, col2 bigint, primary key(id)) + create index i1 as select col1 from t1 + - initialVersionAtLeast: !current_version + definition: | + create table t1(id bigint, col1 bigint, col2 bigint, primary key(id)) + create index i1 as select col1 from t1 + prepare by_col1 from 'select * from t1 where col1 = 1' + prepare by_id from 'select * from t1 where id = 1' --- setup: steps: From d400c801b84cb054c29ce2b97b4915b7848fa566 Mon Sep 17 00:00:00 2001 From: Sergei Pustovykh Date: Fri, 15 May 2026 13:21:26 +0100 Subject: [PATCH 10/12] revert-standard-tests.tamsql --- yaml-tests/src/test/resources/standard-tests.yamsql | 4 ---- 1 file changed, 4 deletions(-) diff --git a/yaml-tests/src/test/resources/standard-tests.yamsql b/yaml-tests/src/test/resources/standard-tests.yamsql index 459b731774..3e2c395d41 100644 --- a/yaml-tests/src/test/resources/standard-tests.yamsql +++ b/yaml-tests/src/test/resources/standard-tests.yamsql @@ -43,10 +43,6 @@ test_block: name: standard-tests tests: - -# - query: select id from T1 where col1=10 - - query: select col2 from T1 where col1=10 - - explain: "COVERING(I1 [EQUALS promote(@c7 AS LONG)] -> [COL1: KEY:[0], ID: KEY:[2]]) | MAP (_.ID AS ID)" - - - query: select id, case when col1 = 10 then 100 when col2 in (6,7,8,9) then 200 else 300 end as NEWCOL From 350ce078f4e50d641a2e2d04bbe179741af63a5e Mon Sep 17 00:00:00 2001 From: Sergei Pustovykh Date: Fri, 15 May 2026 14:07:40 +0100 Subject: [PATCH 11/12] team-scale test gap cover for NoOpSchemaTemplate.getPrepareStatements() --- .../recordlayer/metadata/NoOpSchemaTemplateTests.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/metadata/NoOpSchemaTemplateTests.java b/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/metadata/NoOpSchemaTemplateTests.java index 2698dbb7da..5e59ea451c 100644 --- a/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/metadata/NoOpSchemaTemplateTests.java +++ b/fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/metadata/NoOpSchemaTemplateTests.java @@ -234,4 +234,11 @@ public void testGenerateSchemaWithDifferentParameters() { assertEquals("schema1", schema1.getName()); assertEquals("schema2", schema2.getName()); } + + @Test + public void testGetPrepareStatementsReturnsEmptyMap() { + final NoOpSchemaTemplate template = new NoOpSchemaTemplate("test", 1); + assertNotNull(template.getPrepareStatements()); + assertEquals(0, template.getPrepareStatements().size()); + } } From 99acbd0e1221581b2ef863782223cae2e7dda707 Mon Sep 17 00:00:00 2001 From: Sergei Pustovykh Date: Fri, 15 May 2026 16:53:58 +0100 Subject: [PATCH 12/12] supported_version for whole file in schema-template-prepare.yamsql --- .../resources/schema-template-prepare.yamsql | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/yaml-tests/src/test/resources/schema-template-prepare.yamsql b/yaml-tests/src/test/resources/schema-template-prepare.yamsql index 56749b692a..49af5348b7 100644 --- a/yaml-tests/src/test/resources/schema-template-prepare.yamsql +++ b/yaml-tests/src/test/resources/schema-template-prepare.yamsql @@ -17,18 +17,15 @@ # See the License for the specific language governing permissions and # limitations under the License. +--- +options: + supported_version: !current_version --- schema_template: - - initialVersionLessThan: !current_version - definition: | - create table t1(id bigint, col1 bigint, col2 bigint, primary key(id)) - create index i1 as select col1 from t1 - - initialVersionAtLeast: !current_version - definition: | - create table t1(id bigint, col1 bigint, col2 bigint, primary key(id)) - create index i1 as select col1 from t1 - prepare by_col1 from 'select * from t1 where col1 = 1' - prepare by_id from 'select * from t1 where id = 1' + create table t1(id bigint, col1 bigint, col2 bigint, primary key(id)) + create index i1 as select col1 from t1 + prepare by_col1 from 'select * from t1 where col1 = 1' + prepare by_id from 'select * from t1 where id = 1' --- setup: steps: