diff --git a/docs/datasqrl-spec.md b/docs/datasqrl-spec.md index 51530f6a44..5af78722a4 100644 --- a/docs/datasqrl-spec.md +++ b/docs/datasqrl-spec.md @@ -153,6 +153,7 @@ The `compiler` section of the configuration controls elements of the core compil "compiler" : { "addArguments": true, "logger": "print", + "extendedScalarTypes": false, "explain": { "visual": true, "text": true, @@ -164,9 +165,9 @@ The `compiler` section of the configuration controls elements of the core compil * `addArguments` specifies whether to include table columns as filters in the generated GraphQL schema. This only applies if the GraphQL schema is generated by the compiler. * `logger` configures the logging framework used for logging statements like `EXPORT MyTable TO logger.MyTable;`. It is `print` by default which logs to STDOUT. Set it to the configured log engine for logging output to be sent to that engine, e.g. `"logger": "kafka"`. Set it to `none` to suppress logging output. +* `extendedScalarTypes` optional parameter (defaults to false) that configures the registration and use of graphQL extended scalar types. If enabled, the primary key of tables will be mapped to BigInteger instead of Float in GraphQL. * `explain` configures how the DAG plan compiled by DataSQRL is presented in the `build` directory. If `visual` is true, a visual representation of the DAG is written to the `pipeline_visual.html` file which you can open in any browser. If `text` is true, a textual representation of the DAG is written to the `pipeline_explain.txt` file. If `extended` is true, the DAG outputs include more information like the relational plan which may be very verbose. - ### Profiles ```json diff --git a/pom.xml b/pom.xml index 51a033e334..7110af5c24 100755 --- a/pom.xml +++ b/pom.xml @@ -55,6 +55,7 @@ 5.8.2 1.27.0 19.2 + 19.1 3.5.6 1.11.3 2.3.32 @@ -363,6 +364,13 @@ ${graphql-java.version} + + com.graphql-java + graphql-java-extended-scalars + ${graphql-java-extended-scalars.version} + + + io.vertx diff --git a/sqrl-base/src/main/java/com/datasqrl/config/PackageJson.java b/sqrl-base/src/main/java/com/datasqrl/config/PackageJson.java index f9dde07dd4..73d8749e64 100644 --- a/sqrl-base/src/main/java/com/datasqrl/config/PackageJson.java +++ b/sqrl-base/src/main/java/com/datasqrl/config/PackageJson.java @@ -55,6 +55,8 @@ interface CompilerConfig { boolean isAddArguments(); String getLogger(); + + boolean isExtendedScalarTypes(); } interface OutputConfig { diff --git a/sqrl-calcite/src/main/java/com/datasqrl/calcite/QueryPlanner.java b/sqrl-calcite/src/main/java/com/datasqrl/calcite/QueryPlanner.java index 22b2c437c9..bd6deb193a 100644 --- a/sqrl-calcite/src/main/java/com/datasqrl/calcite/QueryPlanner.java +++ b/sqrl-calcite/src/main/java/com/datasqrl/calcite/QueryPlanner.java @@ -11,6 +11,7 @@ import com.datasqrl.calcite.schema.ExpandTableMacroRule.ExpandTableMacroConfig; import com.datasqrl.calcite.schema.sql.SqlBuilders.SqlSelectBuilder; import com.datasqrl.canonicalizer.ReservedName; +import com.datasqrl.graphql.server.CustomScalars; import com.datasqrl.parse.SqrlParserImpl; import com.datasqrl.util.DataContextImpl; import java.util.Arrays; @@ -185,6 +186,8 @@ public RelDataType parseDatatype(String datatype) { return this.cluster.getTypeFactory().createSqlType(SqlTypeName.INTEGER); } else if (datatype.equalsIgnoreCase("datetime")) { return this.cluster.getTypeFactory().createSqlType(SqlTypeName.TIMESTAMP, 3); + } else if (datatype.equalsIgnoreCase(CustomScalars.GRAPHQL_BIGINTEGER.getName())) { + return this.cluster.getTypeFactory().createSqlType(SqlTypeName.BIGINT); } // Todo fix: only supporting precision for non-alien types diff --git a/sqrl-planner/src/main/java/com/datasqrl/graphql/generate/GraphqlSchemaFactory.java b/sqrl-planner/src/main/java/com/datasqrl/graphql/generate/GraphqlSchemaFactory.java index 196325767c..4965b18c19 100644 --- a/sqrl-planner/src/main/java/com/datasqrl/graphql/generate/GraphqlSchemaFactory.java +++ b/sqrl-planner/src/main/java/com/datasqrl/graphql/generate/GraphqlSchemaFactory.java @@ -9,7 +9,7 @@ import static com.datasqrl.graphql.generate.GraphqlSchemaUtil.getInputType; import static com.datasqrl.graphql.generate.GraphqlSchemaUtil.getOutputType; import static com.datasqrl.graphql.generate.GraphqlSchemaUtil.isValidGraphQLName; -import static com.datasqrl.graphql.generate.GraphqlSchemaUtil.wrap; +import static com.datasqrl.graphql.generate.GraphqlSchemaUtil.wrapNullable; import static com.datasqrl.graphql.jdbc.SchemaConstants.LIMIT; import static com.datasqrl.graphql.jdbc.SchemaConstants.OFFSET; import static graphql.schema.GraphQLNonNull.nonNull; @@ -19,17 +19,14 @@ import com.datasqrl.canonicalizer.NamePath; import com.datasqrl.config.PackageJson.CompilerConfig; -import com.datasqrl.config.SystemBuiltInConnectors; import com.datasqrl.engine.log.LogManager; import com.datasqrl.function.SqrlFunctionParameter; import com.datasqrl.graphql.server.CustomScalars; import com.datasqrl.io.tables.TableType; import com.datasqrl.plan.table.PhysicalRelationalTable; import com.datasqrl.plan.table.ProxyImportRelationalTable; -import com.datasqrl.plan.table.QueryRelationalTable; import com.datasqrl.plan.validate.ExecutionGoal; import com.datasqrl.plan.validate.ResolvedImport; -import com.datasqrl.plan.validate.ScriptPlanner.Mutation; import com.datasqrl.schema.Multiplicity; import com.datasqrl.schema.NestedRelationship; import com.datasqrl.schema.Relationship.JoinType; @@ -37,6 +34,7 @@ import com.google.inject.Inject; import graphql.Scalars; import graphql.language.IntValue; +import graphql.scalars.ExtendedScalars; import graphql.schema.GraphQLArgument; import graphql.schema.GraphQLFieldDefinition; import graphql.schema.GraphQLInputType; @@ -67,7 +65,6 @@ import org.apache.calcite.schema.Table; import org.apache.calcite.util.ImmutableBitSet; import org.apache.commons.collections.ListUtils; -import scala.annotation.meta.field; /** * Creates a default graphql schema based on the SQRL schema @@ -80,6 +77,7 @@ public class GraphqlSchemaFactory { private final Set usedNames = new HashSet<>(); private final SqrlSchema schema; private final boolean addArguments; + private final boolean extendedScalarTypes; private final LogManager logManager; // Root path signifying the 'Query' type. private Map> objectPathToTables; @@ -89,13 +87,14 @@ public class GraphqlSchemaFactory { @Inject public GraphqlSchemaFactory(SqrlSchema schema, CompilerConfig config, LogManager logManager) { - this(schema, config.isAddArguments(), logManager); + this(schema, config.isAddArguments(), config.isExtendedScalarTypes(), logManager); } - public GraphqlSchemaFactory(SqrlSchema schema, boolean addArguments, LogManager logManager) { + public GraphqlSchemaFactory(SqrlSchema schema, boolean addArguments, boolean extendedScalarTypes, LogManager logManager) { this.schema = schema; this.addArguments = addArguments; this.logManager = logManager; + this.extendedScalarTypes = extendedScalarTypes; } public Optional generate(ExecutionGoal goal) { @@ -105,21 +104,22 @@ public Optional generate(ExecutionGoal goal) { this.fieldPathToTables = schema.getTableFunctions().stream() .collect(Collectors.groupingBy(SqrlTableMacro::getAbsolutePath, LinkedHashMap::new, Collectors.toList())); - for (Map.Entry> path : fieldPathToTables.entrySet()) { - if (path.getKey().getLast().isHidden()) continue; + for (Map.Entry> field : fieldPathToTables.entrySet()) { + if (field.getKey().getLast().isHidden()) continue; // hidden field not exposed in graphQL queries // if (goal == ExecutionGoal.TEST) { // if (!path.getValue().get(0).isTest()) continue; // } else { // if (path.getValue().get(0).isTest()) continue; // } - Optional graphQLObjectType = generateObject(path.getValue(), - objectPathToTables.getOrDefault(path.getKey(), List.of())); + Optional graphQLObjectType = generateObject(field.getValue(), // list of table functions de field is in + objectPathToTables.getOrDefault(field.getKey(), List.of())); // List of table functions relationships graphQLObjectType.map(objectTypes::add); } GraphQLObjectType queryType = createQueryType(goal, objectPathToTables.get(NamePath.ROOT)); + // cleaning of invalid objectTypes postProcess(); if (queryFields.isEmpty()) { @@ -129,6 +129,9 @@ public Optional generate(ExecutionGoal goal) { GraphQLSchema.Builder builder = GraphQLSchema.newSchema() .query(queryType); + if (extendedScalarTypes) { // use the plural parameter name in place of only bigInteger to avoid having a conf parameter of each special type mapping feature in the future + builder.additionalTypes(Set.of(CustomScalars.GRAPHQL_BIGINTEGER)); + } if (goal != ExecutionGoal.TEST) { if (logManager.hasLogEngine() && System.getenv().get("ENABLE_SUBSCRIPTIONS") != null) { Optional subscriptions = createSubscriptionTypes(schema); @@ -137,7 +140,7 @@ public Optional generate(ExecutionGoal goal) { Optional mutations = createMutationTypes(schema); mutations.map(builder::mutation); } - builder.additionalTypes(new LinkedHashSet<>(objectTypes)); + builder.additionalTypes(new LinkedHashSet<>(objectTypes)); // the inferred types if (queryType.getFields().isEmpty()) { if (goal == ExecutionGoal.TEST) { @@ -174,7 +177,7 @@ private Optional createMutationTypes(SqrlSchema schema) { RelDataType type = mutation.getRelDataType(); // Create the 'event' argument which should mirror the structure of the type - GraphQLInputType inputType = GraphqlSchemaUtil.createInputTypeForRelDataType(type, NamePath.of(name), seen).orElseThrow( + GraphQLInputType inputType = GraphqlSchemaUtil.createInputTypeForRelDataType(type, NamePath.of(name), seen, extendedScalarTypes).orElseThrow( () -> new IllegalArgumentException("Could not create input type for mutation: " + mutation.getName())); GraphQLArgument inputArgument = GraphQLArgument.newArgument() @@ -185,7 +188,7 @@ private Optional createMutationTypes(SqrlSchema schema) { GraphQLFieldDefinition subscriptionField = GraphQLFieldDefinition.newFieldDefinition() .name(name) .argument(inputArgument) - .type(createOutputTypeForRelDataType(type, NamePath.of(name), seen).get()) + .type(createOutputTypeForRelDataType(type, NamePath.of(name), seen, extendedScalarTypes).get()) .build(); builder.field(subscriptionField); @@ -226,7 +229,7 @@ public Optional createSubscriptionTypes(SqrlSchema schema) { GraphQLFieldDefinition subscriptionField = GraphQLFieldDefinition.newFieldDefinition() .name(tableName) - .type(createOutputTypeForRelDataType(table.getRowType(), NamePath.of(tableName), seen).get()) + .type(createOutputTypeForRelDataType(table.getRowType(), NamePath.of(tableName), seen, extendedScalarTypes).get()) .build(); subscriptionFields.add(subscriptionField); @@ -258,7 +261,7 @@ private GraphQLObjectType createQueryType(ExecutionGoal goal, List generateObject(List tableMac //Todo check: The multiple table macros should all point to the same relnode type Map> relByName = relationships.stream() - .collect(Collectors.groupingBy(g -> g.getFullPath().getLast(), + .collect(Collectors.groupingBy(g -> g.getFullPath().getLast(), //map from relationship field to list of table functions LinkedHashMap::new, Collectors.toList())); - SqrlTableMacro first = tableMacros.get(0); - RelDataType rowType = first.getRowType(); + SqrlTableMacro first = tableMacros.get(0); // take the first table function the field is in + RelDataType rowType = first.getRowType(); // and extract its calcite schema. //todo: check that all table macros are compatible // for (int i = 1; i < tableMacros.size(); i++) { // } + // create the graphQL fields for non-relationship fields List fields = new ArrayList<>(); for (RelDataTypeField field : rowType.getFieldList()) { if (!relByName.containsKey(Name.system(field.getName()))) { createRelationshipField(field).map(fields::add); } } - + // create the graphQL fields for relationship fields for (Map.Entry> rel : relByName.entrySet()) { createRelationshipField(rel.getValue()).map(fields::add); } @@ -338,7 +342,7 @@ private Optional createRelationshipField(List createArguments(SqrlTableMacro field) { } else { return parameters.stream() .filter(p->!((SqrlFunctionParameter)p).isInternal()) - .filter(p->getInputType(p.getType(null), NamePath.of(p.getName()), seen).isPresent()) + .filter(p->getInputType(p.getType(null), NamePath.of(p.getName()), seen, extendedScalarTypes).isPresent()) .map(parameter -> GraphQLArgument.newArgument() .name(((SqrlFunctionParameter)parameter).getVariableName()) - .type(nonNull(getInputType(parameter.getType(null), NamePath.of(parameter.getName()), seen).get())) + .type(nonNull(getInputType(parameter.getType(null), NamePath.of(parameter.getName()), seen, extendedScalarTypes).get())) .build()).collect(Collectors.toList()); } } @@ -417,12 +421,12 @@ private List generatePermuted(SqrlTableMacro macro) { return primaryKeys .stream() - .filter(f -> getInputType(f.getType(), NamePath.of(tableName), seen).isPresent()) + .filter(f -> getInputType(f.getType(), NamePath.of(tableName), seen, extendedScalarTypes).isPresent()) .filter(f -> isValidGraphQLName(f.getName())) .filter(this::isVisible) .map(f -> GraphQLArgument.newArgument() .name(f.getName()) - .type(getInputType(f.getType(), NamePath.of(f.getName()), seen).get()) + .type(getInputType(f.getType(), NamePath.of(f.getName()), seen, extendedScalarTypes).get()) .build()) .collect(Collectors.toList()); } @@ -438,12 +442,12 @@ private GraphQLOutputType createTypeName(SqrlTableMacro sqrlTableMacro) { } private Optional createRelationshipField(RelDataTypeField field) { - return getOutputType(field.getType(), NamePath.of(field.getName()), seen) + return getOutputType(field.getType(), NamePath.of(field.getName()), seen, extendedScalarTypes) .filter(f->isValidGraphQLName(field.getName())) .filter(f->isVisible(field)) .map(t -> GraphQLFieldDefinition.newFieldDefinition() .name(field.getName()) - .type(wrap(t, field.getType())).build()); + .type(wrapNullable(t, field.getType())).build()); } public void postProcess() { diff --git a/sqrl-planner/src/main/java/com/datasqrl/graphql/generate/GraphqlSchemaUtil.java b/sqrl-planner/src/main/java/com/datasqrl/graphql/generate/GraphqlSchemaUtil.java index a59baaab69..3fcbb43e2d 100644 --- a/sqrl-planner/src/main/java/com/datasqrl/graphql/generate/GraphqlSchemaUtil.java +++ b/sqrl-planner/src/main/java/com/datasqrl/graphql/generate/GraphqlSchemaUtil.java @@ -13,6 +13,7 @@ import com.datasqrl.schema.Multiplicity; import graphql.Scalars; import graphql.language.FieldDefinition; +import graphql.scalars.ExtendedScalars; import graphql.schema.GraphQLFieldDefinition; import graphql.schema.GraphQLInputObjectField; import graphql.schema.GraphQLInputObjectType; @@ -35,14 +36,14 @@ @Slf4j public class GraphqlSchemaUtil { - public static GraphQLOutputType wrap(GraphQLOutputType gqlType, RelDataType type) { + public static GraphQLOutputType wrapNullable(GraphQLOutputType gqlType, RelDataType type) { if (!type.isNullable()) { return GraphQLNonNull.nonNull(gqlType); } return gqlType; } - public static GraphQLOutputType wrap(GraphQLOutputType type, Multiplicity multiplicity) { + public static GraphQLOutputType wrapMultiplicity(GraphQLOutputType type, Multiplicity multiplicity) { switch (multiplicity) { case ZERO_ONE: return type; @@ -58,24 +59,24 @@ public static boolean isValidGraphQLName(String name) { return !isSystemHidden(name) && Pattern.matches("[_A-Za-z][_0-9A-Za-z]*", name); } - public static Optional getInputType(RelDataType type, NamePath namePath, Set seen) { - return getInOutType(type, namePath, seen) + public static Optional getInputType(RelDataType type, NamePath namePath, Set seen, boolean extendedScalarTypes) { + return getInOutType(type, namePath, seen, extendedScalarTypes) .map(f->(GraphQLInputType)f); } - public static Optional getOutputType(RelDataType type, NamePath namePath, Set seen) { - return getInOutType(type, namePath, seen) + public static Optional getOutputType(RelDataType type, NamePath namePath, Set seen, boolean extendedScalarTypes) { + return getInOutType(type, namePath, seen, extendedScalarTypes) .map(f->(GraphQLOutputType)f); } public static Optional getInOutType(RelDataType type, NamePath namePath, - Set seen) { - return getInOutTypeHelper(type, namePath, seen); + Set seen, boolean extendedScalarTypes) { + return getInOutTypeHelper(type, namePath, seen, extendedScalarTypes); } // Todo move to dialect? public static Optional getInOutTypeHelper(RelDataType type, NamePath namePath, - Set seen) { + Set seen, boolean extendedScalarTypes) { if (type.getSqlTypeName() == null) { return Optional.empty(); } @@ -97,7 +98,10 @@ public static Optional getInOutTypeHelper(RelDataType type, NamePat case SMALLINT: case INTEGER: return Optional.of(Scalars.GraphQLInt); - case BIGINT: //treat bigint as float to prevent overflow + case BIGINT: + if (extendedScalarTypes) { + return Optional.of(CustomScalars.GRAPHQL_BIGINTEGER); + } case DECIMAL: case FLOAT: case REAL: @@ -129,7 +133,7 @@ public static Optional getInOutTypeHelper(RelDataType type, NamePat return Optional.of(Scalars.GraphQLString); case ARRAY: case MULTISET: - return getOutputType(type.getComponentType(), namePath, seen).map(GraphQLList::list); + return getOutputType(type.getComponentType(), namePath, seen, extendedScalarTypes).map(GraphQLList::list); case STRUCTURED: case ROW: GraphQLObjectType.Builder builder = GraphQLObjectType.newObject(); @@ -137,10 +141,10 @@ public static Optional getInOutTypeHelper(RelDataType type, NamePat builder.name(typeName); for (RelDataTypeField field : type.getFieldList()) { if (field.getName().startsWith(HIDDEN_PREFIX)) continue; - getOutputType(field.getType(), namePath.concat(Name.system(field.getName())), seen) + getOutputType(field.getType(), namePath.concat(Name.system(field.getName())), seen, extendedScalarTypes) .ifPresent(fieldType -> builder.field(GraphQLFieldDefinition.newFieldDefinition() .name(field.getName()) - .type(wrap(fieldType, field.getType())) + .type(wrapNullable(fieldType, field.getType())) .build())); } return Optional.of(builder.build()); @@ -184,22 +188,23 @@ private static String toName(NamePath namePath, String postfix) { } public static Optional createOutputTypeForRelDataType(RelDataType type, - NamePath namePath, Set seen) { + NamePath namePath, Set seen, boolean extendedScalarTypes) { if (!type.isNullable()) { - return getOutputType(type, namePath, seen).map(GraphQLNonNull::nonNull); + return getOutputType(type, namePath, seen, extendedScalarTypes).map(GraphQLNonNull::nonNull); } - return getOutputType(type, namePath, seen); + return getOutputType(type, namePath, seen, extendedScalarTypes); } - public static Optional createInputTypeForRelDataType(RelDataType type, NamePath namePath, Set seen) { + public static Optional createInputTypeForRelDataType(RelDataType type, NamePath namePath, Set seen, boolean extendedScalarTypes) { if (namePath.getLast().isHidden()) return Optional.empty(); if (!type.isNullable()) { - return getGraphQLInputType(type, namePath, seen).map(GraphQLNonNull::nonNull); + return getGraphQLInputType(type, namePath, seen, extendedScalarTypes).map(GraphQLNonNull::nonNull); } - return getGraphQLInputType(type, namePath, seen); + return getGraphQLInputType(type, namePath, seen, extendedScalarTypes); } - private static Optional getGraphQLInputType(RelDataType type, NamePath namePath, Set seen) { + // TODO should not we use getInOutTypeHelper instead, the code seems duplicated ? + private static Optional getGraphQLInputType(RelDataType type, NamePath namePath, Set seen, boolean extendedScalarTypes) { switch (type.getSqlTypeName()) { case BOOLEAN: return Optional.of(Scalars.GraphQLBoolean); @@ -208,6 +213,9 @@ private static Optional getGraphQLInputType(RelDataType type, case TINYINT: return Optional.of(Scalars.GraphQLInt); case BIGINT: + if (extendedScalarTypes) { + return Optional.of(CustomScalars.GRAPHQL_BIGINTEGER); + } case FLOAT: case REAL: case DOUBLE: @@ -227,10 +235,10 @@ private static Optional getGraphQLInputType(RelDataType type, return Optional.of(Scalars.GraphQLString); // Typically handled as Base64 encoded strings case ARRAY: return type.getComponentType() != null - ? getGraphQLInputType(type.getComponentType(), namePath, seen).map(GraphQLList::list) + ? getGraphQLInputType(type.getComponentType(), namePath, seen, extendedScalarTypes).map(GraphQLList::list) : Optional.empty(); case ROW: - return createGraphQLInputObjectType(type, namePath, seen); + return createGraphQLInputObjectType(type, namePath, seen, extendedScalarTypes); case MAP: return Optional.of(CustomScalars.JSON); default: @@ -241,13 +249,13 @@ private static Optional getGraphQLInputType(RelDataType type, /** * Creates a GraphQL input object type for a ROW type. */ - private static Optional createGraphQLInputObjectType(RelDataType rowType, NamePath namePath, Set seen) { + private static Optional createGraphQLInputObjectType(RelDataType rowType, NamePath namePath, Set seen, boolean extendedScalarTypes) { GraphQLInputObjectType.Builder builder = GraphQLInputObjectType.newInputObject(); String typeName = generateUniqueNameForType(namePath, seen, "Input"); builder.name(typeName); for (RelDataTypeField field : rowType.getFieldList()) { - createInputTypeForRelDataType(field.getType(), namePath.concat(Name.system(field.getName())), seen) + createInputTypeForRelDataType(field.getType(), namePath.concat(Name.system(field.getName())), seen, extendedScalarTypes) .ifPresent(fieldType -> builder.field(GraphQLInputObjectField.newInputObjectField() .name(field.getName()) .type(fieldType) @@ -259,16 +267,16 @@ private static Optional createGraphQLInputObjectType(RelDataTy /** * Helper to handle map types, transforming them into a list of key-value pairs. */ - private static Optional createGraphQLInputMapType(RelDataType keyType, RelDataType valueType, NamePath namePath, Set seen) { + private static Optional createGraphQLInputMapType(RelDataType keyType, RelDataType valueType, NamePath namePath, Set seen, boolean extendedScalarTypes) { GraphQLInputObjectType mapEntryType = GraphQLInputObjectType.newInputObject() .name("MapEntry") .field(GraphQLInputObjectField.newInputObjectField() .name("key") - .type(getGraphQLInputType(keyType, namePath, seen).orElse(Scalars.GraphQLString)) + .type(getGraphQLInputType(keyType, namePath, seen, extendedScalarTypes).orElse(Scalars.GraphQLString)) .build()) .field(GraphQLInputObjectField.newInputObjectField() .name("value") - .type(getGraphQLInputType(valueType, namePath, seen).orElse(Scalars.GraphQLString)) + .type(getGraphQLInputType(valueType, namePath, seen, extendedScalarTypes).orElse(Scalars.GraphQLString)) .build()) .build(); return Optional.of(GraphQLList.list(mapEntryType)); diff --git a/sqrl-planner/src/main/java/com/datasqrl/v2/graphql/GraphqlSchemaFactory2.java b/sqrl-planner/src/main/java/com/datasqrl/v2/graphql/GraphqlSchemaFactory2.java new file mode 100644 index 0000000000..73057f9038 --- /dev/null +++ b/sqrl-planner/src/main/java/com/datasqrl/v2/graphql/GraphqlSchemaFactory2.java @@ -0,0 +1,448 @@ +package com.datasqrl.v2.graphql; + +import static com.datasqrl.canonicalizer.Name.HIDDEN_PREFIX; +import static com.datasqrl.canonicalizer.Name.isHiddenString; +import static com.datasqrl.graphql.jdbc.SchemaConstants.LIMIT; +import static com.datasqrl.graphql.jdbc.SchemaConstants.OFFSET; +import static com.datasqrl.v2.graphql.GraphqlSchemaUtil2.*; +import static graphql.schema.GraphQLNonNull.nonNull; + +import com.datasqrl.canonicalizer.NamePath; +import com.datasqrl.config.PackageJson.CompilerConfig; +import com.datasqrl.engine.server.ServerPhysicalPlan; +import com.datasqrl.schema.Multiplicity; +import com.datasqrl.v2.tables.SqrlFunctionParameter; +import com.datasqrl.graphql.server.CustomScalars; +import com.datasqrl.v2.parser.AccessModifier; +import com.datasqrl.v2.tables.SqrlTableFunction; +import com.google.common.base.Preconditions; +import com.google.inject.Inject; +import graphql.Scalars; +import graphql.language.IntValue; +import graphql.schema.GraphQLArgument; +import graphql.schema.GraphQLFieldDefinition; +import graphql.schema.GraphQLList; +import graphql.schema.GraphQLNonNull; +import graphql.schema.GraphQLObjectType; +import graphql.schema.GraphQLOutputType; +import graphql.schema.GraphQLScalarType; +import graphql.schema.GraphQLSchema; +import graphql.schema.GraphQLType; +import graphql.schema.GraphQLTypeReference; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; +import lombok.extern.slf4j.Slf4j; +import org.apache.calcite.rel.type.RelDataType; +import org.apache.calcite.rel.type.RelDataTypeField; +import org.apache.calcite.schema.FunctionParameter; +import org.apache.commons.collections.ListUtils; + +/** + * Creates a default graphql schema based on the SQRL schema + */ + + + +@Slf4j +public class GraphqlSchemaFactory2 { + + private final List queryFields = new ArrayList<>(); + private final List objectTypes = new ArrayList<>(); + private final Set definedTypeNames = new HashSet<>(); + private final boolean extendedScalarTypes; + + @Inject + public GraphqlSchemaFactory2(CompilerConfig config) { + this.extendedScalarTypes = config.isExtendedScalarTypes(); + } + + public Optional generate(ServerPhysicalPlan serverPlan) { + //configure the schema builder + GraphQLSchema.Builder graphQLSchemaBuilder = GraphQLSchema.newSchema(); + if (extendedScalarTypes) { // use the plural parameter name in place of only bigInteger to avoid having a conf parameter of each special type mapping feature in the future + graphQLSchemaBuilder.additionalTypes(Set.of(CustomScalars.GRAPHQL_BIGINTEGER)); + } + + // process query table functions + final Optional queriesObjectType = createQueriesOrSubscriptionsObjectType(serverPlan, AccessModifier.QUERY); + //TODO when there is no query fields createQueriesOrSubscriptionsObjectType return an empty optional, so this test is no more neeeded. + // and if we remove cleanInvalidTypes, the whole queryFields is no more needed. + /* + if (queryFields.isEmpty()) { // there must be at least 1 query + return Optional.empty(); + } +*/ + queriesObjectType.ifPresentOrElse( + graphQLSchemaBuilder::query, + () -> {throw new IllegalArgumentException("No queryable tables found for server");} // there is no query + ); + + // process subscriptions table functions + final Optional subscriptionsObjectType = createQueriesOrSubscriptionsObjectType(serverPlan, AccessModifier.SUBSCRIPTION); + subscriptionsObjectType.ifPresent(graphQLSchemaBuilder::subscription); + + // process mutations table functions + final Optional mutationsObjectType = createMutationsObjectType(); + mutationsObjectType.ifPresent(graphQLSchemaBuilder::mutation); + + graphQLSchemaBuilder.additionalTypes(new LinkedHashSet<>(objectTypes)); // the cleaned types + + return Optional.of(graphQLSchemaBuilder.build()); + } + + /** + * GraphQL queries and subscriptions are generated the same way. So we call this method with + * {@link AccessModifier#QUERY} for generating queries and with {@link AccessModifier#SUBSCRIPTION} for generating subscriptions. + */ + public Optional createQueriesOrSubscriptionsObjectType(ServerPhysicalPlan serverPlan, AccessModifier tableFunctionsType) { + + final List tableFunctions = + serverPlan.getFunctions().stream() + .filter(function -> function.getVisibility().getAccess() == tableFunctionsType) + .collect(Collectors.toList()); + + // group table functions by their parent path + Map> tableFunctionsByTheirParentPath = + tableFunctions.stream() + .collect(Collectors.groupingBy( + function -> function.getFullPath().popLast(), + LinkedHashMap::new, + Collectors.toList() + ) + ); + + for (SqrlTableFunction tableFunction : tableFunctions) { + // create resultType of the table function and add it to the GraphQl object types + if (!tableFunction.isRelationship()) { // root table function + Optional resultType = + createRootTableFunctionResultType( + tableFunction, + tableFunctionsByTheirParentPath.getOrDefault(tableFunction.getFullPath(), List.of()) // List of table functions which parent is tableFunction (its relationships). + ); + resultType.map(objectTypes::add); + } else { // relationship table function + Optional resultType = createRelationshipTableFunctionResultType(tableFunction); + resultType.map(objectTypes::add); + } + } + // create root type ("Query" or "Subscription") + final List rootTableFunctions = tableFunctions.stream() + .filter(tableFunction -> !tableFunction.isRelationship()) + .collect(Collectors.toList()); + Optional rootObjectType = createRootType(rootTableFunctions, tableFunctionsType); + //TODO no more needed? + //TODO fix cleanInvalidTypes: it removes nestedTypes. +// cleanInvalidTypes(); + return rootObjectType; + } + + + /** + * Create the graphQL result type for a root table function (non-relationship) + */ + private Optional createRootTableFunctionResultType(SqrlTableFunction tableFunction, List itsRelationships) { + + String typeName; + if (tableFunction.getFunctionAnalysis().getOptionalBaseTable().isPresent()) { + final String baseTableName = tableFunction.getBaseTable().getName(); + if (definedTypeNames.contains(baseTableName)) {// result type was already defined + return Optional.empty(); + } + else { // it is a new result type. Using base table name + typeName = baseTableName; + } + } else { // there is no base table. Use the function name (guaranteed to be uniq by the compiler) + typeName = tableFunction.getFullPath().getLast().getDisplay(); + } + /* BROWSE THE FIELDS + They are either + - a non-relationship field : + - a scalar type + - a nested relDataType (which is no more planed as a table function). For that case we stop at depth=1 for now + - a relationship field (a table function with path size = 2) that needs to be wired up to the root table + */ + + // non-relationship fields + // now all relationships are functions that are separate from the rowType. So there can no more have relationship fields inside it + RelDataType rowType = tableFunction.getRowType(); + List fields = new ArrayList<>(); + for (RelDataTypeField field : rowType.getFieldList()) { + createRelDataTypeField(field).map(fields::add); + } + + // relationship fields (reference to types defined when processing the relationship) need to be wired into the root table. + for (SqrlTableFunction relationship : itsRelationships) { + createRelationshipField(relationship).map(fields::add); + } + + if (fields.isEmpty()) { + return Optional.empty(); + } + + GraphQLObjectType objectType = GraphQLObjectType.newObject() + .name(typeName) + .fields(fields) + .build(); + definedTypeNames.add(typeName); + queryFields.addAll(fields); + return Optional.of(objectType); + } + /** + * Generate the result type for relationship table functions + */ + private Optional createRelationshipTableFunctionResultType(SqrlTableFunction tableFunction) { + + if (tableFunction.getFunctionAnalysis().getOptionalBaseTable().isPresent()) { // the type was created in the root table function + return Optional.empty(); + } + /* BROWSE THE FIELDS + There is No more nested relationships, so when we browse the fields, they are either + - a scalar type + - a nested relDataType (which is no more planed as a table function). For that case we stop at depth=1 for now + */ + RelDataType rowType = tableFunction.getRowType(); + List fields = new ArrayList<>(); + for (RelDataTypeField field : rowType.getFieldList()) { + createRelDataTypeField(field).map(fields::add); + } + + if (fields.isEmpty()) { + return Optional.empty(); + } + + String typeName = uniquifyNameForPath(tableFunction.getFullPath()); + GraphQLObjectType objectType = GraphQLObjectType.newObject() + .name(typeName) + .fields(fields) + .build(); + definedTypeNames.add(typeName); + queryFields.addAll(fields); + return Optional.of(objectType); + } + + public Optional createMutationsObjectType() { + // TODO implement + return Optional.empty(); + } + + /** + * GraphQL queries and subscriptions are generated the same way. So we call this method with + * {@link AccessModifier#QUERY} for generating root Query type and with {@link AccessModifier#SUBSCRIPTION} for generating root subscription type. + */ + private Optional createRootType(List rootTableFunctions, AccessModifier tableFunctionsType) { + + List fields = new ArrayList<>(); + + for (SqrlTableFunction tableFunction : rootTableFunctions) { + String tableFunctionName = tableFunction.getFullPath().getDisplay(); + if (!isValidGraphQLName(tableFunctionName)) { + continue; + } + + final GraphQLOutputType type = + tableFunctionsType == AccessModifier.QUERY + ? (GraphQLOutputType) wrapMultiplicity(createTypeReference(tableFunction), tableFunction.getMultiplicity()) + : createTypeReference(tableFunction); // type is nullable because there can be no update in the subscription + GraphQLFieldDefinition field = GraphQLFieldDefinition.newFieldDefinition() + .name(tableFunctionName) + .type(type) + .arguments(createArguments(tableFunction)) + .build(); + fields.add(field); + } + if (fields.isEmpty()) { + return Optional.empty(); + } + String rootTypeName = tableFunctionsType.name().toLowerCase(); + rootTypeName = Character.toUpperCase(rootTypeName.charAt(0)) + rootTypeName.substring(1); + // rootTypeName == "Query" or "Subscription" + GraphQLObjectType rootQueryObjectType = GraphQLObjectType.newObject() + .name(rootTypeName) + .fields(fields) + .build(); + + definedTypeNames.add(rootTypeName); + // TODO no more needed ? + // for downstream cleaning invalid types + if (tableFunctionsType == AccessModifier.QUERY) { + this.queryFields.addAll(fields); + } + + return Optional.of(rootQueryObjectType); + } + + + /** + * Create a non-relationship field : + * - a scalar type + * - a nested relDataType (= structured type) (which is no more planed as a table function) and which we recursively process + * + */ + private Optional createRelDataTypeField(RelDataTypeField field) { + return getOutputType(field.getType(), NamePath.of(field.getName()), extendedScalarTypes) + .filter(type -> isValidGraphQLName(field.getName())) + .filter(type -> isVisible(field)) + .map(type -> GraphQLFieldDefinition.newFieldDefinition() + .name(field.getName()) + .type((GraphQLOutputType) wrapNullable(type, field.getType())) + .build() + ); + } + + /** + * For a relationship table function sur as this:
{@code Customer.orders := SELECT * FROM Orders o WHERE
+   * this.customerid = o.customerid; }
Create a type reference for the orders field in the Customer table. + */ + private Optional createRelationshipField(SqrlTableFunction relationship) { + String fieldName = relationship.getFullPath().getLast().getDisplay(); + if (!isValidGraphQLName(fieldName) || isHiddenString(fieldName)) { + return Optional.empty(); + } + + // reference the type that will be defined when the table function relationship is processed + GraphQLFieldDefinition field = GraphQLFieldDefinition.newFieldDefinition() + .name(fieldName) + .type((GraphQLOutputType) wrapMultiplicity(createTypeReference(relationship), relationship.getMultiplicity())) + .arguments(createArguments(relationship)) + .build(); + + return Optional.of(field); + } + + private List createArguments(SqrlTableFunction tableFunction) { + if (tableFunction.getMultiplicity() != Multiplicity.MANY) { + return List.of(); + } + + List parameters = tableFunction.getParameters().stream() + .filter(parameter->!((SqrlFunctionParameter)parameter).isParentField()) + .collect(Collectors.toList()); + + final List parametersArguments = parameters.stream() + .filter(p -> getInputType(p.getType(null), NamePath.of(p.getName()), extendedScalarTypes).isPresent()) + .map(parameter -> GraphQLArgument.newArgument() + .name(parameter.getName()) + .type(nonNull(getInputType(parameter.getType(null), NamePath.of(parameter.getName()), extendedScalarTypes).get())) + .build()).collect(Collectors.toList()); + List limitAndOffsetArguments = List.of(); + if(tableFunction.getVisibility().getAccess() != AccessModifier.SUBSCRIPTION) { + limitAndOffsetArguments = generateLimitAndOffsetArguments(); + } + return ListUtils.union(parametersArguments, limitAndOffsetArguments); + } + + private List generateLimitAndOffsetArguments() { + GraphQLArgument limit = GraphQLArgument.newArgument() + .name(LIMIT) + .type(Scalars.GraphQLInt) + .defaultValueLiteral(IntValue.of(10)) + .build(); + + GraphQLArgument offset = GraphQLArgument.newArgument() + .name(OFFSET) + .type(Scalars.GraphQLInt) + .defaultValueLiteral(IntValue.of(0)) + .build(); + return List.of(limit, offset); + } + + + private boolean isVisible(RelDataTypeField f) { + return !f.getName().startsWith(HIDDEN_PREFIX); + } + + private GraphQLOutputType createTypeReference(SqrlTableFunction tableFunction) { + String typeName = + tableFunction.getFunctionAnalysis().getOptionalBaseTable().isPresent() + ? tableFunction.getBaseTable().getName() + : uniquifyNameForPath(tableFunction.getFullPath()); + return new GraphQLTypeReference(typeName); + } + + + // TODO no more needed ? + public void cleanInvalidTypes() { + // Ensure every field points to a valid type + boolean found; + int attempts = 10; + do { + found = false; + Iterator iterator = objectTypes.iterator(); + List replacedType = new ArrayList<>(); + while (iterator.hasNext()) { + GraphQLObjectType objectType = iterator.next(); + List invalidFields = new ArrayList<>(); + + for (GraphQLFieldDefinition field : objectType.getFields()) { + if (!isValidType(field.getType())) { + invalidFields.add(field); + } + } + + // Refactor to remove invalid fields + List fields = new ArrayList<>(objectType.getFields()); + boolean fieldsRemoved = fields.removeAll(invalidFields); + + // After removing invalid fields, if an object has no fields, it should be removed + if (fields.isEmpty()) { + iterator.remove(); + found = true; + } else if (fieldsRemoved) { + GraphQLObjectType newObjectType = GraphQLObjectType.newObject(objectType).clearFields() + .fields(fields).build(); + replacedType.add(newObjectType); + iterator.remove(); + found = true; + } + } + + //Add new types back + objectTypes.addAll(replacedType); + + found |= queryFields.removeIf(field -> !isValidType(field.getType())); + + // Ensure each object has at least one field + found |= objectTypes.removeIf(objectType -> objectType.getFields().isEmpty()); + } while (found && --attempts != 0); + + if (found) { + throw new RuntimeException("Schema too complexity high, could not be reduced"); + } + } + + boolean isValidType(GraphQLType type) { + type = unbox(type); + // You can expand this logic depending on the intricacies of type validation + if (type instanceof GraphQLTypeReference) { + GraphQLTypeReference typeReference = (GraphQLTypeReference) type; + for (GraphQLObjectType objectType : this.objectTypes) { + if (typeReference.getName().equalsIgnoreCase(objectType.getName())) { + return true; + } + } + } + + return isBaseGraphQLType(type); + } + + private GraphQLType unbox(GraphQLType type) { + if (type instanceof GraphQLNonNull) { + return unbox(((GraphQLNonNull) type).getWrappedType()); + } else if (type instanceof GraphQLList) { + return unbox(((GraphQLList) type).getWrappedType()); + } + return type; + } + + boolean isBaseGraphQLType(GraphQLType type) { + return type instanceof GraphQLScalarType; + } +} diff --git a/sqrl-planner/src/main/java/com/datasqrl/v2/graphql/GraphqlSchemaUtil2.java b/sqrl-planner/src/main/java/com/datasqrl/v2/graphql/GraphqlSchemaUtil2.java new file mode 100644 index 0000000000..3ce14da450 --- /dev/null +++ b/sqrl-planner/src/main/java/com/datasqrl/v2/graphql/GraphqlSchemaUtil2.java @@ -0,0 +1,245 @@ +/* + * Copyright (c) 2021, DataSQRL. All rights reserved. Use is subject to license terms. + */ +package com.datasqrl.v2.graphql; + +import static com.datasqrl.canonicalizer.Name.HIDDEN_PREFIX; +import static com.datasqrl.canonicalizer.Name.isSystemHidden; + +import com.datasqrl.canonicalizer.Name; +import com.datasqrl.canonicalizer.NamePath; +import com.datasqrl.graphql.server.CustomScalars; +import com.datasqrl.json.FlinkJsonType; +import com.datasqrl.schema.Multiplicity; +import graphql.Scalars; +import graphql.schema.GraphQLFieldDefinition; +import graphql.schema.GraphQLInputObjectField; +import graphql.schema.GraphQLInputObjectType; +import graphql.schema.GraphQLInputType; +import graphql.schema.GraphQLList; +import graphql.schema.GraphQLNonNull; +import graphql.schema.GraphQLObjectType; +import graphql.schema.GraphQLOutputType; +import graphql.schema.GraphQLType; +import java.util.Optional; +import java.util.regex.Pattern; + +import lombok.extern.slf4j.Slf4j; +import org.apache.calcite.rel.type.RelDataType; +import org.apache.calcite.rel.type.RelDataTypeField; +import org.apache.flink.table.planner.plan.schema.RawRelDataType; + +@Slf4j +public class GraphqlSchemaUtil2 { + + public static GraphQLType wrapNullable(GraphQLType gqlType, RelDataType type) { + if (!type.isNullable()) { + return GraphQLNonNull.nonNull(gqlType); + } + return gqlType; + } + + public static GraphQLType wrapMultiplicity(GraphQLType type, Multiplicity multiplicity) { + switch (multiplicity) { + case ZERO_ONE: + return type; + case ONE: + return GraphQLNonNull.nonNull(type); + case MANY: + default: + return GraphQLList.list(GraphQLNonNull.nonNull(type)); + } + } + + public static boolean isValidGraphQLName(String name) { + return !isSystemHidden(name) && Pattern.matches("[_A-Za-z][_0-9A-Za-z]*", name); + } + + public static Optional getInputType(RelDataType type, NamePath namePath, boolean extendedScalarTypes) { + return getInOutType(type, namePath, extendedScalarTypes) + .map(f->(GraphQLInputType)f); + } + + public static Optional getOutputType(RelDataType type, NamePath namePath, boolean extendedScalarTypes) { + return getInOutType(type, namePath, extendedScalarTypes) + .map(f->(GraphQLOutputType)f); + } + + public static Optional getInOutType(RelDataType type, NamePath namePath, boolean extendedScalarTypes) { + if (type.getSqlTypeName() == null) { + return Optional.empty(); + } + + switch (type.getSqlTypeName()) { + case OTHER: + if (type instanceof RawRelDataType) { + RawRelDataType rawRelDataType = (RawRelDataType) type; + Class originatingClass = rawRelDataType.getRawType().getOriginatingClass(); + if (originatingClass.isAssignableFrom(FlinkJsonType.class)) { + return Optional.of(CustomScalars.JSON); + } + } + + return Optional.empty(); + case BOOLEAN: + return Optional.of(Scalars.GraphQLBoolean); + case TINYINT: + case SMALLINT: + case INTEGER: + return Optional.of(Scalars.GraphQLInt); + case BIGINT: + if (extendedScalarTypes) { + return Optional.of(CustomScalars.GRAPHQL_BIGINTEGER); + } + case DECIMAL: + case FLOAT: + case REAL: + case DOUBLE: + return Optional.of(Scalars.GraphQLFloat); + case DATE: + return Optional.of(CustomScalars.DATE); + case TIME: + return Optional.of(CustomScalars.TIME); + case TIME_WITH_LOCAL_TIME_ZONE: + case TIMESTAMP: + case TIMESTAMP_WITH_LOCAL_TIME_ZONE: + return Optional.of(CustomScalars.DATETIME); + case INTERVAL_YEAR: + case INTERVAL_YEAR_MONTH: + case INTERVAL_MONTH: + case INTERVAL_DAY: + case INTERVAL_DAY_HOUR: + case INTERVAL_DAY_MINUTE: + case INTERVAL_DAY_SECOND: + case INTERVAL_HOUR: + case INTERVAL_HOUR_MINUTE: + case INTERVAL_HOUR_SECOND: + case INTERVAL_MINUTE: + case INTERVAL_MINUTE_SECOND: + case INTERVAL_SECOND: + case CHAR: + case VARCHAR: + return Optional.of(Scalars.GraphQLString); + // arity many, create a GraphQLList of the component type + case ARRAY: + case MULTISET: + return getOutputType(type.getComponentType(), namePath, extendedScalarTypes).map(GraphQLList::list); + // nested type, arity 1 + case STRUCTURED: + case ROW: + return createGraphQLOutputStructuredType(type, namePath, extendedScalarTypes); + case MAP: + return Optional.of(CustomScalars.JSON); + case BINARY: + case VARBINARY: + case NULL: + case ANY: + case SYMBOL: + case DISTINCT: + case CURSOR: + case COLUMN_LIST: + case DYNAMIC_STAR: + case GEOMETRY: + case SARG: + default: + return Optional.empty(); + } + } + + + //TODO will be used for mutations. Do not remove, inline when coding mutations + public static Optional createOutputTypeForRelDataType(RelDataType type, NamePath namePath, boolean extendedScalarTypes) { + return getOutputType(type, namePath, extendedScalarTypes).map(t -> (GraphQLOutputType) wrapNullable(t, type)); + } + + private static Optional getGraphQLInputType(RelDataType type, NamePath namePath, boolean extendedScalarTypes) { + switch (type.getSqlTypeName()) { + case BOOLEAN: + return Optional.of(Scalars.GraphQLBoolean); + case INTEGER: + case SMALLINT: + case TINYINT: + return Optional.of(Scalars.GraphQLInt); + case BIGINT: + if (extendedScalarTypes) { + return Optional.of(CustomScalars.GRAPHQL_BIGINTEGER); + } + case FLOAT: + case REAL: + case DOUBLE: + case DECIMAL: + return Optional.of(Scalars.GraphQLFloat); + case CHAR: + case VARCHAR: + return Optional.of(Scalars.GraphQLString); + case DATE: + return Optional.of(CustomScalars.DATE); + case TIME: + return Optional.of(CustomScalars.TIME); + case TIMESTAMP: + return Optional.of(CustomScalars.DATETIME); + case BINARY: + case VARBINARY: + return Optional.of(Scalars.GraphQLString); // Typically handled as Base64 encoded strings + case ARRAY: + return type.getComponentType() != null // traverse inner type + ? getGraphQLInputType(type.getComponentType(), namePath, extendedScalarTypes).map(GraphQLList::list) + : Optional.empty(); + case ROW: + return createGraphQLInputStructuredType(type, namePath, extendedScalarTypes); + case MAP: + return Optional.of(CustomScalars.JSON); + default: + return Optional.empty(); // Unsupported types are omitted + } + } + + /** + * Creates a GraphQL input object type for a ROW type. + */ + private static Optional createGraphQLInputStructuredType(RelDataType rowType, NamePath namePath, boolean extendedScalarTypes) { + GraphQLInputObjectType.Builder builder = GraphQLInputObjectType.newInputObject(); + String typeName = uniquifyNameForPath(namePath, "Input"); + builder.name(typeName); + + for (RelDataTypeField field : rowType.getFieldList()) { + final NamePath fieldPath = namePath.concat(Name.system(field.getName())); + if (namePath.getLast().isHidden()) continue; + RelDataType type = field.getType(); + getGraphQLInputType(type, fieldPath, extendedScalarTypes).map(t -> (GraphQLInputType) wrapNullable(t, type)) + .ifPresent(fieldType -> builder.field(GraphQLInputObjectField.newInputObjectField() + .name(field.getName()) + .type(fieldType) + .build())); + } + return Optional.of(builder.build()); + } + + /** + * Creates a GraphQL output object type for a ROW type. + */ + private static Optional createGraphQLOutputStructuredType(RelDataType type, NamePath namePath, boolean extendedScalarTypes) { + GraphQLObjectType.Builder builder = GraphQLObjectType.newObject(); + String typeName = uniquifyNameForPath(namePath, "Output"); + builder.name(typeName); + for (RelDataTypeField field : type.getFieldList()) { + if (field.getName().startsWith(HIDDEN_PREFIX)) continue; + getOutputType(field.getType(), namePath.concat(Name.system(field.getName())), extendedScalarTypes) // recursively traverse + .ifPresent(fieldType -> builder.field(GraphQLFieldDefinition.newFieldDefinition() + .name(field.getName()) + .type((GraphQLOutputType) wrapNullable(fieldType, field.getType())) + .build() + ) + ); + } + return Optional.of(builder.build()); + } + + public static String uniquifyNameForPath(NamePath fullPath) { + return fullPath.toString("_"); + } + + public static String uniquifyNameForPath(NamePath fullPath, String postfix) { + return fullPath.toString("_").concat(postfix); + } +} diff --git a/sqrl-planner/src/main/java/com/datasqrl/v2/graphql/GraphqlSchemaValidator2.java b/sqrl-planner/src/main/java/com/datasqrl/v2/graphql/GraphqlSchemaValidator2.java new file mode 100644 index 0000000000..e2387ee02c --- /dev/null +++ b/sqrl-planner/src/main/java/com/datasqrl/v2/graphql/GraphqlSchemaValidator2.java @@ -0,0 +1,370 @@ +package com.datasqrl.v2.graphql; + +import static com.datasqrl.graphql.server.TypeDefinitionRegistryUtil.getQueryTypeName; +import static com.datasqrl.graphql.server.TypeDefinitionRegistryUtil.getType; +import static com.datasqrl.graphql.util.GraphqlCheckUtil.checkState; +import static com.datasqrl.graphql.util.GraphqlCheckUtil.createThrowable; + +import com.datasqrl.calcite.SqrlFramework; +import com.datasqrl.calcite.function.SqrlTableMacro; +import com.datasqrl.canonicalizer.Name; +import com.datasqrl.canonicalizer.NamePath; +import com.datasqrl.canonicalizer.ReservedName; +import com.datasqrl.error.ErrorCollector; +import com.datasqrl.graphql.APIConnectorManager; +import com.datasqrl.graphql.generate.GraphqlSchemaUtil; +import com.datasqrl.graphql.inference.SchemaWalker; +import com.datasqrl.io.tables.TableSource; +import com.datasqrl.plan.queries.APISource; +import com.google.common.collect.Iterables; +import com.google.inject.Inject; +import graphql.language.EnumTypeDefinition; +import graphql.language.FieldDefinition; +import graphql.language.InputObjectTypeDefinition; +import graphql.language.InputValueDefinition; +import graphql.language.ListType; +import graphql.language.NonNullType; +import graphql.language.ObjectTypeDefinition; +import graphql.language.ScalarTypeDefinition; +import graphql.language.Type; +import graphql.language.TypeDefinition; +import graphql.language.TypeName; +import graphql.schema.idl.SchemaParser; +import graphql.schema.idl.TypeDefinitionRegistry; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; +import org.apache.calcite.jdbc.SqrlSchema; +import org.apache.calcite.rel.type.RelDataType; +import org.apache.calcite.rel.type.RelDataTypeField; +import org.apache.calcite.schema.Function; +import org.apache.calcite.sql.validate.SqlNameMatcher; + +public class GraphqlSchemaValidator2 extends SchemaWalker { + + private final Map visitedObj = new HashMap<>(); + private final ErrorCollector errorCollector; + + @Inject + public GraphqlSchemaValidator2(SqrlFramework framework, APIConnectorManager apiConnectorManager, ErrorCollector errorCollector) { + this(framework.getCatalogReader().nameMatcher(), framework.getSchema(), apiConnectorManager, errorCollector); + } + + public GraphqlSchemaValidator2(SqlNameMatcher nameMatcher, SqrlSchema schema, APIConnectorManager apiManager, ErrorCollector errorCollector) { + super(nameMatcher, schema, apiManager); + this.errorCollector = errorCollector; + } + + @Override + protected void walkSubscription(ObjectTypeDefinition m, FieldDefinition fieldDefinition, + TypeDefinitionRegistry registry, APISource source) { + //Assure they are root tables + Collection functions = schema.getFunctions(fieldDefinition.getName(), false); + checkState(functions.size() == 1, fieldDefinition.getSourceLocation(), + "Cannot overload subscription"); + Function function = Iterables.getOnlyElement(functions); + checkState(function instanceof SqrlTableMacro, fieldDefinition.getSourceLocation(), + "Subscription not a sqrl table"); + + //todo: validate that it is a valid table + + } + + @Override + protected void walkMutation(APISource source, TypeDefinitionRegistry registry, + ObjectTypeDefinition m, FieldDefinition fieldDefinition) { + // Check we've found the mutation + TableSource mutationSink = apiManager.getMutationSource(source, + Name.system(fieldDefinition.getName())); + if (mutationSink == null) { +// throw createThrowable(fieldDefinition.getSourceLocation(), +// "Could not find mutation source: %s.", fieldDefinition.getName()); + } + + validateStructurallyEqualMutation(fieldDefinition, getValidMutationReturnType(fieldDefinition, registry), + getValidMutationInput(fieldDefinition, registry), + List.of(ReservedName.MUTATION_TIME.getCanonical(), ReservedName.MUTATION_PRIMARY_KEY.getDisplay()), registry); + + } + + private Object validateStructurallyEqualMutation(FieldDefinition fieldDefinition, + ObjectTypeDefinition returnTypeDefinition, InputObjectTypeDefinition inputType, + List allowedFieldNames, TypeDefinitionRegistry registry) { + + //The return type can have event_time + for (FieldDefinition returnTypeFieldDefinition : returnTypeDefinition.getFieldDefinitions()) { + if (allowedFieldNames.contains(returnTypeFieldDefinition.getName())) { + continue; + } + + String name = returnTypeFieldDefinition.getName(); + InputValueDefinition inputDefinition = findExactlyOneInputValue(fieldDefinition, name, + inputType.getInputValueDefinitions()); + + //validate type structurally equal + validateStructurallyEqualMutation(returnTypeFieldDefinition, inputDefinition, registry); + } + + return null; + } + + private void validateStructurallyEqualMutation(FieldDefinition fieldDefinition, + InputValueDefinition inputDefinition, TypeDefinitionRegistry registry) { + checkState(fieldDefinition.getName().equals(inputDefinition.getName()), + fieldDefinition.getSourceLocation(), "Name must be equal to the input name {} {}", + fieldDefinition.getName(), inputDefinition.getName()); + Type definitionType = fieldDefinition.getType(); + Type inputType = inputDefinition.getType(); + + validateStructurallyType(fieldDefinition, definitionType, inputType, registry); + } + + private Object validateStructurallyType(FieldDefinition field, Type definitionType, + Type inputType, TypeDefinitionRegistry registry) { + if (inputType instanceof NonNullType) { + //subType may be nullable if type is non-null + NonNullType nonNullType = (NonNullType) inputType; + if (definitionType instanceof NonNullType) { + NonNullType nonNullDefinitionType = (NonNullType) definitionType; + return validateStructurallyType(field, nonNullDefinitionType.getType(), + nonNullType.getType(), registry); + } else { + return validateStructurallyType(field, definitionType, nonNullType.getType(), registry); + } + } else if (inputType instanceof ListType) { + //subType must be a list + checkState(definitionType instanceof ListType, definitionType.getSourceLocation(), + "List type mismatch for field. Must match the input type. " + field.getName()); + ListType inputListType = (ListType) inputType; + ListType definitionListType = (ListType) definitionType; + return validateStructurallyType(field, definitionListType.getType(), inputListType.getType(), registry); + } else if (inputType instanceof TypeName) { + //If subtype nonnull then it could return errors + checkState(!(definitionType instanceof NonNullType), definitionType.getSourceLocation(), + "Non-null found on field %s, could result in errors if input type is null", + field.getName()); + checkState(!(definitionType instanceof ListType), definitionType.getSourceLocation(), + "List type found on field %s when the input is a scalar type", field.getName()); + + //If typeName, resolve then + TypeName inputTypeName = (TypeName) inputType; + TypeName defTypeName = (TypeName) unboxNonNull(definitionType); + TypeDefinition inputTypeDef = registry.getType(inputTypeName).orElseThrow( + () -> createThrowable(inputTypeName.getSourceLocation(), "Could not find type: %s", + inputTypeName.getName())); + TypeDefinition defTypeDef = registry.getType(defTypeName).orElseThrow( + () -> createThrowable(defTypeName.getSourceLocation(), "Could not find type: %s", + defTypeName.getName())); + + //If input or scalar + if (inputTypeDef instanceof ScalarTypeDefinition) { + checkState(defTypeDef instanceof ScalarTypeDefinition && inputTypeDef.getName() + .equals(defTypeDef.getName()), field.getSourceLocation(), + "Scalar types not matching for field [%s]: found %s but wanted %s", field.getName(), + inputTypeDef.getName(), defTypeDef.getName()); + return null; + } else if (inputTypeDef instanceof EnumTypeDefinition) { + checkState(defTypeDef instanceof EnumTypeDefinition + || defTypeDef instanceof ScalarTypeDefinition && inputTypeDef.getName() + .equals(defTypeDef.getName()), field.getSourceLocation(), + "Enum types not matching for field [%s]: found %s but wanted %s", field.getName(), + inputTypeDef.getName(), defTypeDef.getName()); + return null; + } else if (inputTypeDef instanceof InputObjectTypeDefinition) { + checkState(defTypeDef instanceof ObjectTypeDefinition, field.getSourceLocation(), + "Return object type must match with an input object type not matching for field [%s]: found %s but wanted %s", + field.getName(), inputTypeDef.getName(), defTypeDef.getName()); + ObjectTypeDefinition objectDefinition = (ObjectTypeDefinition) defTypeDef; + InputObjectTypeDefinition inputDefinition = (InputObjectTypeDefinition) inputTypeDef; + return validateStructurallyEqualMutation(field, objectDefinition, inputDefinition, + List.of(), registry); + } else { + throw createThrowable(inputTypeDef.getSourceLocation(), "Unknown type encountered: %s", + inputTypeDef.getName()); + } + } + + throw createThrowable(field.getSourceLocation(), "Unknown type encountered for field: %s", + field.getName()); + } + + private InputValueDefinition findExactlyOneInputValue(FieldDefinition fieldDefinition, + String name, List inputValueDefinitions) { + InputValueDefinition found = null; + for (InputValueDefinition inputDefinition : inputValueDefinitions) { + if (inputDefinition.getName().equals(name)) { + checkState(found == null, inputDefinition.getSourceLocation(), "Duplicate fields found"); + found = inputDefinition; + } + } + + checkState(found != null, fieldDefinition.getSourceLocation(), + "Could not find field %s in type %s", name, fieldDefinition.getName()); + + return found; + } + + private InputObjectTypeDefinition getValidMutationInput(FieldDefinition fieldDefinition, TypeDefinitionRegistry registry) { + checkState(!(fieldDefinition.getInputValueDefinitions().isEmpty()), + fieldDefinition.getSourceLocation(), fieldDefinition.getName() + + " has too few arguments. Must have one non-null input type argument."); + checkState(fieldDefinition.getInputValueDefinitions().size() == 1, + fieldDefinition.getSourceLocation(), fieldDefinition.getName() + + " has too many arguments. Must have one non-null input type argument."); + checkState(fieldDefinition.getInputValueDefinitions().get(0).getType() instanceof NonNullType, + fieldDefinition.getSourceLocation(), + "[" + fieldDefinition.getName() + "] " + fieldDefinition.getInputValueDefinitions().get(0) + .getName() + "Must be non-null."); + NonNullType nonNullType = (NonNullType) fieldDefinition.getInputValueDefinitions().get(0) + .getType(); + checkState(nonNullType.getType() instanceof TypeName, fieldDefinition.getSourceLocation(), + "Must be a singular value"); + TypeName name = (TypeName) nonNullType.getType(); + + Optional typeDef = registry.getType(name); + checkState(typeDef.isPresent(), fieldDefinition.getSourceLocation(), + "Could not find input type:" + name.getName()); + checkState(typeDef.get() instanceof InputObjectTypeDefinition, + fieldDefinition.getSourceLocation(), + "Input must be an input object type:" + fieldDefinition.getName()); + + return (InputObjectTypeDefinition) typeDef.get(); + } + + + private ObjectTypeDefinition getValidMutationReturnType(FieldDefinition fieldDefinition, TypeDefinitionRegistry registry) { + Type type = fieldDefinition.getType(); + if (type instanceof NonNullType) { + type = ((NonNullType) type).getType(); + } + + checkState(type instanceof TypeName, type.getSourceLocation(), + "[%s] must be a singular return value", fieldDefinition.getName()); + TypeName name = (TypeName) type; + + TypeDefinition typeDef = registry.getType(name).orElseThrow( + () -> createThrowable(name.getSourceLocation(), "Could not find return type: %s", name.getName())); + checkState(typeDef instanceof ObjectTypeDefinition, typeDef.getSourceLocation(), + "Return must be an object type: %s", fieldDefinition.getName()); + + return (ObjectTypeDefinition) typeDef; + } + + @Override + protected void visitUnknownObject(ObjectTypeDefinition type, FieldDefinition field, NamePath path, + Optional rel) { + throw createThrowable(field.getSourceLocation(), "Unknown field at location %s", rel.map( + r -> field.getName() + ". Possible scalars are [" + r.getFieldNames().stream() + .filter(GraphqlSchemaUtil::isValidGraphQLName).collect(Collectors.joining(", ")) + "]") + .orElse(field.getName())); + } + + @Override + protected void visitScalar(ObjectTypeDefinition type, FieldDefinition field, NamePath path, + RelDataType relDataType, RelDataTypeField relDataTypeField) { + } + + @Override + protected void visitQuery(ObjectTypeDefinition parentType, ObjectTypeDefinition type, + FieldDefinition field, NamePath path, Optional rel, + List functions) { + checkState(!functions.isEmpty(), field.getSourceLocation(), "Could not find functions"); + checkValidArrayNonNullType(field.getType()); + +// if (visitedObj.get(type) != null && !visitedObj.get(type).getIsTypeOf() +// .isEmpty()) { + //todo readd the check to see if we can share a type +// if (!sqrlTable.getIsTypeOf() +// .contains(visitedObj.get(objectDefinition).getIsTypeOf().get(0))) { +// checkState(visitedObj.get(parentType) == null, +// || visitedObj.get(objectDefinition) == sqrlTable, +// field.getSourceLocation(), +// "Cannot redefine a type to point to a different SQRL table. Use an interface instead.\n" +// + "The graphql field [%s] points to Sqrl table [%s] but already had [%s].", +// parentType.getName() + ":" + field.getName(), +// functions.get(0).getFullPath().getDisplay(), +// visitedObj.get(parentType) == null ? null : visitedObj.get(parentType).getFullPath().getDisplay()); +// } +// } + visitedObj.put(parentType, functions.get(0)); + + //todo: better structural checking +// walkChildren((ObjectTypeDefinition) type, functions.get(0), field); +// List invalidFields = getInvalidFields(typeDef, table); +// boolean structurallyEqual = structurallyEqual(typeDef, table); +// //todo clean up, add lazy evaluation +// checkState(structurallyEqual, invalidFields.isEmpty() ? typeDef.getSourceLocation() +// : invalidFields.get(invalidFields.size() - 1).getSourceLocation(), +// "Field(s) [%s] could not be found on type [%s]. Possible fields are: [%s]", String.join(",", +// invalidFields.stream().map(FieldDefinition::getName).collect(Collectors.toList())), +// typeDef.getName(), String.join(", ", table.tableMacro.getRowType().getFieldNames())); + } + + + private void checkValidArrayNonNullType(Type type) { + Type root = type; + if (type instanceof NonNullType) { + type = ((NonNullType) type).getType(); + } + if (type instanceof ListType) { + type = ((ListType) type).getType(); + } + if (type instanceof NonNullType) { + type = ((NonNullType) type).getType(); + } + checkState(type instanceof TypeName, root.getSourceLocation(), + "Type must be a non-null array, array, or non-null"); + } + +// private boolean structurallyEqual(ImplementingTypeDefinition typeDef, SQRLTable table) { +// return typeDef.getFieldDefinitions().stream() +// .allMatch(f -> table.getField(Name.system(((NamedNode) f).getName())).isPresent()); +// } +// +// private List getInvalidFields(ObjectTypeDefinition typeDef, SQRLTable table) { +// return typeDef.getFieldDefinitions().stream() +// .filter(f -> table.getField(Name.system(f.getName())).isEmpty()) +// .collect(Collectors.toList()); +// } + + + private TypeDefinition unwrapObjectType(Type type, TypeDefinitionRegistry registry) { + //type can be in a single array with any non-nulls, e.g. [customer!]! + type = unboxNonNull(type); + if (type instanceof ListType) { + type = ((ListType) type).getType(); + } + type = unboxNonNull(type); + + Optional typeDef = registry.getType(type); + + checkState(typeDef.isPresent(), type.getSourceLocation(), "Could not find Object type [%s]", + type instanceof TypeName ? ((TypeName) type).getName() : type.toString()); + + return typeDef.get(); + } + + private Type unboxNonNull(Type type) { + if (type instanceof NonNullType) { + return unboxNonNull(((NonNullType) type).getType()); + } + return type; + } + + public void validate(APISource source) { + try { + TypeDefinitionRegistry registry = (new SchemaParser()).parse(source.getSchemaDefinition()); + Optional queryType = getType(registry, () -> getQueryTypeName(registry)); + if (queryType.isEmpty()) { + throw createThrowable(null, "Cannot find graphql Query type"); + } + + walk(source); + } catch (Exception e) { + throw errorCollector.handle(e); + } + } +} diff --git a/sqrl-planner/src/main/java/com/datasqrl/v2/graphql/InferGraphqlSchema2.java b/sqrl-planner/src/main/java/com/datasqrl/v2/graphql/InferGraphqlSchema2.java new file mode 100644 index 0000000000..2547ac5d95 --- /dev/null +++ b/sqrl-planner/src/main/java/com/datasqrl/v2/graphql/InferGraphqlSchema2.java @@ -0,0 +1,77 @@ +package com.datasqrl.v2.graphql; + +import com.datasqrl.calcite.SqrlFramework; +import com.datasqrl.canonicalizer.NameCanonicalizer; +import com.datasqrl.engine.pipeline.ExecutionPipeline; +import com.datasqrl.engine.server.ServerPhysicalPlan; +import com.datasqrl.error.ErrorCollector; +import com.datasqrl.graphql.APIConnectorManager; +import com.datasqrl.graphql.GraphqlSchemaParser; +import com.datasqrl.graphql.inference.GraphqlQueryBuilder; +import com.datasqrl.graphql.inference.GraphqlQueryGenerator; +import com.datasqrl.plan.queries.APISource; +import com.datasqrl.plan.queries.APISubscription; +import com.datasqrl.util.SqlNameUtil; +import com.google.inject.Inject; +import graphql.schema.GraphQLSchema; +import graphql.schema.GraphqlTypeComparatorRegistry; +import graphql.schema.idl.SchemaPrinter; + +import java.util.*; +import lombok.AllArgsConstructor; +import lombok.SneakyThrows; + +/** + * Creates new table functions from the GraphQL schema. + */ +@AllArgsConstructor(onConstructor_ = @Inject) +public class InferGraphqlSchema2 { + + private final ExecutionPipeline pipeline; + private final SqrlFramework framework; + private final ErrorCollector errorCollector; + private final APIConnectorManager apiManager; + private final GraphqlSchemaFactory2 graphqlSchemaFactory; + private final GraphqlSchemaParser parser; + + @SneakyThrows + public Optional inferGraphQLSchema(ServerPhysicalPlan serverPlan) { + Optional gqlSchema = graphqlSchemaFactory.generate(serverPlan); + + SchemaPrinter.Options opts = SchemaPrinter.Options.defaultOptions() + .setComparators(GraphqlTypeComparatorRegistry.AS_IS_REGISTRY) + .includeDirectives(false); + + return gqlSchema.map(schema -> new SchemaPrinter(opts).print(schema)); + } + + + private ErrorCollector createErrorCollectorWithSchema(APISource apiSource) { + return errorCollector.withSchema(apiSource.getName().getDisplay(), apiSource.getSchemaDefinition()); + } + + // Validates the schema and generates queries and subscriptions + public void validateAndGenerateQueries(APISource apiSource) { + GraphqlSchemaValidator2 schemaValidator = new GraphqlSchemaValidator2(framework, apiManager, createErrorCollectorWithSchema(apiSource)); + schemaValidator.validate(apiSource); + + GraphqlQueryGenerator queryGenerator = new GraphqlQueryGenerator( + framework.getCatalogReader().nameMatcher(), + framework.getSchema(), + new GraphqlQueryBuilder(framework, apiManager, new SqlNameUtil(NameCanonicalizer.SYSTEM)), + apiManager + ); + + queryGenerator.walk(apiSource); + + // Add queries to apiManager + queryGenerator.getQueries().forEach(apiManager::addQuery); + + // Add subscriptions to apiManager + final APISource source = apiSource; + queryGenerator.getSubscriptions().forEach(subscription -> + apiManager.addSubscription( + new APISubscription(subscription.getAbsolutePath().getFirst(), source), subscription) + ); + } +} diff --git a/sqrl-planner/src/test/java/com/datasqrl/calcite/DataTypeTest.java b/sqrl-planner/src/test/java/com/datasqrl/calcite/DataTypeTest.java new file mode 100644 index 0000000000..e69de29bb2 diff --git a/sqrl-server/sqrl-server-core/pom.xml b/sqrl-server/sqrl-server-core/pom.xml index e9d74b210a..c0ad09cda4 100644 --- a/sqrl-server/sqrl-server-core/pom.xml +++ b/sqrl-server/sqrl-server-core/pom.xml @@ -32,7 +32,6 @@ com.graphql-java graphql-java-extended-scalars - 19.1 diff --git a/sqrl-server/sqrl-server-core/src/main/java/com/datasqrl/graphql/server/CustomScalars.java b/sqrl-server/sqrl-server-core/src/main/java/com/datasqrl/graphql/server/CustomScalars.java index ebca23cb06..bca054ff76 100644 --- a/sqrl-server/sqrl-server-core/src/main/java/com/datasqrl/graphql/server/CustomScalars.java +++ b/sqrl-server/sqrl-server-core/src/main/java/com/datasqrl/graphql/server/CustomScalars.java @@ -45,4 +45,5 @@ public Object parseLiteral(Object input) { public static final GraphQLScalarType DATE = ExtendedScalars.Date; public static final GraphQLScalarType TIME = ExtendedScalars.LocalTime; public static final GraphQLScalarType JSON = ExtendedScalars.Json; + public static final GraphQLScalarType GRAPHQL_BIGINTEGER = ExtendedScalars.GraphQLBigInteger.transform(builder->builder.name("GraphQLBigInteger")); } \ No newline at end of file diff --git a/sqrl-server/sqrl-server-core/src/main/java/com/datasqrl/graphql/server/GraphQLEngineBuilder.java b/sqrl-server/sqrl-server-core/src/main/java/com/datasqrl/graphql/server/GraphQLEngineBuilder.java index ad1f4c5527..4ac7179144 100644 --- a/sqrl-server/sqrl-server-core/src/main/java/com/datasqrl/graphql/server/GraphQLEngineBuilder.java +++ b/sqrl-server/sqrl-server-core/src/main/java/com/datasqrl/graphql/server/GraphQLEngineBuilder.java @@ -59,7 +59,7 @@ public class GraphQLEngineBuilder implements QueryBaseVisitor, ResolvedQueryVisitor { - private final List addlTypes; + private final List extendedScalarTypes; private final SubscriptionConfiguration> subscriptionConfiguration; private final MutationConfiguration> mutationConfiguration; @@ -72,18 +72,18 @@ public class GraphQLEngineBuilder implements .build(); private GraphQLEngineBuilder(Builder builder) { - this.addlTypes = builder.addlTypes; + this.extendedScalarTypes = builder.extendedScalarTypes; this.subscriptionConfiguration = builder.subscriptionConfiguration; this.mutationConfiguration = builder.mutationConfiguration; } public static class Builder { - private List addlTypes = new ArrayList<>(); + private List extendedScalarTypes = new ArrayList<>(); private SubscriptionConfiguration> subscriptionConfiguration; private MutationConfiguration> mutationConfiguration; - public Builder withAdditionalTypes(List types) { - this.addlTypes = types; + public Builder withExtendedScalarTypes(List types) { + this.extendedScalarTypes = types; return this; } @@ -157,7 +157,7 @@ private RuntimeWiring createWiring(TypeDefinitionRegistry registry, GraphQLCodeR .scalar(CustomScalars.JSON) ; - addlTypes.forEach(t->wiring.scalar(t)); + extendedScalarTypes.forEach(t->wiring.scalar(t)); for (Map.Entry typeEntry : registry.types().entrySet()) { if (typeEntry.getValue() instanceof InterfaceTypeDefinition) { diff --git a/sqrl-server/sqrl-server-vertx/src/main/java/com/datasqrl/graphql/GraphQLServer.java b/sqrl-server/sqrl-server-vertx/src/main/java/com/datasqrl/graphql/GraphQLServer.java index e9c3e63ecc..696b152bd3 100644 --- a/sqrl-server/sqrl-server-vertx/src/main/java/com/datasqrl/graphql/GraphQLServer.java +++ b/sqrl-server/sqrl-server-vertx/src/main/java/com/datasqrl/graphql/GraphQLServer.java @@ -6,9 +6,9 @@ import com.datasqrl.canonicalizer.NameCanonicalizer; import com.datasqrl.graphql.config.CorsHandlerOptions; import com.datasqrl.graphql.config.ServerConfig; +import com.datasqrl.graphql.server.CustomScalars; import com.datasqrl.graphql.server.GraphQLEngineBuilder; import com.datasqrl.graphql.server.RootGraphqlModel; -import com.datasqrl.graphql.type.SqrlVertxScalars; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.module.SimpleModule; import com.google.common.base.Strings; @@ -331,8 +331,8 @@ public GraphQL createGraphQL(Map client, Promise startP .withMutationConfiguration( new MutationConfigurationImpl(model, vertx, config)) .withSubscriptionConfiguration( - new SubscriptionConfigurationImpl(model, vertx, config, startPromise, vertxJdbcClient) - ) + new SubscriptionConfigurationImpl(model, vertx, config, startPromise, vertxJdbcClient)) + .withExtendedScalarTypes(List.of(CustomScalars.GRAPHQL_BIGINTEGER)) .build(), new VertxContext(vertxJdbcClient, canonicalizer)); MeterRegistry meterRegistry = BackendRegistries.getDefaultNow(); diff --git a/sqrl-testing/sqrl-integration-tests/src/test/resources/dagplanner/importsWithNestedType.sqrl b/sqrl-testing/sqrl-integration-tests/src/test/resources/dagplanner/importsWithNestedType.sqrl new file mode 100644 index 0000000000..993de36a1d --- /dev/null +++ b/sqrl-testing/sqrl-integration-tests/src/test/resources/dagplanner/importsWithNestedType.sqrl @@ -0,0 +1,3 @@ +IMPORT ecommerceTs.Orders; + + diff --git a/sqrl-testing/sqrl-integration-tests/src/test/resources/dagplanner/relationship-fail.sqrl b/sqrl-testing/sqrl-integration-tests/src/test/resources/dagplanner/relationship-fail.sqrl new file mode 100644 index 0000000000..a8b84791b0 --- /dev/null +++ b/sqrl-testing/sqrl-integration-tests/src/test/resources/dagplanner/relationship-fail.sqrl @@ -0,0 +1,13 @@ +IMPORT ecommerceTs.Customer; --base table = Customer +-- Orders has nested data `entries` (see orders.schema.yml) +IMPORT ecommerceTs.Orders; --base table = Orders + +rootFunction($i : INT) := SELECT id FROM Orders WHERE id = $i; -- no base table +rootFunctionWithBaseTable($i : INT) := SELECT * FROM Orders WHERE id = $i; -- base table = Orders + +-- A relationship, `this.customerid` gets replaced with an internal parameter that gets substituted with customerid on the parent table Customer +Customer.orders := SELECT * FROM Orders o WHERE this.customerid = o.customerid; --base table +Customer.orders2 := SELECT id FROM Orders o WHERE this.customerid = o.customerid; --no base table + +-- cannot have a relationship in the FROM clause. +-- CustomerEntries($customerid : BIGINT) := SELECT entries e FROM Customer.orders customerOrders WHERE customerOrders.customerid = $customerid; \ No newline at end of file diff --git a/sqrl-testing/sqrl-integration-tests/src/test/resources/dagplanner/relationship-subscribe.sqrl b/sqrl-testing/sqrl-integration-tests/src/test/resources/dagplanner/relationship-subscribe.sqrl new file mode 100644 index 0000000000..10a75c773e --- /dev/null +++ b/sqrl-testing/sqrl-integration-tests/src/test/resources/dagplanner/relationship-subscribe.sqrl @@ -0,0 +1,15 @@ +IMPORT ecommerceTs.Customer; --base table = Customer +-- Orders has nested data `entries` (see orders.schema.yml) +IMPORT ecommerceTs.Orders; --base table = Orders + +-- projections are not supported on kafka engine for subscriptions +rootFunction($i : INT) := SELECT id FROM Orders WHERE id = $i; -- no base table +rootFunctionWithBaseTable($i : INT) := SUBSCRIBE SELECT * FROM Orders WHERE id = $i; -- base table = Orders +rootFunctionWithBaseTable2($i : INT) := SELECT * FROM Orders WHERE id = $i; -- base table = Orders + +-- A relationship, `this.customerid` gets replaced with an internal parameter that gets substituted with customerid on the parent table Customer +Customer.orders := SELECT * FROM Orders o WHERE this.customerid = o.customerid; --base table +Customer.orders2($i : INT) := SELECT id FROM Orders o WHERE this.customerid = o.customerid AND id = $i; --no base table + +-- cannot have a relationship in the FROM clause. +-- CustomerEntries($customerid : BIGINT) := SELECT entries e FROM Customer.orders customerOrders WHERE customerOrders.customerid = $customerid; \ No newline at end of file diff --git a/sqrl-testing/sqrl-integration-tests/src/test/resources/dagplanner/relationship.sqrl b/sqrl-testing/sqrl-integration-tests/src/test/resources/dagplanner/relationship.sqrl new file mode 100644 index 0000000000..c9df8317d9 --- /dev/null +++ b/sqrl-testing/sqrl-integration-tests/src/test/resources/dagplanner/relationship.sqrl @@ -0,0 +1,13 @@ +IMPORT ecommerceTs.Customer; --base table = Customer +-- Orders has nested data `entries` (see orders.schema.yml) +IMPORT ecommerceTs.Orders; --base table = Orders + +rootFunction($i : INT) := SELECT id FROM Orders WHERE id = $i; -- no base table +rootFunctionWithBaseTable($i : INT) := SELECT * FROM Orders WHERE id = $i; -- base table = Orders + +-- A relationship, `this.customerid` gets replaced with an internal parameter that gets substituted with customerid on the parent table Customer +Customer.orders := SELECT * FROM Orders o WHERE this.customerid = o.customerid; --base table +Customer.orders2($i : INT) := SELECT id FROM Orders o WHERE this.customerid = o.customerid AND id = $i; --no base table + +-- cannot have a relationship in the FROM clause. +-- CustomerEntries($customerid : BIGINT) := SELECT entries e FROM Customer.orders customerOrders WHERE customerOrders.customerid = $customerid; \ No newline at end of file diff --git a/sqrl-testing/sqrl-integration-tests/src/test/resources/snapshots/com/datasqrl/UseCaseCompileTest/simple-with-bigint-support--package.txt b/sqrl-testing/sqrl-integration-tests/src/test/resources/snapshots/com/datasqrl/UseCaseCompileTest/simple-with-bigint-support--package.txt new file mode 100644 index 0000000000..6a10fdace4 --- /dev/null +++ b/sqrl-testing/sqrl-integration-tests/src/test/resources/snapshots/com/datasqrl/UseCaseCompileTest/simple-with-bigint-support--package.txt @@ -0,0 +1,397 @@ +>>>pipeline_explain.txt +=== Orders +ID: orders_2 +Type: stream +Stage: flink +Primary Key: id, customerid, time +Timestamp : _ingest_time +Schema: + - id: BIGINT NOT NULL + - customerid: BIGINT NOT NULL + - time: TIMESTAMP_WITH_LOCAL_TIME_ZONE(3) NOT NULL + - productid: BIGINT NOT NULL + - quantity: BIGINT NOT NULL + - unit_price: DOUBLE + - discount: DOUBLE + - _ingest_time: TIMESTAMP_LTZ(3) *PROCTIME* NOT NULL +Plan: +LogicalTableScan(table=[[orders_1]]) + +>>>flink.json +{ + "flinkSql" : [ + "CREATE TEMPORARY TABLE `orders_1` (\n `id` BIGINT NOT NULL,\n `customerid` BIGINT NOT NULL,\n `time` TIMESTAMP(3) WITH LOCAL TIME ZONE NOT NULL,\n `productid` BIGINT NOT NULL,\n `quantity` BIGINT NOT NULL,\n `unit_price` DOUBLE,\n `discount` DOUBLE,\n `_ingest_time` AS PROCTIME(),\n PRIMARY KEY (`id`, `customerid`, `time`) NOT ENFORCED\n) WITH (\n 'format' = 'flexible-json',\n 'path' = '${DATA_PATH}/orders.jsonl',\n 'source.monitor-interval' = '10000',\n 'connector' = 'filesystem'\n);", + "CREATE TEMPORARY TABLE `orders_2` (\n `id` BIGINT NOT NULL,\n `customerid` BIGINT NOT NULL,\n `time` TIMESTAMP(3) WITH LOCAL TIME ZONE NOT NULL,\n `productid` BIGINT NOT NULL,\n `quantity` BIGINT NOT NULL,\n `unit_price` DOUBLE,\n `discount` DOUBLE,\n `_ingest_time` TIMESTAMP(3) WITH LOCAL TIME ZONE NOT NULL,\n PRIMARY KEY (`id`, `customerid`, `time`) NOT ENFORCED\n) WITH (\n 'password' = '${JDBC_PASSWORD}',\n 'connector' = 'jdbc-sqrl',\n 'driver' = 'org.postgresql.Driver',\n 'table-name' = 'orders_2',\n 'url' = '${JDBC_URL}',\n 'username' = '${JDBC_USERNAME}'\n);", + "CREATE VIEW `table$1`\nAS\nSELECT *\nFROM `orders_1`;", + "EXECUTE STATEMENT SET BEGIN\nINSERT INTO `orders_2`\n(SELECT *\n FROM `table$1`)\n;\nEND;" + ], + "connectors" : [ + "jdbc-sqrl", + "filesystem" + ], + "formats" : [ + "flexible-json" + ] +} +>>>kafka.json +{ + "topics" : [ ] +} +>>>postgres.json +{ + "ddl" : [ + { + "name" : "orders_2", + "columns" : [ + "\"id\" BIGINT NOT NULL", + "\"customerid\" BIGINT NOT NULL", + "\"time\" TIMESTAMP WITH TIME ZONE NOT NULL", + "\"productid\" BIGINT NOT NULL", + "\"quantity\" BIGINT NOT NULL", + "\"unit_price\" DOUBLE PRECISION ", + "\"discount\" DOUBLE PRECISION ", + "\"_ingest_time\" TIMESTAMP WITH TIME ZONE NOT NULL" + ], + "primaryKeys" : [ + "\"id\"", + "\"customerid\"", + "\"time\"" + ], + "sql" : "CREATE TABLE IF NOT EXISTS orders_2 (\"id\" BIGINT NOT NULL,\"customerid\" BIGINT NOT NULL,\"time\" TIMESTAMP WITH TIME ZONE NOT NULL,\"productid\" BIGINT NOT NULL,\"quantity\" BIGINT NOT NULL,\"unit_price\" DOUBLE PRECISION ,\"discount\" DOUBLE PRECISION ,\"_ingest_time\" TIMESTAMP WITH TIME ZONE NOT NULL , PRIMARY KEY (\"id\",\"customerid\",\"time\"));" + }, + { + "indexName" : "orders_2_btree_c1c2", + "tableName" : "orders_2", + "columns" : [ + "customerid", + "time" + ], + "type" : "BTREE", + "sql" : "CREATE INDEX IF NOT EXISTS orders_2_btree_c1c2 ON orders_2 USING btree (\"customerid\",\"time\");" + }, + { + "indexName" : "orders_2_btree_c2c0", + "tableName" : "orders_2", + "columns" : [ + "time", + "id" + ], + "type" : "BTREE", + "sql" : "CREATE INDEX IF NOT EXISTS orders_2_btree_c2c0 ON orders_2 USING btree (\"time\",\"id\");" + } + ], + "views" : [ + { + "name" : "Orders", + "sql" : "CREATE OR REPLACE VIEW \"Orders\"(\"id\", \"customerid\", \"time\", \"productid\", \"quantity\", \"unit_price\", \"discount\", \"_ingest_time\") AS SELECT *\nFROM \"orders_2\"\nORDER BY \"_ingest_time\" DESC NULLS LAST, \"id\", \"customerid\", \"time\";" + } + ] +} +>>>vertx.json +{ + "model" : { + "coords" : [ + { + "type" : "args", + "parentType" : "Query", + "fieldName" : "Orders", + "matchs" : [ + { + "arguments" : [ + { + "type" : "variable", + "type" : "variable", + "path" : "time" + }, + { + "type" : "variable", + "type" : "variable", + "path" : "limit" + }, + { + "type" : "variable", + "type" : "variable", + "path" : "offset" + } + ], + "query" : { + "type" : "PagedJdbcQuery", + "type" : "PagedJdbcQuery", + "sql" : "SELECT *\nFROM \"orders_2\"\nWHERE \"time\" = $1\nORDER BY \"_ingest_time\" DESC NULLS LAST, \"id\", \"customerid\"", + "parameters" : [ + { + "type" : "arg", + "type" : "arg", + "path" : "time" + } + ] + } + }, + { + "arguments" : [ + { + "type" : "variable", + "type" : "variable", + "path" : "limit" + }, + { + "type" : "variable", + "type" : "variable", + "path" : "offset" + } + ], + "query" : { + "type" : "PagedJdbcQuery", + "type" : "PagedJdbcQuery", + "sql" : "SELECT *\nFROM \"orders_2\"\nORDER BY \"_ingest_time\" DESC NULLS LAST, \"id\", \"customerid\", \"time\"", + "parameters" : [ ] + } + }, + { + "arguments" : [ + { + "type" : "variable", + "type" : "variable", + "path" : "id" + }, + { + "type" : "variable", + "type" : "variable", + "path" : "time" + }, + { + "type" : "variable", + "type" : "variable", + "path" : "limit" + }, + { + "type" : "variable", + "type" : "variable", + "path" : "offset" + } + ], + "query" : { + "type" : "PagedJdbcQuery", + "type" : "PagedJdbcQuery", + "sql" : "SELECT *\nFROM \"orders_2\"\nWHERE \"id\" = $1 AND \"time\" = $2\nORDER BY \"_ingest_time\" DESC NULLS LAST, \"customerid\"", + "parameters" : [ + { + "type" : "arg", + "type" : "arg", + "path" : "id" + }, + { + "type" : "arg", + "type" : "arg", + "path" : "time" + } + ] + } + }, + { + "arguments" : [ + { + "type" : "variable", + "type" : "variable", + "path" : "id" + }, + { + "type" : "variable", + "type" : "variable", + "path" : "limit" + }, + { + "type" : "variable", + "type" : "variable", + "path" : "offset" + } + ], + "query" : { + "type" : "PagedJdbcQuery", + "type" : "PagedJdbcQuery", + "sql" : "SELECT *\nFROM \"orders_2\"\nWHERE \"id\" = $1\nORDER BY \"_ingest_time\" DESC NULLS LAST, \"customerid\", \"time\"", + "parameters" : [ + { + "type" : "arg", + "type" : "arg", + "path" : "id" + } + ] + } + }, + { + "arguments" : [ + { + "type" : "variable", + "type" : "variable", + "path" : "customerid" + }, + { + "type" : "variable", + "type" : "variable", + "path" : "time" + }, + { + "type" : "variable", + "type" : "variable", + "path" : "limit" + }, + { + "type" : "variable", + "type" : "variable", + "path" : "offset" + } + ], + "query" : { + "type" : "PagedJdbcQuery", + "type" : "PagedJdbcQuery", + "sql" : "SELECT *\nFROM \"orders_2\"\nWHERE \"customerid\" = $1 AND \"time\" = $2\nORDER BY \"_ingest_time\" DESC NULLS LAST, \"id\"", + "parameters" : [ + { + "type" : "arg", + "type" : "arg", + "path" : "customerid" + }, + { + "type" : "arg", + "type" : "arg", + "path" : "time" + } + ] + } + }, + { + "arguments" : [ + { + "type" : "variable", + "type" : "variable", + "path" : "customerid" + }, + { + "type" : "variable", + "type" : "variable", + "path" : "limit" + }, + { + "type" : "variable", + "type" : "variable", + "path" : "offset" + } + ], + "query" : { + "type" : "PagedJdbcQuery", + "type" : "PagedJdbcQuery", + "sql" : "SELECT *\nFROM \"orders_2\"\nWHERE \"customerid\" = $1\nORDER BY \"_ingest_time\" DESC NULLS LAST, \"id\", \"time\"", + "parameters" : [ + { + "type" : "arg", + "type" : "arg", + "path" : "customerid" + } + ] + } + }, + { + "arguments" : [ + { + "type" : "variable", + "type" : "variable", + "path" : "id" + }, + { + "type" : "variable", + "type" : "variable", + "path" : "customerid" + }, + { + "type" : "variable", + "type" : "variable", + "path" : "time" + }, + { + "type" : "variable", + "type" : "variable", + "path" : "limit" + }, + { + "type" : "variable", + "type" : "variable", + "path" : "offset" + } + ], + "query" : { + "type" : "PagedJdbcQuery", + "type" : "PagedJdbcQuery", + "sql" : "SELECT *\nFROM \"orders_2\"\nWHERE \"id\" = $1 AND \"customerid\" = $2 AND \"time\" = $3\nORDER BY \"_ingest_time\" DESC NULLS LAST", + "parameters" : [ + { + "type" : "arg", + "type" : "arg", + "path" : "id" + }, + { + "type" : "arg", + "type" : "arg", + "path" : "customerid" + }, + { + "type" : "arg", + "type" : "arg", + "path" : "time" + } + ] + } + }, + { + "arguments" : [ + { + "type" : "variable", + "type" : "variable", + "path" : "id" + }, + { + "type" : "variable", + "type" : "variable", + "path" : "customerid" + }, + { + "type" : "variable", + "type" : "variable", + "path" : "limit" + }, + { + "type" : "variable", + "type" : "variable", + "path" : "offset" + } + ], + "query" : { + "type" : "PagedJdbcQuery", + "type" : "PagedJdbcQuery", + "sql" : "SELECT *\nFROM \"orders_2\"\nWHERE \"id\" = $1 AND \"customerid\" = $2\nORDER BY \"_ingest_time\" DESC NULLS LAST, \"time\"", + "parameters" : [ + { + "type" : "arg", + "type" : "arg", + "path" : "id" + }, + { + "type" : "arg", + "type" : "arg", + "path" : "customerid" + } + ] + } + } + ] + } + ], + "mutations" : [ ], + "subscriptions" : [ ], + "schema" : { + "type" : "string", + "type" : "string", + "schema" : "\"An RFC-3339 compliant Full Date Scalar\"\nscalar Date\n\n\"An RFC-3339 compliant DateTime Scalar\"\nscalar DateTime\n\n\"An arbitrary precision signed integer\"\nscalar GraphQLBigInteger\n\n\"A JSON scalar\"\nscalar JSON\n\n\"24-hour clock time value string in the format `hh:mm:ss` or `hh:mm:ss.sss`.\"\nscalar LocalTime\n\ntype Orders {\n id: GraphQLBigInteger!\n customerid: GraphQLBigInteger!\n time: DateTime!\n productid: GraphQLBigInteger!\n quantity: GraphQLBigInteger!\n unit_price: Float\n discount: Float\n}\n\ntype Query {\n Orders(id: GraphQLBigInteger, customerid: GraphQLBigInteger, time: DateTime, limit: Int = 10, offset: Int = 0): [Orders!]\n}\n" + } + } +} diff --git a/sqrl-testing/sqrl-integration-tests/src/test/resources/usecases/simple-with-bigint-support/ecommerce/orders.jsonl b/sqrl-testing/sqrl-integration-tests/src/test/resources/usecases/simple-with-bigint-support/ecommerce/orders.jsonl new file mode 100644 index 0000000000..d30540c413 --- /dev/null +++ b/sqrl-testing/sqrl-integration-tests/src/test/resources/usecases/simple-with-bigint-support/ecommerce/orders.jsonl @@ -0,0 +1,14 @@ +{"id": 6, "customerid": 6, "time": "2023-01-02T22:21:00.000Z", "productid": 115,"quantity": 1,"unit_price": 9.22 } +{"id": 5, "customerid": 11, "time": "2023-01-02T22:21:00.000Z", "productid": 115,"quantity": 1,"unit_price": 9.22 } +{"id": 2, "customerid": 11, "time": "2023-01-02T22:21:00.000Z", "productid": 115,"quantity": 1,"unit_price": 9.22 } +{"id": 3, "customerid": 9, "time": "2023-01-02T22:21:00.000Z", "productid": 115,"quantity": 1,"unit_price": 9.22 } +{"id": 5, "customerid": 11, "time": "2023-01-02T22:22:00.000Z", "productid": 115,"quantity": 1,"unit_price": 9.22 } +{"id": 2, "customerid": 10, "time": "2023-01-02T22:22:00.000Z", "productid": 115,"quantity": 1,"unit_price": 9.22 } +{"id": 2, "customerid": 8, "time": "2023-01-02T22:22:00.000Z", "productid": 115,"quantity": 1,"unit_price": 9.22 } +{"id": 5, "customerid": 11, "time": "2023-01-02T22:24:00.000Z", "productid": 115,"quantity": 2,"unit_price": 9.22 } +{"id": 2, "customerid": 10, "time": "2023-01-02T22:24:00.000Z", "productid": 115,"quantity": 1,"unit_price": 9.22 } +{"id": 5, "customerid": 11, "time": "2023-01-02T22:23:00.000Z", "productid": 115,"quantity": 1,"unit_price": 9.22 } +{"id": 2, "customerid": 11, "time": "2023-01-02T22:23:00.000Z", "productid": 115,"quantity": 1,"unit_price": 9.22 } +{"id": 5, "customerid": 11, "time": "2023-01-02T22:25:00.000Z", "productid": 115,"quantity": 2,"unit_price": 9.22 } +{"id": 2, "customerid": 10, "time": "2023-01-02T22:25:00.000Z", "productid": 115,"quantity": 1,"unit_price": 9.22 } +{"id": 3, "customerid": 7, "time": "2023-01-02T22:25:00.000Z", "productid": 115,"quantity": 1,"unit_price": 9.22 } \ No newline at end of file diff --git a/sqrl-testing/sqrl-integration-tests/src/test/resources/usecases/simple-with-bigint-support/ecommerce/orders.schema.yml b/sqrl-testing/sqrl-integration-tests/src/test/resources/usecases/simple-with-bigint-support/ecommerce/orders.schema.yml new file mode 100644 index 0000000000..f2558f85e7 --- /dev/null +++ b/sqrl-testing/sqrl-integration-tests/src/test/resources/usecases/simple-with-bigint-support/ecommerce/orders.schema.yml @@ -0,0 +1,29 @@ +--- +name: "Orders" +schema_version: "1" +partial_schema: false +columns: +- name: "id" + type: "BIGINT" + tests: + - "not_null" +- name: "customerid" + type: "BIGINT" + tests: + - "not_null" +- name: "time" + type: "TIMESTAMP" + tests: + - "not_null" +- name: "productid" + type: "BIGINT" + tests: + - "not_null" +- name: "quantity" + type: "BIGINT" + tests: + - "not_null" +- name: "unit_price" + type: "DOUBLE" +- name: "discount" + type: "DOUBLE" diff --git a/sqrl-testing/sqrl-integration-tests/src/test/resources/usecases/simple-with-bigint-support/ecommerce/orders.table.json b/sqrl-testing/sqrl-integration-tests/src/test/resources/usecases/simple-with-bigint-support/ecommerce/orders.table.json new file mode 100644 index 0000000000..c6ddeda039 --- /dev/null +++ b/sqrl-testing/sqrl-integration-tests/src/test/resources/usecases/simple-with-bigint-support/ecommerce/orders.table.json @@ -0,0 +1,19 @@ +{ + "version": 1, + "flink" : { + "format" : "flexible-json", + "path" : "${DATA_PATH}/orders.jsonl", + "source.monitor-interval" : 10000, + "connector" : "filesystem" + }, + "table" : { + "type" : "source", + "timestamp" : "_ingest_time", + "primary-key" : ["id", "customerid", "time"] + }, + "metadata" : { + "_ingest_time" : { + "attribute" : "proctime()" + } + } +} \ No newline at end of file diff --git a/sqrl-testing/sqrl-integration-tests/src/test/resources/usecases/simple-with-bigint-support/package.json b/sqrl-testing/sqrl-integration-tests/src/test/resources/usecases/simple-with-bigint-support/package.json new file mode 100644 index 0000000000..7169fb3df5 --- /dev/null +++ b/sqrl-testing/sqrl-integration-tests/src/test/resources/usecases/simple-with-bigint-support/package.json @@ -0,0 +1,6 @@ +{ + "version": "1", + "compiler": { + "extendedScalarTypes": true + } +} \ No newline at end of file diff --git a/sqrl-testing/sqrl-integration-tests/src/test/resources/usecases/simple-with-bigint-support/simple-with-bigint-support.graphql b/sqrl-testing/sqrl-integration-tests/src/test/resources/usecases/simple-with-bigint-support/simple-with-bigint-support.graphql new file mode 100644 index 0000000000..b261100dd2 --- /dev/null +++ b/sqrl-testing/sqrl-integration-tests/src/test/resources/usecases/simple-with-bigint-support/simple-with-bigint-support.graphql @@ -0,0 +1,28 @@ +"An RFC-3339 compliant Full Date Scalar" +scalar Date + +"An RFC-3339 compliant DateTime Scalar" +scalar DateTime + +"An arbitrary precision signed integer" +scalar GraphQLBigInteger + +"A JSON scalar" +scalar JSON + +"24-hour clock time value string in the format `hh:mm:ss` or `hh:mm:ss.sss`." +scalar LocalTime + +type Orders { + id: GraphQLBigInteger! + customerid: GraphQLBigInteger! + time: DateTime! + productid: GraphQLBigInteger! + quantity: GraphQLBigInteger! + unit_price: Float + discount: Float +} + +type Query { + Orders(id: GraphQLBigInteger, customerid: GraphQLBigInteger, time: DateTime, limit: Int = 10, offset: Int = 0): [Orders!] +} \ No newline at end of file diff --git a/sqrl-testing/sqrl-integration-tests/src/test/resources/usecases/simple-with-bigint-support/simple-with-bigint-support.sqrl b/sqrl-testing/sqrl-integration-tests/src/test/resources/usecases/simple-with-bigint-support/simple-with-bigint-support.sqrl new file mode 100644 index 0000000000..d5c12f0907 --- /dev/null +++ b/sqrl-testing/sqrl-integration-tests/src/test/resources/usecases/simple-with-bigint-support/simple-with-bigint-support.sqrl @@ -0,0 +1,4 @@ +IMPORT ecommerce.Orders; + +/*+test */ +BigIntOrdersTest := SELECT * FROM Orders ORDER BY id ASC; \ No newline at end of file diff --git a/sqrl-testing/sqrl-integration-tests/src/test/resources/usecases/simple-with-bigint-support/snapshots-simple-with-bigint-support/BigIntOrdersTest.snapshot b/sqrl-testing/sqrl-integration-tests/src/test/resources/usecases/simple-with-bigint-support/snapshots-simple-with-bigint-support/BigIntOrdersTest.snapshot new file mode 100644 index 0000000000..72047125f9 --- /dev/null +++ b/sqrl-testing/sqrl-integration-tests/src/test/resources/usecases/simple-with-bigint-support/snapshots-simple-with-bigint-support/BigIntOrdersTest.snapshot @@ -0,0 +1 @@ +{"data":{"BigIntOrdersTest":[{"id":2,"customerid":8,"time":"2023-01-03T06:22:00.000Z","productid":115,"quantity":1,"unit_price":9.22,"discount":null},{"id":2,"customerid":10,"time":"2023-01-03T06:22:00.000Z","productid":115,"quantity":1,"unit_price":9.22,"discount":null},{"id":2,"customerid":10,"time":"2023-01-03T06:24:00.000Z","productid":115,"quantity":1,"unit_price":9.22,"discount":null},{"id":2,"customerid":10,"time":"2023-01-03T06:25:00.000Z","productid":115,"quantity":1,"unit_price":9.22,"discount":null},{"id":2,"customerid":11,"time":"2023-01-03T06:21:00.000Z","productid":115,"quantity":1,"unit_price":9.22,"discount":null},{"id":2,"customerid":11,"time":"2023-01-03T06:23:00.000Z","productid":115,"quantity":1,"unit_price":9.22,"discount":null},{"id":3,"customerid":7,"time":"2023-01-03T06:25:00.000Z","productid":115,"quantity":1,"unit_price":9.22,"discount":null},{"id":3,"customerid":9,"time":"2023-01-03T06:21:00.000Z","productid":115,"quantity":1,"unit_price":9.22,"discount":null},{"id":5,"customerid":11,"time":"2023-01-03T06:21:00.000Z","productid":115,"quantity":1,"unit_price":9.22,"discount":null},{"id":5,"customerid":11,"time":"2023-01-03T06:22:00.000Z","productid":115,"quantity":1,"unit_price":9.22,"discount":null}]}} \ No newline at end of file diff --git a/sqrl-tools/sqrl-config/src/main/java/com/datasqrl/config/CompilerConfigImpl.java b/sqrl-tools/sqrl-config/src/main/java/com/datasqrl/config/CompilerConfigImpl.java index 3270e13a81..949f0a7cd2 100644 --- a/sqrl-tools/sqrl-config/src/main/java/com/datasqrl/config/CompilerConfigImpl.java +++ b/sqrl-tools/sqrl-config/src/main/java/com/datasqrl/config/CompilerConfigImpl.java @@ -21,6 +21,10 @@ public boolean isAddArguments() { return sqrlConfig.asBool("addArguments") .getOptional().orElse(true); } + public boolean isExtendedScalarTypes() { + return sqrlConfig.asBool("extendedScalarTypes") + .getOptional().orElse(false); // by default don't use the extended scalar types (map PK as float) for backward compatibility + } @Override public String getLogger() { diff --git a/sqrl-tools/sqrl-config/src/main/resources/jsonSchema/packageSchema.json b/sqrl-tools/sqrl-config/src/main/resources/jsonSchema/packageSchema.json index aaab440ccc..56a127045b 100644 --- a/sqrl-tools/sqrl-config/src/main/resources/jsonSchema/packageSchema.json +++ b/sqrl-tools/sqrl-config/src/main/resources/jsonSchema/packageSchema.json @@ -76,6 +76,9 @@ }, "logger": { "type": "string" + }, + "extendedScalarTypes": { + "type": "boolean" } } }, diff --git a/sqrl-tools/sqrl-packager/src/main/java/com/datasqrl/compile/CompilationProcessV2.java b/sqrl-tools/sqrl-packager/src/main/java/com/datasqrl/compile/CompilationProcessV2.java index a3520be7bd..45af50a7a5 100644 --- a/sqrl-tools/sqrl-packager/src/main/java/com/datasqrl/compile/CompilationProcessV2.java +++ b/sqrl-tools/sqrl-packager/src/main/java/com/datasqrl/compile/CompilationProcessV2.java @@ -2,7 +2,6 @@ import com.datasqrl.actions.CreateDatabaseQueries; import com.datasqrl.actions.GraphqlPostplanHook; -import com.datasqrl.actions.InferGraphqlSchema; import com.datasqrl.actions.DagWriter; import com.datasqrl.canonicalizer.Name; import com.datasqrl.config.BuildPath; @@ -16,6 +15,7 @@ import com.datasqrl.plan.queries.APISource; import com.datasqrl.plan.queries.APISourceImpl; import com.datasqrl.util.ServiceLoaderDiscovery; +import com.datasqrl.v2.graphql.InferGraphqlSchema2; import com.datasqrl.v2.dag.DAGBuilder; import com.datasqrl.v2.dag.DAGPlanner; import com.datasqrl.v2.dag.PipelineDAG; @@ -45,7 +45,7 @@ public class CompilationProcessV2 { private final PhysicalPlanner physicalPlanner; private final GraphqlPostplanHook graphqlPostplanHook; private final CreateDatabaseQueries createDatabaseQueries; - private final InferGraphqlSchema inferencePostcompileHook; + private final InferGraphqlSchema2 inferGraphqlSchema; private final DagWriter writeDeploymentArtifactsHook; // private final FlinkSqlGenerator flinkSqlGenerator; private final GraphqlSourceFactory graphqlSourceFactory; @@ -77,17 +77,14 @@ public Pair executeCompilation(Optional testsPath) - make sure we generate the right testplan - create the RootGraphQL model and attach to serverPlan */ - if (serverPlan.isPresent() && false) { + if (serverPlan.isPresent()) { Optional apiSource = graphqlSourceFactory.get(); if (apiSource.isEmpty() || executionGoal == ExecutionGoal.TEST) { //Infer schema from functions - //TODO: rewrite the following to use the functions from the serverPlan - apiSource = inferencePostcompileHook.inferGraphQLSchema() + apiSource = inferGraphqlSchema.inferGraphQLSchema(serverPlan.get()) .map(schemaString -> new APISourceImpl(Name.system(""), schemaString)); } assert apiSource.isPresent(); - - //TODO: Validates and generates queries - inferencePostcompileHook.validateAndGenerateQueries(apiSource.get(), null); + inferGraphqlSchema.validateAndGenerateQueries(apiSource.get()); //TODO: Generates RootGraphQLModel, use serverplan as argument only graphqlPostplanHook.updatePlan(apiSource, null);