Skip to content

Commit 7cca930

Browse files
authored
Support SQL views (#3680)
This introduces support for SQL views. Views are virtual tables defined by SQL queries that are compiled lazily when referenced, providing a way to encapsulate complex queries and present simplified interfaces to users. **Implementation Overview** Views are stored as raw SQL text in the catalog and compiled on-demand during query execution. The implementation follows a layered architecture: At the core layer, we introduce `RawView` which represents the persisted view definition. Views are stored in the metadata alongside user-defined functions and are serialized/deserialized using the existing protobuf infrastructure. The `RecordMetaData` and `RecordMetaDataBuilder` classes now maintain a map of views that get persisted with the schema. At the relational API layer, `RecordLayerView` wraps the raw view definition with compilation capabilities. Each view contains a lazy compilation function that transforms the SQL text into a logical plan when the view is first referenced. This compiled plan can be cached to avoid recompilation on subsequent accesses. The query parser has been extended to recognize view references in `FROM` clauses. When a view is encountered, the semantic analyzer retrieves the view definition from the catalog and triggers compilation by invoking the view's compilation function. The resulting logical operator is then seamlessly integrated into the query plan, allowing views to be used just like regular tables. **Features** - View Definition Storage: Views are defined using `CREATE VIEW` syntax and stored as SQL text in the schema template - Lazy Compilation: View definitions are compiled into logical plans only when referenced in queries - Nested Views: Views can reference other views, enabling layered abstractions - Complex Queries: Views support CTEs, joins, aggregations, and other advanced SQL features - Schema Integration: Views are first-class metadata objects alongside tables and functions The implementation includes comprehensive testing through YAML integration tests that validate various view scenarios including simple filters, nested views, CTEs within views, and views referencing user-defined functions. This fixes #3670.
1 parent 1e28ef4 commit 7cca930

File tree

36 files changed

+2053
-95
lines changed

36 files changed

+2053
-95
lines changed

fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/RecordMetaData.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import com.apple.foundationdb.record.metadata.UnnestedRecordType;
3232
import com.apple.foundationdb.record.metadata.expressions.KeyExpression;
3333
import com.apple.foundationdb.record.metadata.expressions.LiteralKeyExpression;
34+
import com.apple.foundationdb.record.metadata.View;
3435
import com.apple.foundationdb.record.query.plan.cascades.UserDefinedFunction;
3536
import com.apple.foundationdb.record.query.plan.cascades.typing.Type;
3637
import com.apple.foundationdb.record.query.plan.serialization.DefaultPlanSerializationRegistry;
@@ -87,6 +88,8 @@ public class RecordMetaData implements RecordMetaDataProvider {
8788
@Nonnull
8889
private final Map<String, UserDefinedFunction> userDefinedFunctionMap;
8990
@Nonnull
91+
private final Map<String, View> viewMap;
92+
@Nonnull
9093
private final Map<String, Index> indexes;
9194
@Nonnull
9295
private final Map<String, Index> universalIndexes;
@@ -117,6 +120,7 @@ protected RecordMetaData(@Nonnull RecordMetaData orig) {
117120
Collections.unmodifiableMap(orig.universalIndexes),
118121
Collections.unmodifiableList(orig.formerIndexes),
119122
Collections.unmodifiableMap(orig.userDefinedFunctionMap),
123+
Collections.unmodifiableMap(orig.viewMap),
120124
orig.splitLongRecords,
121125
orig.storeRecordVersions,
122126
orig.version,
@@ -137,6 +141,7 @@ protected RecordMetaData(@Nonnull Descriptors.FileDescriptor recordsDescriptor,
137141
@Nonnull Map<String, Index> universalIndexes,
138142
@Nonnull List<FormerIndex> formerIndexes,
139143
@Nonnull Map<String, UserDefinedFunction> userDefinedFunctionMap,
144+
@Nonnull Map<String, View> viewMap,
140145
boolean splitLongRecords,
141146
boolean storeRecordVersions,
142147
int version,
@@ -154,6 +159,7 @@ protected RecordMetaData(@Nonnull Descriptors.FileDescriptor recordsDescriptor,
154159
this.universalIndexes = universalIndexes;
155160
this.formerIndexes = formerIndexes;
156161
this.userDefinedFunctionMap = userDefinedFunctionMap;
162+
this.viewMap = viewMap;
157163
this.splitLongRecords = splitLongRecords;
158164
this.storeRecordVersions = storeRecordVersions;
159165
this.version = version;
@@ -702,6 +708,7 @@ public RecordMetaDataProto.MetaData toProto(@Nullable Descriptors.FileDescriptor
702708
PlanSerializationContext serializationContext = new PlanSerializationContext(DefaultPlanSerializationRegistry.INSTANCE,
703709
PlanHashable.CURRENT_FOR_CONTINUATION);
704710
builder.addAllUserDefinedFunctions(userDefinedFunctionMap.values().stream().map(func -> func.toProto(serializationContext)).collect(Collectors.toList()));
711+
builder.addAllViews(viewMap.values().stream().map(View::toProto).collect(Collectors.toList()));
705712
builder.setSplitLongRecords(splitLongRecords);
706713
builder.setStoreRecordVersions(storeRecordVersions);
707714
builder.setVersion(version);
@@ -726,6 +733,11 @@ public Map<String, UserDefinedFunction> getUserDefinedFunctionMap() {
726733
return userDefinedFunctionMap;
727734
}
728735

736+
@Nonnull
737+
public Map<String, View> getViewMap() {
738+
return viewMap;
739+
}
740+
729741
@Nonnull
730742
public static Map<String, Descriptors.FieldDescriptor> getFieldDescriptorMapFromTypes(@Nonnull final Collection<RecordType> recordTypes) {
731743
if (recordTypes.size() == 1) {

fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/RecordMetaDataBuilder.java

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
import com.apple.foundationdb.record.provider.foundationdb.IndexMaintainerRegistry;
4343
import com.apple.foundationdb.record.provider.foundationdb.IndexMaintainerFactoryRegistryImpl;
4444
import com.apple.foundationdb.record.provider.foundationdb.MetaDataProtoEditor;
45+
import com.apple.foundationdb.record.metadata.View;
4546
import com.apple.foundationdb.record.query.plan.cascades.UserDefinedFunction;
4647
import com.apple.foundationdb.record.query.plan.serialization.DefaultPlanSerializationRegistry;
4748
import com.apple.foundationdb.record.query.plan.serialization.PlanSerialization;
@@ -115,6 +116,8 @@ public class RecordMetaDataBuilder implements RecordMetaDataProvider {
115116
@Nonnull
116117
private final Map<String, UserDefinedFunction> userDefinedFunctionMap;
117118
@Nonnull
119+
private final Map<String, View> viewMap;
120+
@Nonnull
118121
private final Map<String, Index> indexes;
119122
@Nonnull
120123
private final Map<String, Index> universalIndexes;
@@ -150,6 +153,7 @@ public class RecordMetaDataBuilder implements RecordMetaDataProvider {
150153
evolutionValidator = MetaDataEvolutionValidator.getDefaultInstance();
151154
syntheticRecordTypes = new HashMap<>();
152155
userDefinedFunctionMap = new HashMap<>();
156+
viewMap = new HashMap<>();
153157
}
154158

155159
private void processSchemaOptions(boolean processExtensionOptions) {
@@ -234,6 +238,10 @@ private void loadProtoExceptRecords(@Nonnull RecordMetaDataProto.MetaData metaDa
234238
PlanHashable.CURRENT_FOR_CONTINUATION), function);
235239
userDefinedFunctionMap.put(func.getFunctionName(), func);
236240
}
241+
for (final RecordMetaDataProto.PView viewProto: metaDataProto.getViewsList()) {
242+
final View view = View.fromProto(viewProto);
243+
viewMap.put(view.getName(), view);
244+
}
237245
if (metaDataProto.hasSplitLongRecords()) {
238246
splitLongRecords = metaDataProto.getSplitLongRecords();
239247
}
@@ -1207,6 +1215,10 @@ public void addUserDefinedFunctions(@Nonnull Iterable<? extends UserDefinedFunct
12071215
functions.forEach(this::addUserDefinedFunction);
12081216
}
12091217

1218+
public void addView(@Nonnull View view) {
1219+
viewMap.put(view.getName(), view);
1220+
}
1221+
12101222
public boolean isSplitLongRecords() {
12111223
return splitLongRecords;
12121224
}
@@ -1448,7 +1460,7 @@ public RecordMetaData build(boolean validate) {
14481460
Map<Object, SyntheticRecordType<?>> recordTypeKeyToSyntheticRecordTypeMap = Maps.newHashMapWithExpectedSize(syntheticRecordTypes.size());
14491461
RecordMetaData metaData = new RecordMetaData(recordsDescriptor, getUnionDescriptor(), unionFields,
14501462
builtRecordTypes, builtSyntheticRecordTypes, recordTypeKeyToSyntheticRecordTypeMap,
1451-
indexes, universalIndexes, formerIndexes, userDefinedFunctionMap,
1463+
indexes, universalIndexes, formerIndexes, userDefinedFunctionMap, viewMap,
14521464
splitLongRecords, storeRecordVersions, version, subspaceKeyCounter, usesSubspaceKeyCounter, recordCountKey, localFileDescriptor != null);
14531465
for (RecordTypeBuilder recordTypeBuilder : recordTypes.values()) {
14541466
KeyExpression primaryKey = recordTypeBuilder.getPrimaryKey();
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
/*
2+
* View.java
3+
*
4+
* This source file is part of the FoundationDB open source project
5+
*
6+
* Copyright 2015-2025 Apple Inc. and the FoundationDB project authors
7+
*
8+
* Licensed under the Apache License, Version 2.0 (the "License");
9+
* you may not use this file except in compliance with the License.
10+
* You may obtain a copy of the License at
11+
*
12+
* http://www.apache.org/licenses/LICENSE-2.0
13+
*
14+
* Unless required by applicable law or agreed to in writing, software
15+
* distributed under the License is distributed on an "AS IS" BASIS,
16+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17+
* See the License for the specific language governing permissions and
18+
* limitations under the License.
19+
*/
20+
21+
package com.apple.foundationdb.record.metadata;
22+
23+
import com.apple.foundationdb.record.RecordMetaDataProto;
24+
25+
import javax.annotation.Nonnull;
26+
import java.util.Objects;
27+
28+
/**
29+
* Represents a SQL view definition stored in the catalog.
30+
* Views are stored as raw SQL text and compiled lazily when referenced in queries.
31+
* This class is used for serialization/deserialization of view definitions.
32+
*/
33+
public class View {
34+
35+
@Nonnull
36+
private final String name;
37+
38+
@Nonnull
39+
private final String definition;
40+
41+
public View(@Nonnull final String name, @Nonnull final String definition) {
42+
this.name = name;
43+
this.definition = definition;
44+
}
45+
46+
@Nonnull
47+
public String getName() {
48+
return name;
49+
}
50+
51+
@Nonnull
52+
public String getDefinition() {
53+
return definition;
54+
}
55+
56+
@Nonnull
57+
public RecordMetaDataProto.PView toProto() {
58+
return RecordMetaDataProto.PView.newBuilder()
59+
.setName(name)
60+
.setDefinition(definition)
61+
.build();
62+
}
63+
64+
@Override
65+
public boolean equals(final Object o) {
66+
if (this == o) {
67+
return true;
68+
}
69+
if (o == null || getClass() != o.getClass()) {
70+
return false;
71+
}
72+
final View that = (View) o;
73+
return Objects.equals(name, that.name) && Objects.equals(definition, that.definition);
74+
}
75+
76+
@Override
77+
public int hashCode() {
78+
return Objects.hash(name, definition);
79+
}
80+
81+
@Override
82+
public String toString() {
83+
return "View{" +
84+
"name='" + name + '\'' +
85+
", definition='" + definition + '\'' +
86+
'}';
87+
}
88+
89+
@Nonnull
90+
public static View fromProto(@Nonnull final RecordMetaDataProto.PView sqlView) {
91+
return new View(sqlView.getName(), sqlView.getDefinition());
92+
}
93+
}

fdb-record-layer-core/src/main/proto/record_metadata.proto

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,7 @@ message MetaData {
195195
repeated JoinedRecordType joined_record_types = 12;
196196
repeated UnnestedRecordType unnested_record_types = 13;
197197
repeated PUserDefinedFunction user_defined_functions = 14;
198+
repeated PView views = 15;
198199
extensions 1000 to 2000;
199200
}
200201

@@ -205,6 +206,11 @@ message PUserDefinedFunction {
205206
}
206207
}
207208

209+
message PView {
210+
optional string name = 1;
211+
optional string definition = 2;
212+
}
213+
208214
message JoinedRecordType {
209215
optional string name = 1;
210216
optional com.apple.foundationdb.record.expressions.Value record_type_key = 4;

fdb-record-layer-core/src/test/java/com/apple/foundationdb/record/metadata/RecordMetaDataBuilderTest.java

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -850,4 +850,43 @@ void canSerializeAndDeserializeSyntheticRecordTypes() {
850850
assertEquals(expectedIndex.getRootExpression(), actualIndex.getRootExpression(), "Incorrect index root expression");
851851
}
852852
}
853+
854+
@Test
855+
void recordMetadataCopyConstructorWorks() {
856+
// Create a sample RecordMetaData object.
857+
RecordMetaDataBuilder builder = RecordMetaData.newBuilder().setRecords(TestRecords1Proto.getDescriptor());
858+
builder.setSplitLongRecords(true);
859+
builder.setStoreRecordVersions(true);
860+
builder.setVersion(42);
861+
862+
final RecordMetaData original = builder.build();
863+
864+
// Create a subclass to access the protected copy constructor
865+
final RecordMetaData copy = new RecordMetaData(original) {
866+
};
867+
868+
// Verify that all properties are correctly copied
869+
assertEquals(original.getVersion(), copy.getVersion(), "Version should match");
870+
assertEquals(original.isSplitLongRecords(), copy.isSplitLongRecords(), "splitLongRecords should match");
871+
assertEquals(original.isStoreRecordVersions(), copy.isStoreRecordVersions(), "storeRecordVersions should match");
872+
assertEquals(original.usesSubspaceKeyCounter(), copy.usesSubspaceKeyCounter(), "usesSubspaceKeyCounter should match");
873+
assertEquals(original.getSubspaceKeyCounter(), copy.getSubspaceKeyCounter(), "subspaceKeyCounter should match");
874+
875+
// Verify record types
876+
assertEquals(original.getRecordTypes().size(), copy.getRecordTypes().size(), "Record type count should match");
877+
assertIterableEquals(original.getRecordTypes().keySet(), copy.getRecordTypes().keySet(), "Record type names should match");
878+
879+
// Verify indexes
880+
assertEquals(original.getAllIndexes().size(), copy.getAllIndexes().size(), "Index count should match");
881+
for (Index originalIndex : original.getAllIndexes()) {
882+
Index copiedIndex = copy.getIndex(originalIndex.getName());
883+
assertNotNull(copiedIndex, "Index should exist in copy: " + originalIndex.getName());
884+
assertEquals(originalIndex.getName(), copiedIndex.getName(), "Index name should match");
885+
assertEquals(originalIndex.getRootExpression(), copiedIndex.getRootExpression(), "Index root expression should match");
886+
}
887+
888+
// Verify descriptors
889+
assertEquals(original.getRecordsDescriptor(), copy.getRecordsDescriptor(), "Records descriptor should match");
890+
assertEquals(original.getUnionDescriptor(), copy.getUnionDescriptor(), "Union descriptor should match");
891+
}
853892
}
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
/*
2+
* ViewTest.java
3+
*
4+
* This source file is part of the FoundationDB open source project
5+
*
6+
* Copyright 2015-2025 Apple Inc. and the FoundationDB project authors
7+
*
8+
* Licensed under the Apache License, Version 2.0 (the "License");
9+
* you may not use this file except in compliance with the License.
10+
* You may obtain a copy of the License at
11+
*
12+
* http://www.apache.org/licenses/LICENSE-2.0
13+
*
14+
* Unless required by applicable law or agreed to in writing, software
15+
* distributed under the License is distributed on an "AS IS" BASIS,
16+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17+
* See the License for the specific language governing permissions and
18+
* limitations under the License.
19+
*/
20+
21+
package com.apple.foundationdb.record.metadata;
22+
23+
import com.apple.foundationdb.record.RecordMetaDataProto;
24+
import org.junit.jupiter.api.Test;
25+
26+
import static org.junit.jupiter.api.Assertions.assertEquals;
27+
import static org.junit.jupiter.api.Assertions.assertNotEquals;
28+
import static org.junit.jupiter.api.Assertions.assertTrue;
29+
30+
/**
31+
* Test cases for {@link View}.
32+
*/
33+
public class ViewTest {
34+
35+
@Test
36+
public void testConstructorAndGetters() {
37+
final String viewName = "test_view";
38+
final String definition = "SELECT * FROM table1";
39+
40+
final View view = new View(viewName, definition);
41+
42+
assertEquals(viewName, view.getName());
43+
assertEquals(definition, view.getDefinition());
44+
}
45+
46+
@Test
47+
public void testEqualsAndHashCode() {
48+
final View view1 = new View("view1", "SELECT * FROM t1");
49+
final View view2 = new View("view1", "SELECT * FROM t1");
50+
final View view3 = new View("view2", "SELECT * FROM t1");
51+
final View view4 = new View("view1", "SELECT * FROM t2");
52+
53+
// Test equality
54+
assertEquals(view1, view2);
55+
assertEquals(view1.hashCode(), view2.hashCode());
56+
57+
// Test inequality - different names
58+
assertNotEquals(view1, view3);
59+
60+
// Test inequality - different definitions
61+
assertNotEquals(view1, view4);
62+
63+
// Test self equality
64+
assertEquals(view1, view1);
65+
66+
// Test null inequality
67+
assertNotEquals(view1, null);
68+
}
69+
70+
@Test
71+
public void testToString() {
72+
final View view = new View("my_view", "SELECT id, name FROM users");
73+
final String result = view.toString();
74+
75+
assertTrue(result.contains("my_view"));
76+
assertTrue(result.contains("SELECT id, name FROM users"));
77+
}
78+
79+
@Test
80+
public void testProtoSerialization() {
81+
final String viewName = "serialization_test";
82+
final String definition = "SELECT a, b FROM table WHERE c > 10";
83+
final View originalView = new View(viewName, definition);
84+
85+
final RecordMetaDataProto.PView proto = originalView.toProto();
86+
87+
assertEquals(viewName, proto.getName());
88+
assertEquals(definition, proto.getDefinition());
89+
}
90+
91+
@Test
92+
public void testProtoDeserialization() {
93+
final String viewName = "deserialization_test";
94+
final String definition = "SELECT x, y, z FROM test_table";
95+
96+
final RecordMetaDataProto.PView protoView = RecordMetaDataProto.PView.newBuilder()
97+
.setName(viewName)
98+
.setDefinition(definition)
99+
.build();
100+
101+
final View view = View.fromProto(protoView);
102+
103+
assertEquals(viewName, view.getName());
104+
assertEquals(definition, view.getDefinition());
105+
}
106+
107+
@Test
108+
public void testRoundTripSerialization() {
109+
final View originalView = new View("round_trip_view", "SELECT * FROM employees WHERE salary > 50000");
110+
111+
// Serialize to proto
112+
final RecordMetaDataProto.PView proto = originalView.toProto();
113+
114+
// Deserialize from proto
115+
final View deserializedView = View.fromProto(proto);
116+
117+
// Verify equality
118+
assertEquals(originalView.getName(), deserializedView.getName());
119+
assertEquals(originalView.getDefinition(), deserializedView.getDefinition());
120+
}
121+
}

0 commit comments

Comments
 (0)