Skip to content

Commit

Permalink
[MOREL-147] Allow Calcite schemas to contain sub-schemas
Browse files Browse the repository at this point in the history
Close #147
  • Loading branch information
GavinRay97 authored and julianhyde committed May 30, 2022
1 parent 03650d6 commit 0429a6d
Show file tree
Hide file tree
Showing 3 changed files with 190 additions and 11 deletions.
2 changes: 1 addition & 1 deletion src/main/java/net/hydromatic/morel/compile/BuiltIn.java
Original file line number Diff line number Diff line change
Expand Up @@ -1494,7 +1494,7 @@ public enum BuiltIn {
* vector {@code vec}, from left to right (i.e., increasing indices), until
* {@code f(x)} evaluates to {@code false}; it returns {@code false} if such
* an {@code x} exists and {@code true} otherwise. It is equivalent to
* {@code not (exists (not o f ) vec))}. */
* {@code not (exists (not o f) vec))}. */
VECTOR_ALL("Vector", "all", ts ->
ts.forallType(1, h ->
ts.fnType(ts.fnType(h.get(0), BOOL), h.vector(0), BOOL))),
Expand Down
44 changes: 34 additions & 10 deletions src/main/java/net/hydromatic/morel/foreign/CalciteForeignValue.java
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.stream.Collectors;

import static java.util.Objects.requireNonNull;
Expand All @@ -56,11 +58,22 @@ public CalciteForeignValue(Calcite calcite, SchemaPlus schema, boolean lower) {
}

public Type type(TypeSystem typeSystem) {
return toType(schema, typeSystem);
}

private Type toType(SchemaPlus schema, TypeSystem typeSystem) {
final ImmutableSortedMap.Builder<String, Type> fields =
ImmutableSortedMap.orderedBy(RecordType.ORDERING);
schema.getTableNames().forEach(tableName ->
fields.put(convert(tableName),
toType(schema.getTable(tableName), typeSystem)));
schema.getTableNames().forEach(tableName -> {
Table table = requireNonNull(schema.getTable(tableName));
fields.put(convert(tableName), toType(table, typeSystem));
});
schema.getSubSchemaNames().forEach(subSchemaName -> {
final SchemaPlus subSchema =
requireNonNull(schema.getSubSchema(subSchemaName));
fields.put(convert(subSchemaName), toType(subSchema, typeSystem));
});

return typeSystem.recordType(fields.build());
}

Expand All @@ -83,26 +96,37 @@ private static String convert(boolean lower, String name) {
}

public Object value() {
final ImmutableList.Builder<List<Object>> fieldValues =
ImmutableList.builder();
return valueFor(schema);
}

private ImmutableList<Object> valueFor(SchemaPlus schema) {
final SortedMap<String, Object> fieldValues =
new TreeMap<>(RecordType.ORDERING);
final List<String> names = Schemas.path(schema).names();
schema.getTableNames().forEach(tableName -> {
final RelBuilder b = calcite.relBuilder;
b.scan(plus(names, tableName));
final List<RexNode> exprList = b.peek().getRowType()
.getFieldList().stream()
.map(f ->
Ord.of(f.getIndex(),
lower ? f.getName().toLowerCase(Locale.ROOT) : f.getName()))
.map(f -> Ord.of(f.getIndex(), convert(f.getName())))
.sorted(Map.Entry.comparingByValue())
.map(p -> b.alias(b.field(p.i), p.e))
.collect(Collectors.toList());
b.project(exprList, ImmutableList.of(), true);
final RelNode rel = b.build();
final Converter<Object[]> converter = Converters.ofRow(rel.getRowType());
fieldValues.add(new RelList(rel, calcite.dataContext, converter));
fieldValues.put(convert(tableName),
new RelList(rel, calcite.dataContext, converter));
});

// Recursively walk sub-schemas and add their tables to fieldValues
schema.getSubSchemaNames().forEach(subSchemaName -> {
final SchemaPlus subSchema =
requireNonNull(schema.getSubSchema(subSchemaName));
fieldValues.put(convert(subSchemaName),
valueFor(subSchema));
});
return fieldValues.build();
return ImmutableList.copyOf(fieldValues.values());
}

