Skip to content
Open
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
601c7fd
feat: Add opt-in SqrlPagination metadata for paginated GraphQL queries
velo Jul 6, 2026
1dfe13a
Merge remote-tracking branch 'origin/main' into feat/graphql-paginati…
velo Jul 8, 2026
33fa40a
feat: Rename pagination type to OffsetPageInfo and compute metadata l…
velo Jul 8, 2026
d611559
test: Prove lazy pagination metadata via recorded SQL in PagedQueryIT
velo Jul 8, 2026
5720bc5
feat: Add compiler config to generate paginated results in inferred G…
velo Jul 8, 2026
d072d3c
refactor: Require users to declare OffsetPageInfo instead of auto-inj…
velo Jul 9, 2026
990820a
fix: Avoid NPE on null pagination bind params and report real pageSiz…
velo Jul 9, 2026
35269ac
test: Cover paginated event-times selection when the result has no ro…
velo Jul 9, 2026
8f61cdb
Merge branch 'main' of github.com:DataSQRL/sqrl into feat/graphql-pag…
velo Jul 10, 2026
6d7e26c
refactor: Replace count-based pagination with OFFSET_PAGE_INFO and in…
velo Jul 10, 2026
3a461bb
test: Regenerate DAG writer snapshot for paginated clickstream usecase
velo Jul 10, 2026
e14fd9b
Merge branch 'main' of github.com:DataSQRL/sqrl into feat/graphql-pag…
velo Jul 22, 2026
1028589
refactor: Separate OFFSET_PAGE_INFO execution from LIMIT_AND_OFFSET a…
velo Jul 22, 2026
5432f0e
make `PagedRowTimeIndexRewriter` compatible with the general `Physica…
ferenc-csaky Jul 24, 2026
77fd8b9
Merge branch 'main' of github.com:DataSQRL/sqrl into feat/graphql-pag…
velo Jul 29, 2026
6888e8e
fix: Adapt paginated snapshots and JdbcStatement type import after me…
velo Jul 29, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,11 @@ public int getDefaultLimit() {
return sqrlConfig.asInt("default-limit").get();
}

@Override
public boolean generatePaginatedResults() {
return sqrlConfig.asBool("paginated-results").withDefault(false).get();
}

public enum Endpoints {
OPS_ONLY, // only support the pre-defined operations in the GraphQL API, do not support flexible
// GraphQL queries TODO: not yet implemented
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@ interface CompilerApiConfig {
int getMaxResultDepth();

int getDefaultLimit();

boolean generatePaginatedResults();
}

interface ExplainConfig {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,8 @@ protected void visitQuery(
ObjectTypeDefinition parentType,
FieldDefinition atField,
SqrlTableFunction tableFunction,
TypeDefinitionRegistry registry) {
TypeDefinitionRegistry registry,
boolean paged) {
// As we no more merge user provided graphQL schema with the inferred schema, we no more need to
// generate as many queries as the permutations of its arguments.
// We now have a single executable query linked to the table function and already fully defined
Expand All @@ -168,13 +169,18 @@ protected void visitQuery(
.map(InputValueDefinition::getName)
.anyMatch(
name -> name.equals(SchemaConstants.LIMIT) || name.equals(SchemaConstants.OFFSET));
var countSql = paged ? buildCountSql(executableJdbcReadQuery.getSql()) : null;
Comment thread
velo marked this conversation as resolved.
Outdated
var countWithEventTimesSql =
paged ? buildCountWithEventTimesSql(tableFunction, executableJdbcReadQuery.getSql()) : null;
queryBase =
new SqlQuery(
executableJdbcReadQuery.getSql(),
parameters,
hasLimitOrOffset ? PaginationType.LIMIT_AND_OFFSET : PaginationType.NONE,
executableJdbcReadQuery.getCacheDuration().toMillis(),
executableJdbcReadQuery.getDatabase());
executableJdbcReadQuery.getDatabase(),
countSql,
countWithEventTimesSql);
var coordsBuilder =
ArgumentLookupQueryCoords.builder()
.parentType(parentType.getName())
Expand All @@ -186,6 +192,34 @@ protected void visitQuery(
queryCoords.add(coordsBuilder.build());
}

/** Builds the companion COUNT(*) query for a paginated result. */
private static String buildCountSql(String baseSql) {
return "SELECT COUNT(*) AS \"total_records\" FROM (" + baseSql + ") x";
}

/**
* Variant of the count query that also computes MIN/MAX over the designated rowtime column for
* {@code firstEventTime}/{@code lastEventTime}. Returns null when the result has no rowtime. The
* rowtime column name is the same identifier as in the base query's output.
*/
private static String buildCountWithEventTimesSql(
SqrlTableFunction tableFunction, String baseSql) {
return tableFunction
.getRowTime()
.map(tableFunction::getField)
.map(RelDataTypeField::getName)
.map(
col ->
"SELECT COUNT(*) AS \"total_records\", MIN(\""
+ col
+ "\") AS \"first_event_time\", MAX(\""
+ col
+ "\") AS \"last_event_time\" FROM ("
+ baseSql
+ ") x")
.orElse(null);
}
Comment thread
velo marked this conversation as resolved.
Outdated

private static QueryParameterHandler convert(FunctionParameter fnParam) {
final var sqrlParam = (SqrlFunctionParameter) fnParam;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
import graphql.schema.GraphQLDirective;
import graphql.schema.GraphQLEnumType;
import graphql.schema.GraphQLFieldDefinition;
import graphql.schema.GraphQLNonNull;
import graphql.schema.GraphQLObjectType;
import graphql.schema.GraphQLOutputType;
import graphql.schema.GraphQLSchema;
Expand Down Expand Up @@ -72,11 +73,13 @@ public class GraphqlSchemaFactory {
private final boolean extendedScalarTypes;
private final boolean addApiDirective;
private final int defaultLimit;
private final boolean generatePaginatedResults;

public GraphqlSchemaFactory(CompilerConfig config) {
this.extendedScalarTypes = config.isExtendedScalarTypes();
this.addApiDirective = !config.getApiConfig().isGraphQLProtocolOnly();
this.defaultLimit = config.getApiConfig().getDefaultLimit();
this.generatePaginatedResults = config.getApiConfig().generatePaginatedResults();
}

public GraphQLSchema generate(ServerPhysicalPlan serverPlan) {
Expand Down Expand Up @@ -284,9 +287,7 @@ private Optional<GraphQLObjectType> createRootType(

final var type =
tableFunctionsType == AccessModifier.QUERY
? (GraphQLOutputType)
wrapMultiplicity(
createTypeReference(tableFunction), tableFunction.getMultiplicity())
? createQueryResultType(tableFunction)
: createTypeReference(
tableFunction); // type is nullable because there can be no update in the
// subscription
Expand Down Expand Up @@ -346,13 +347,15 @@ private Optional<GraphQLFieldDefinition> createRelationshipField(SqrlTableFuncti
}

// reference the type that will be defined when the table function relationship is processed
var fieldType =
relationship.getVisibility().access() == AccessModifier.QUERY
? createQueryResultType(relationship)
: (GraphQLOutputType)
wrapMultiplicity(createTypeReference(relationship), relationship.getMultiplicity());
var field =
GraphQLFieldDefinition.newFieldDefinition()
.name(fieldName)
.type(
(GraphQLOutputType)
wrapMultiplicity(
createTypeReference(relationship), relationship.getMultiplicity()))
.type(fieldType)
.arguments(createArguments(relationship));
relationship.getDocumentation().getDocStringOpt().ifPresent(field::description);

Expand Down Expand Up @@ -432,6 +435,44 @@ private GraphQLOutputType createTypeReference(SqrlTableFunction tableFunction) {
return new GraphQLTypeReference(typeName);
}

/**
* Result type of a query table function: when paginated results are enabled, MANY results are
* wrapped in a {@code <Type>Page {results, pagination}} type so the server computes {@code
* OffsetPageInfo} metadata; otherwise the plain multiplicity-wrapped type.
*/
private GraphQLOutputType createQueryResultType(SqrlTableFunction tableFunction) {
if (generatePaginatedResults && tableFunction.getMultiplicity() == Multiplicity.MANY) {
return GraphQLNonNull.nonNull(createPageType(tableFunction));
}
return (GraphQLOutputType)
wrapMultiplicity(createTypeReference(tableFunction), tableFunction.getMultiplicity());
}

private GraphQLTypeReference createPageType(SqrlTableFunction tableFunction) {
var elementType = (GraphQLTypeReference) createTypeReference(tableFunction);
var pageTypeName = elementType.getName() + "Page";
if (!definedTypeNames.contains(pageTypeName)) {
definedTypeNames.add(pageTypeName);
objectTypes.add(
GraphQLObjectType.newObject()
.name(pageTypeName)
.field(
GraphQLFieldDefinition.newFieldDefinition()
.name("results")
.type((GraphQLOutputType) wrapMultiplicity(elementType, Multiplicity.MANY)))
.field(
GraphQLFieldDefinition.newFieldDefinition()
.name("pagination")
.type(new GraphQLTypeReference(OffsetPageInfoUtil.PAGINATION_TYPE_NAME)))
.build());
if (!definedTypeNames.contains(OffsetPageInfoUtil.PAGINATION_TYPE_NAME)) {
definedTypeNames.add(OffsetPageInfoUtil.PAGINATION_TYPE_NAME);
objectTypes.add(OffsetPageInfoUtil.createPageInfoType());
}
}
return new GraphQLTypeReference(pageTypeName);
}

public static final String API_DIRECTIVE_NAME = "api";

private void addApiDirectiveTypes(GraphQLSchema.Builder graphQLSchemaBuilder) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -374,8 +374,23 @@ protected void visitQuery(
ObjectTypeDefinition parentType,
FieldDefinition atField,
SqrlTableFunction tableFunction,
TypeDefinitionRegistry registry) {
TypeDefinitionRegistry registry,
boolean paged) {
checkValidArrayNonNullType(atField.getType());
if (paged) {
OffsetPageInfoUtil.validatePaginationType(registry, atField.getSourceLocation());
var argNames =
atField.getInputValueDefinitions().stream()
.map(InputValueDefinition::getName)
.collect(Collectors.toSet());
checkState(
argNames.contains(LIMIT) && argNames.contains(OFFSET),
atField.getSourceLocation(),
"Paginated query [%s] must declare both '%s' and '%s' arguments",
atField.getName(),
LIMIT,
OFFSET);
}
checkArgumentsMatchParameters(atField, tableFunction, registry);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@

import com.datasqrl.canonicalizer.Name;
import com.datasqrl.canonicalizer.NamePath;
import com.datasqrl.plan.table.Multiplicity;
import com.datasqrl.planner.dag.plan.MutationTable;
import com.datasqrl.planner.parser.AccessModifier;
import com.datasqrl.planner.tables.SqrlTableFunction;
Expand Down Expand Up @@ -138,14 +139,34 @@ private void walkTableFunction(
typeDefinition.getSourceLocation(),
"Could not infer non-object type on graphql schema: %s",
typeDefinition.getName());
var resultType = (ObjectTypeDefinition) typeDefinition;

// A page wrapper ({results: [Element!], pagination: OffsetPageInfo}) is treated like a list of
// Element: validate/walk the element type against the function row type and compute pagination
// metadata via a companion count query.
var pagedElement = OffsetPageInfoUtil.getPagedElementType(resultType, registry);
var paged = pagedElement.isPresent();
if (paged) {
checkState(
tableFunction.getVisibility().access() == AccessModifier.QUERY,
atField.getSourceLocation(),
"Paginated result types are only supported for queries: %s",
atField.getName());
checkState(
tableFunction.getMultiplicity() == Multiplicity.MANY,
atField.getSourceLocation(),
"Paginated result types require a multi-row result (no LIMIT 1): %s",
atField.getName());
resultType = pagedElement.get();
}

if (tableFunction.getVisibility().access()
== AccessModifier.QUERY) { // walking a query table function
visitQuery(parentType, atField, tableFunction, registry);
visitQuery(parentType, atField, tableFunction, registry, paged);
} else { // walking a subscription table function
visitSubscription(atField, tableFunction, registry);
}
var functionRowType = tableFunction.getRowType();
var resultType = (ObjectTypeDefinition) typeDefinition;
walkObjectType(true, resultType, Optional.of(functionRowType), registry);
}

Expand Down Expand Up @@ -257,7 +278,8 @@ protected abstract void visitQuery(
ObjectTypeDefinition parentType,
FieldDefinition atField,
SqrlTableFunction tableFunction,
TypeDefinitionRegistry registry);
TypeDefinitionRegistry registry,
boolean paged);

protected abstract void visitSubscription(
FieldDefinition atField, SqrlTableFunction tableFunction, TypeDefinitionRegistry registry);
Expand Down
Loading