/** Returns a copy of a list with one element appended. */
Expand Down
155 changes: 155 additions & 0 deletions src/test/java/net/hydromatic/morel/CalciteTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
/*
* Licensed to Julian Hyde under one or more contributor license
* agreements. See the NOTICE file distributed with this work
* for additional information regarding copyright ownership.
* Julian Hyde 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 net.hydromatic.morel;

import net.hydromatic.morel.foreign.Calcite;
import net.hydromatic.morel.foreign.CalciteForeignValue;
import net.hydromatic.morel.foreign.ForeignValue;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import org.apache.calcite.adapter.java.ReflectiveSchema;
import org.apache.calcite.schema.Schema;
import org.apache.calcite.schema.SchemaPlus;
import org.junit.jupiter.api.Test;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.InputStream;
import java.io.PrintStream;
import java.util.Map;

import static org.apache.calcite.util.Util.toLinux;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.core.Is.is;

/** Tests Morel's integration with Calcite.
*
* <p>Algebra is covered in {@link AlgebraTest} but this test covers
* what's left, such as {@link CalciteForeignValue} and schemas.
*/
class CalciteTest {
final boolean debug = hashCode() < hashCode(); // always false

/** Tests that you if you create a
* {@link net.hydromatic.morel.foreign.CalciteForeignValue}
* whose schema has nested schemas, those schemas appear as fields. */
@Test void testNestedSchema() {
final Schema userSchema = new ReflectiveSchema(new UserSchema());
final Schema taskSchema = new ReflectiveSchema(new TaskSchema());

final Map<String, ForeignValue> foreignValueMap = Calcite.withDataSets(
ImmutableMap.of("user", (Calcite calcite) -> {
SchemaPlus newSchema =
calcite.rootSchema.add("users", userSchema);
newSchema.add("task", taskSchema);
newSchema.add("task2", taskSchema);
return new CalciteForeignValue(calcite, newSchema, true);
})).foreignValues();

final String sql = "user;\n"
+ "user.task;\n"
+ "user.task2;\n"
+ "from t in user.users yield t;\n"
+ "from t in user.task2.tasks yield t;\n";
final InputStream in = new ByteArrayInputStream(sql.getBytes());
final ByteArrayOutputStream out = new ByteArrayOutputStream();
final Main main =
new Main(ImmutableList.of(), in, new PrintStream(out), foreignValueMap,
new File(""));

main.run();
if (debug) {
System.out.println(out);
}

String expected = "val it = {task={tasks=<relation>},"
+ "task2={tasks=<relation>},"
+ "users=<relation>}\n"
+ " : {task:{tasks:{completed:bool, name:string} list}, "
+ "task2:{tasks:{completed:bool, name:string} list}, "
+ "users:{age:int, name:string} list}\n"
+ "val it = {tasks=<relation>} : {tasks:{completed:bool, name:string} list}\n"
+ "val it = {tasks=<relation>} : {tasks:{completed:bool, name:string} list}\n"
+ "val it = [{age=20,name=\"John\"},{age=21,name=\"Jane\"},{age=22,name=\"Jack\"}]\n"
+ " : {age:int, name:string} list\n"
+ "val it =\n"
+ " [{completed=false,name=\"Buy milk\"},{completed=false,name=\"Buy eggs\"},\n"
+ " {completed=false,name=\"Buy bread\"}] : {completed:bool, name:string} list\n";
assertThat(toLinux(out.toString()), is(expected));
}

/** Java object that will, via reflection, become create the "user" schema. */
public static class UserSchema {
@Override public String toString() {
return "UserSchema";
}

/** Array that will, via reflection, become the "users" table in the
* "user" schema. */
@SuppressWarnings("unused") // used via reflection
public final User[] users = {
new User("John", 20),
new User("Jane", 21),
new User("Jack", 22)
};
}

/** Row in the "users" table. */
public static class User {
public final String name;
public final int age;

User(String name, int age) {
this.name = name;
this.age = age;
}
}

/** Java object that will, via reflection, become create the "task" schema. */
public static class TaskSchema {
@Override public String toString() {
return "TaskSchema";
}

/** Array that will, via reflection, become the "tasks" table in the
* "task" schema. */
@SuppressWarnings("unused") // used via reflection
public final Task[] tasks = {
new Task("Buy milk", false),
new Task("Buy eggs", false),
new Task("Buy bread", false)
};
}


/** Row in the "tasks" table. */
public static class Task {
public final String name;
public final boolean completed;

Task(String name, boolean completed) {
this.name = name;
this.completed = completed;
}
}
}

// End CalciteTest.java

0 comments on commit 0429a6d

Please sign in to comment.