From 50d5cb698e9191703341d270b20382c6b232ad6a Mon Sep 17 00:00:00 2001 From: Karthikeyan C Date: Thu, 21 Mar 2024 17:19:30 +0530 Subject: [PATCH 1/4] Major refactor to accomodate joins in the SQL query syntax --- src/cosmosDb.ts | 1 + src/execution.ts | 48 ++++++-- src/introspectContainerSchema.ts | 1 - src/sqlGeneration.ts | 197 ++++++++++++++++++++++++------- 4 files changed, 190 insertions(+), 57 deletions(-) diff --git a/src/cosmosDb.ts b/src/cosmosDb.ts index 9a7a45d..3eac879 100644 --- a/src/cosmosDb.ts +++ b/src/cosmosDb.ts @@ -1,4 +1,5 @@ import { CosmosClient, Database, Container, SqlQuerySpec } from "@azure/cosmos" +import * as sdk from '@hasura/ndc-sdk-typescript' export type RawCosmosDbConfig = { databaseName: string, diff --git a/src/execution.ts b/src/execution.ts index a964096..372020c 100644 --- a/src/execution.ts +++ b/src/execution.ts @@ -25,14 +25,15 @@ function validateOrderBy(orderBy: sdk.OrderBy, collectionObjectType: schema.Obje } } - -function parseQueryRequest(collectionsSchema: schema.CollectionsSchema, queryRequest: sdk.QueryRequest): sql.SqlQueryGenerationContext { +function parseQueryRequest(collectionsSchema: schema.CollectionsSchema, queryRequest: sdk.QueryRequest): sql.SqlQueryParts { let isAggregateQuery = false; const collection: string = queryRequest.collection; const collectionDefinition: schema.CollectionDefinition = collectionsSchema.collections[collection]; + const rootContainerAlias = `${collection[0]}`; + let requestedFields: sql.SelectColumns = {}; if (collectionDefinition === undefined) @@ -64,7 +65,10 @@ function parseQueryRequest(collectionsSchema: schema.CollectionsSchema, queryReq } else { requestedFields[fieldName] = { kind: 'column', - columnName: queryField.column + column: { + name: queryField.column, + prefix: [rootContainerAlias] + } } }; @@ -87,14 +91,20 @@ function parseQueryRequest(collectionsSchema: schema.CollectionsSchema, queryReq if (aggregateField.distinct) { requestedFields[fieldName] = { kind: 'aggregate', - columnName: aggregateField.column, + column: { + name: aggregateField.column, + prefix: [rootContainerAlias], + }, aggregateFunction: 'DISTINCT COUNT' }; } else { requestedFields[fieldName] = { kind: 'aggregate', - columnName: aggregateField.column, + column: { + name: aggregateField.column, + prefix: [rootContainerAlias], + }, aggregateFunction: 'COUNT' } } @@ -107,7 +117,11 @@ function parseQueryRequest(collectionsSchema: schema.CollectionsSchema, queryReq } else { requestedFields[fieldName] = { kind: 'aggregate', - columnName: aggregateField.column, + column: { + name: aggregateField.column, + prefix: [rootContainerAlias] + }, + aggregateFunction: aggregateField.function } @@ -116,7 +130,10 @@ function parseQueryRequest(collectionsSchema: schema.CollectionsSchema, queryReq case "star_count": requestedFields[fieldName] = { kind: 'aggregate', - columnName: '*', + column: { + name: "*", + prefix: [rootContainerAlias], + }, aggregateFunction: 'COUNT' }; break; @@ -124,10 +141,16 @@ function parseQueryRequest(collectionsSchema: schema.CollectionsSchema, queryReq }) } - let sqlGenCtx: sql.SqlQueryGenerationContext = { - selectFields: requestedFields, + let fromClause: sql.FromClause = { + source: collection, + sourceAlias: `${collection[0]}`, + }; + + let sqlGenCtx: sql.SqlQueryParts = { + select: requestedFields, + from: fromClause, isAggregateQuery - } + }; if (queryRequest.query.limit != null) { if (queryRequest.query.offset != null) { @@ -148,6 +171,7 @@ function parseQueryRequest(collectionsSchema: schema.CollectionsSchema, queryReq } sqlGenCtx.predicate = queryRequest.query.predicate; + return sqlGenCtx } @@ -166,9 +190,9 @@ export async function executeQuery(queryRequest: sdk.QueryRequest, collectionsSc if (dbContainer === undefined || dbContainer == null) throw new sdk.InternalServerError(`Couldn't find the container '${collection}' in the schema.`) - const sqlGenCtx: sql.SqlQueryGenerationContext = parseQueryRequest(collectionsSchema, queryRequest); + const sqlGenCtx: sql.SqlQueryParts = parseQueryRequest(collectionsSchema, queryRequest); - const sqlQuery = sql.generateSqlQuery(sqlGenCtx, collection, collection[0]); + const sqlQuery = sql.generateSqlQuerySpec(sqlGenCtx, collection, queryRequest.variables); const queryResponse = await runSQLQuery<{ [k: string]: unknown }>(sqlQuery, dbContainer); diff --git a/src/introspectContainerSchema.ts b/src/introspectContainerSchema.ts index 00b9aa3..8b294bc 100644 --- a/src/introspectContainerSchema.ts +++ b/src/introspectContainerSchema.ts @@ -22,7 +22,6 @@ export async function fetchLatestNRowsFromContainer(n: number, container: Contai } export async function inferJSONSchemaFromContainerRows(rows: string[], containerTypeName: string): Promise { - console.log("Container rows are ", JSON.stringify(rows, null, 2)); const jsonInput = jsonInputForTargetLanguage("schema"); await jsonInput.addSource({ diff --git a/src/sqlGeneration.ts b/src/sqlGeneration.ts index cb0b08e..bc525d0 100644 --- a/src/sqlGeneration.ts +++ b/src/sqlGeneration.ts @@ -2,14 +2,19 @@ import * as sdk from "@hasura/ndc-sdk-typescript"; import * as cosmos from "@azure/cosmos"; import { SqlQuerySpec } from "@azure/cosmos"; +export type Column = { + name: string, + prefix: string[], +} + export type SelectContainerColumn = { kind: 'column', - columnName: string + column: Column, } export type SelectAggregate = { kind: 'aggregate', - columnName: string, + column: Column, aggregateFunction: string } @@ -21,6 +26,12 @@ export type SelectColumns = { [alias: string]: (SelectContainerColumn | SelectAggregate) } +export type QueryVariable = { + [k: string]: unknown +} + +export type QueryVariables = QueryVariable[] | null | undefined + export type SqlQueryGenerationContext = { selectFields: SelectColumns, limit?: number | null, @@ -28,6 +39,7 @@ export type SqlQueryGenerationContext = { orderBy?: sdk.OrderBy | null, predicate?: sdk.Expression | null, isAggregateQuery: boolean, + variables?: QueryVariables } /* @@ -37,49 +49,143 @@ type SqlParameters = { [column: string]: any[] } -export function generateSqlQuery(sqlGenCtx: SqlQueryGenerationContext, containerName: string, containerAlias: string): SqlQuerySpec { - let sqlQueryParts: string[] = [] - let selectColumns: string = formatSelectColumns(sqlGenCtx.selectFields, containerAlias) +export type FromClause = { + source: string, + sourceAlias: string, + in?: string, +} + +export type ContainerExpression = { + kind: 'containerExpression', + containerExpression: string +} + +export type SqlExpression = { + kind: 'sqlExpression', + sqlExpression: SqlQueryParts +} + +export type ArrayJoinTarget = ContainerExpression | SqlExpression + +export type ArrayJoinClause = { + joinIdentifier: string, + arrayJoinTarget: ArrayJoinTarget, +} - sqlQueryParts.push(["SELECT", selectColumns].join(" ")); - sqlQueryParts.push(["FROM", containerName, containerAlias].join(" ")); +export type JoinClause = ArrayJoinClause; // TODO: Handle the case of `JOIN ((SELECT VALUE t FROM t IN p.tags WHERE t.name IN ("winter", "fall")))`, if needed. + +export type SqlQueryParts = { + select: SelectColumns, // TODO: Handle `SELECT VALUE` and `SELECT DISTINCT` here itself? If there is a need for it. + from?: FromClause | null, + join?: JoinClause[] | null, + predicate?: sdk.Expression | null, + offset?: number | null, + limit?: number | null, + orderBy?: sdk.OrderBy | null, + isAggregateQuery: boolean, + +} + + + +type VariablesMappings = { + /* + The variableTarget will be the name of the column + which gets the value of the variable + */ + [variableTarget: string]: string +} +function formatJoinClause(joinClause: JoinClause): string { + let joinTarget = + joinClause.arrayJoinTarget.kind === 'containerExpression' + ? joinClause.arrayJoinTarget.containerExpression + : constructSqlQuery(joinClause.arrayJoinTarget.sqlExpression, joinClause.joinIdentifier); + + return `${joinClause.arrayJoinTarget} in ${joinTarget}` +} + +function constructSqlQuery(sqlQueryParts: SqlQueryParts, fromContainerAlias: string): cosmos.SqlQuerySpec { + let selectColumns = formatSelectColumns(sqlQueryParts.select); + + let fromClause = + sqlQueryParts.from === null || sqlQueryParts.from === undefined + ? null + : `${sqlQueryParts.from.source} ${sqlQueryParts.from.sourceAlias}${sqlQueryParts.from.in ? ' IN' + sqlQueryParts.from.in : ''}`; + + let joinClause = null; + + if (sqlQueryParts.join !== null && sqlQueryParts.join !== undefined) { + joinClause = "JOIN " + sqlQueryParts.join?.map(joinClause => formatJoinClause(joinClause)).join("\nJOIN ") + } + + let whereClause = null; let predicateParameters: SqlParameters = {}; - if (sqlGenCtx.predicate != null && sqlGenCtx.predicate != undefined) { - const whereExp = visitExpression(predicateParameters, sqlGenCtx.predicate, containerAlias); - sqlQueryParts.push(`WHERE ${whereExp}`); + let utilisedVariables: VariablesMappings = {}; // This will be used to add the join mappings to the where expression. + + + + if (sqlQueryParts.predicate != null && sqlQueryParts.predicate != undefined) { + + const whereExp = visitExpression(predicateParameters, utilisedVariables, sqlQueryParts.predicate, fromContainerAlias); + // TODO: incorporate the `predicateParameters` obtained above. + whereClause = `WHERE ${whereExp}`; } - if (sqlGenCtx.orderBy != null && sqlGenCtx.orderBy != null && sqlGenCtx.orderBy.elements.length > 0) { - const orderByClause = visitOrderByElements(sqlGenCtx.orderBy.elements, containerAlias); - sqlQueryParts.push(["ORDER BY", orderByClause].join(" ")) + let orderByClause = null; + + if (sqlQueryParts.orderBy != null && sqlQueryParts.orderBy != null && sqlQueryParts.orderBy.elements.length > 0) { + orderByClause = visitOrderByElements(sqlQueryParts.orderBy.elements, fromContainerAlias); } - if (sqlGenCtx.offset != undefined && sqlGenCtx.offset != null) { - sqlQueryParts.push(["OFFSET", `${sqlGenCtx.offset}`].join(" ")) + let offsetClause = null; + + if (sqlQueryParts.offset != undefined && sqlQueryParts.offset != null) { + offsetClause = `${sqlQueryParts.offset}`; } - if (sqlGenCtx.limit != undefined && sqlGenCtx.limit != null) { - sqlQueryParts.push(["LIMIT", `${sqlGenCtx.limit}`].join(" ")) + let limitClause = null; + + if (sqlQueryParts.limit != undefined && sqlQueryParts.limit != null) { + limitClause = `${sqlQueryParts.limit}` + } - const query = sqlQueryParts.join("\n"); + const query = + `SELECT ${selectColumns} + ${fromClause ? 'FROM ' + fromClause : ''} + ${joinClause ?? ''} + ${whereClause ?? ''} + ${orderByClause ? 'ORDER BY ' + orderByClause : ''} + ${offsetClause ? 'OFFSET ' + offsetClause : ''} + ${limitClause ? 'LIMIT ' + limitClause : ''}`; return { query, parameters: serializeSqlParameters(predicateParameters) - }; + } +} + +export function generateSqlQuerySpec(sqlGenCtx: SqlQueryParts, containerName: string, queryVariables: QueryVariables): SqlQuerySpec { + + const querySpec = constructSqlQuery(sqlGenCtx, `${containerName[0]}`); + + return querySpec } -function formatSelectColumns(fieldsToSelect: SelectColumns, containerAlias: string): string { +function formatColumn(column: Column) { + return `${column.prefix.join(".")}.${column.name}` +} + +function formatSelectColumns(fieldsToSelect: SelectColumns): string { return Object.entries(fieldsToSelect).map(([alias, selectColumn]) => { switch (selectColumn.kind) { case 'column': - return `${containerAlias}.${selectColumn.columnName} as ${alias}` + return `${formatColumn(selectColumn.column)} as ${alias}` case 'aggregate': - return `${selectColumn.aggregateFunction}(${containerAlias}.${selectColumn.columnName}) as ${alias}` + return `${selectColumn.aggregateFunction} (${formatColumn(selectColumn.column)}) as ${alias} ` } }).join(","); @@ -105,7 +211,7 @@ function visitOrderByElement(value: sdk.OrderByElement, containerAlias: string): if (value.target.path.length > 0) { throw new sdk.NotSupported("Relationships are not supported in order_by.") } else { - return `${containerAlias}.${value.target.name} ${direction}` + return `${containerAlias}.${value.target.name} ${direction} ` } case 'single_column_aggregate': @@ -119,31 +225,32 @@ function visitOrderByElement(value: sdk.OrderByElement, containerAlias: string): /* Wraps the expression in parantheses to avoid generating SQL with wrong operator precedence. */ -function visitExpressionWithParentheses(parameters: SqlParameters, expression: sdk.Expression, containerAlias: string): string { - return `(${visitExpression(parameters, expression, containerAlias)})` +function visitExpressionWithParentheses(parameters: SqlParameters, variables: VariablesMappings, expression: sdk.Expression, containerAlias: string): string { + return `(${visitExpression(parameters, variables, expression, containerAlias)})` } -function visitExpression(parameters: SqlParameters, expression: sdk.Expression, containerAlias: string): String { +function visitExpression(parameters: SqlParameters, variables: VariablesMappings, expression: sdk.Expression, containerAlias: string): string { switch (expression.type) { case "and": if (expression.expressions.length > 0) { - return expression.expressions.map(expr => visitExpressionWithParentheses(parameters, expr, containerAlias)).join(" AND ") + return expression.expressions.map(expr => visitExpressionWithParentheses(parameters, variables, expr, containerAlias)).join(" AND ") } else { return "true" }; case "or": if (expression.expressions.length > 0) { - return expression.expressions.map(expr => visitExpressionWithParentheses(parameters, expr, containerAlias)).join(" OR ") + return expression.expressions.map(expr => visitExpressionWithParentheses(parameters, variables, expr, containerAlias)).join(" OR ") } else { return "false" }; case "not": - return `NOT ${visitExpressionWithParentheses(parameters, expression.expression, containerAlias)}` + return `NOT ${visitExpressionWithParentheses(parameters, variables, expression.expression, containerAlias)} ` case "unary_comparison_operator": switch (expression.operator) { case "is_null": - return `IS_NULL(${visitComparisonTarget(expression.column, containerAlias)})` + return `IS_NULL(${visitComparisonTarget(expression.column, containerAlias) + })` default: throw new sdk.BadRequest("Unknown unary comparison operator") @@ -152,17 +259,17 @@ function visitExpression(parameters: SqlParameters, expression: sdk.Expression, const comparisonTarget = visitComparisonTarget(expression.column, containerAlias); switch (expression.operator) { case "eq": - return `${comparisonTarget} = ${visitComparisonValue(parameters, expression.value, comparisonTarget)}` + return `${comparisonTarget} = ${visitComparisonValue(parameters, variables, expression.value, comparisonTarget, containerAlias)} ` case "neq": - return `${comparisonTarget} != ${visitComparisonValue(parameters, expression.value, comparisonTarget)}` + return `${comparisonTarget} != ${visitComparisonValue(parameters, variables, expression.value, comparisonTarget, containerAlias)} ` case "gte": - return `${comparisonTarget} >= ${visitComparisonValue(parameters, expression.value, comparisonTarget)}` + return `${comparisonTarget} >= ${visitComparisonValue(parameters, variables, expression.value, comparisonTarget, containerAlias)} ` case "gt": - return `${comparisonTarget} > ${visitComparisonValue(parameters, expression.value, comparisonTarget)}` + return `${comparisonTarget} > ${visitComparisonValue(parameters, variables, expression.value, comparisonTarget, containerAlias)} ` case "lte": - return `${comparisonTarget} <= ${visitComparisonValue(parameters, expression.value, comparisonTarget)}` + return `${comparisonTarget} <= ${visitComparisonValue(parameters, variables, expression.value, comparisonTarget, containerAlias)} ` case "lt": - return `${comparisonTarget} < ${visitComparisonValue(parameters, expression.value, comparisonTarget)}` + return `${comparisonTarget} <${visitComparisonValue(parameters, variables, expression.value, comparisonTarget, containerAlias)}` default: throw new sdk.BadRequest(`Unknown binary comparison operator ${expression.operator} found`) } @@ -174,11 +281,11 @@ function visitExpression(parameters: SqlParameters, expression: sdk.Expression, } } -function visitComparisonTarget(target: sdk.ComparisonTarget, containerAlias: string): string { +export function visitComparisonTarget(target: sdk.ComparisonTarget, containerAlias: string): string { switch (target.type) { case 'column': if (target.path.length > 0) { - throw new sdk.NotSupported("Relationships are not supported"); + throw new sdk.NotSupported("Nested fields are not supported yet"); } return `${containerAlias}.${target.name}`; case 'root_collection_column': @@ -186,7 +293,7 @@ function visitComparisonTarget(target: sdk.ComparisonTarget, containerAlias: str } } -function visitComparisonValue(parameters: SqlParameters, target: sdk.ComparisonValue, comparisonTarget: string): string { +export function visitComparisonValue(parameters: SqlParameters, variables: VariablesMappings, target: sdk.ComparisonValue, comparisonTarget: string, containerAlias: string): string { switch (target.type) { case 'scalar': const comparisonTargetName = comparisonTarget.replace(".", "_"); @@ -194,20 +301,22 @@ function visitComparisonValue(parameters: SqlParameters, target: sdk.ComparisonV if (comparisonTargetParameterValues != null) { const index = comparisonTargetParameterValues.findIndex((element) => element === target.value); if (index !== -1) { - return `@${comparisonTargetName}${index}` + return `@${comparisonTargetName}_${index} ` } else { let newIndex = parameters[comparisonTargetName].push(target.value); - return `@${comparisonTargetName}${newIndex}` + return `@${comparisonTargetName}_${newIndex} ` } } else { parameters[comparisonTargetName] = [target.value]; - return `@${comparisonTargetName}0` + return `@${comparisonTargetName}_0` } case 'column': throw new sdk.NotSupported("Column comparisons are not supported in field predicates yet"); case 'variable': - throw new sdk.NotSupported("Variables are not supported yet") + variables[comparisonTarget] = `vars.${target.name} ` + return `vars.${target.name} ` + } } @@ -219,7 +328,7 @@ function serializeSqlParameters(parameters: SqlParameters): cosmos.SqlParameter[ for (let i = 0; i < comparisonTargetValues.length; i++) { sqlParameters.push({ - name: `@${comparisonTarget}${i}`, + name: `@${comparisonTarget}_${i}`, value: comparisonTargetValues[i] }) } From 01800eecca2c21121b38243c218bff577b63610b Mon Sep 17 00:00:00 2001 From: Karthikeyan C Date: Fri, 22 Mar 2024 12:02:11 +0530 Subject: [PATCH 2/4] rename SqlQueryParts to SqlQueryContext --- src/execution.ts | 6 +++--- src/sqlGeneration.ts | 11 ++++------- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/src/execution.ts b/src/execution.ts index 372020c..8cd147a 100644 --- a/src/execution.ts +++ b/src/execution.ts @@ -25,7 +25,7 @@ function validateOrderBy(orderBy: sdk.OrderBy, collectionObjectType: schema.Obje } } -function parseQueryRequest(collectionsSchema: schema.CollectionsSchema, queryRequest: sdk.QueryRequest): sql.SqlQueryParts { +function parseQueryRequest(collectionsSchema: schema.CollectionsSchema, queryRequest: sdk.QueryRequest): sql.SqlQueryContext { let isAggregateQuery = false; const collection: string = queryRequest.collection; @@ -146,7 +146,7 @@ function parseQueryRequest(collectionsSchema: schema.CollectionsSchema, queryReq sourceAlias: `${collection[0]}`, }; - let sqlGenCtx: sql.SqlQueryParts = { + let sqlGenCtx: sql.SqlQueryContext = { select: requestedFields, from: fromClause, isAggregateQuery @@ -190,7 +190,7 @@ export async function executeQuery(queryRequest: sdk.QueryRequest, collectionsSc if (dbContainer === undefined || dbContainer == null) throw new sdk.InternalServerError(`Couldn't find the container '${collection}' in the schema.`) - const sqlGenCtx: sql.SqlQueryParts = parseQueryRequest(collectionsSchema, queryRequest); + const sqlGenCtx: sql.SqlQueryContext = parseQueryRequest(collectionsSchema, queryRequest); const sqlQuery = sql.generateSqlQuerySpec(sqlGenCtx, collection, queryRequest.variables); diff --git a/src/sqlGeneration.ts b/src/sqlGeneration.ts index bc525d0..4800ba9 100644 --- a/src/sqlGeneration.ts +++ b/src/sqlGeneration.ts @@ -62,7 +62,7 @@ export type ContainerExpression = { export type SqlExpression = { kind: 'sqlExpression', - sqlExpression: SqlQueryParts + sqlExpression: SqlQueryContext } export type ArrayJoinTarget = ContainerExpression | SqlExpression @@ -74,7 +74,7 @@ export type ArrayJoinClause = { export type JoinClause = ArrayJoinClause; // TODO: Handle the case of `JOIN ((SELECT VALUE t FROM t IN p.tags WHERE t.name IN ("winter", "fall")))`, if needed. -export type SqlQueryParts = { +export type SqlQueryContext = { select: SelectColumns, // TODO: Handle `SELECT VALUE` and `SELECT DISTINCT` here itself? If there is a need for it. from?: FromClause | null, join?: JoinClause[] | null, @@ -83,11 +83,8 @@ export type SqlQueryParts = { limit?: number | null, orderBy?: sdk.OrderBy | null, isAggregateQuery: boolean, - } - - type VariablesMappings = { /* The variableTarget will be the name of the column @@ -105,7 +102,7 @@ function formatJoinClause(joinClause: JoinClause): string { return `${joinClause.arrayJoinTarget} in ${joinTarget}` } -function constructSqlQuery(sqlQueryParts: SqlQueryParts, fromContainerAlias: string): cosmos.SqlQuerySpec { +function constructSqlQuery(sqlQueryParts: SqlQueryContext, fromContainerAlias: string): cosmos.SqlQuerySpec { let selectColumns = formatSelectColumns(sqlQueryParts.select); let fromClause = @@ -167,7 +164,7 @@ function constructSqlQuery(sqlQueryParts: SqlQueryParts, fromContainerAlias: str } } -export function generateSqlQuerySpec(sqlGenCtx: SqlQueryParts, containerName: string, queryVariables: QueryVariables): SqlQuerySpec { +export function generateSqlQuerySpec(sqlGenCtx: SqlQueryContext, containerName: string, queryVariables: QueryVariables): SqlQuerySpec { const querySpec = constructSqlQuery(sqlGenCtx, `${containerName[0]}`); From 667f83f869bfde5044e6ef769bbaa3bff88e1368 Mon Sep 17 00:00:00 2001 From: Karthikeyan C Date: Fri, 22 Mar 2024 12:27:26 +0530 Subject: [PATCH 3/4] use a better alias for the root container --- src/execution.ts | 4 ++-- src/sqlGeneration.ts | 20 ++++++++++++-------- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/src/execution.ts b/src/execution.ts index 8cd147a..c6a5c9e 100644 --- a/src/execution.ts +++ b/src/execution.ts @@ -32,7 +32,7 @@ function parseQueryRequest(collectionsSchema: schema.CollectionsSchema, queryReq const collectionDefinition: schema.CollectionDefinition = collectionsSchema.collections[collection]; - const rootContainerAlias = `${collection[0]}`; + const rootContainerAlias = `root_${collection}`; let requestedFields: sql.SelectColumns = {}; @@ -143,7 +143,7 @@ function parseQueryRequest(collectionsSchema: schema.CollectionsSchema, queryReq let fromClause: sql.FromClause = { source: collection, - sourceAlias: `${collection[0]}`, + sourceAlias: rootContainerAlias, }; let sqlGenCtx: sql.SqlQueryContext = { diff --git a/src/sqlGeneration.ts b/src/sqlGeneration.ts index 4800ba9..bb1cd44 100644 --- a/src/sqlGeneration.ts +++ b/src/sqlGeneration.ts @@ -110,18 +110,11 @@ function constructSqlQuery(sqlQueryParts: SqlQueryContext, fromContainerAlias: s ? null : `${sqlQueryParts.from.source} ${sqlQueryParts.from.sourceAlias}${sqlQueryParts.from.in ? ' IN' + sqlQueryParts.from.in : ''}`; - let joinClause = null; - - if (sqlQueryParts.join !== null && sqlQueryParts.join !== undefined) { - joinClause = "JOIN " + sqlQueryParts.join?.map(joinClause => formatJoinClause(joinClause)).join("\nJOIN ") - } - let whereClause = null; let predicateParameters: SqlParameters = {}; let utilisedVariables: VariablesMappings = {}; // This will be used to add the join mappings to the where expression. - if (sqlQueryParts.predicate != null && sqlQueryParts.predicate != undefined) { const whereExp = visitExpression(predicateParameters, utilisedVariables, sqlQueryParts.predicate, fromContainerAlias); @@ -130,6 +123,15 @@ function constructSqlQuery(sqlQueryParts: SqlQueryContext, fromContainerAlias: s } + let joinClause = null; + + if (sqlQueryParts.join !== null && sqlQueryParts.join !== undefined) { + joinClause = "JOIN " + sqlQueryParts.join?.map(joinClause => formatJoinClause(joinClause)).join("\nJOIN ") + } + + + + let orderByClause = null; if (sqlQueryParts.orderBy != null && sqlQueryParts.orderBy != null && sqlQueryParts.orderBy.elements.length > 0) { @@ -166,7 +168,9 @@ function constructSqlQuery(sqlQueryParts: SqlQueryContext, fromContainerAlias: s export function generateSqlQuerySpec(sqlGenCtx: SqlQueryContext, containerName: string, queryVariables: QueryVariables): SqlQuerySpec { - const querySpec = constructSqlQuery(sqlGenCtx, `${containerName[0]}`); + const querySpec = constructSqlQuery(sqlGenCtx, `root_${containerName}`); + + console.log(JSON.stringify(querySpec, null, 2)); return querySpec From 64e96160a0e900455c0a7cda4f6274c582f03ed0 Mon Sep 17 00:00:00 2001 From: Karthikeyan C Date: Fri, 22 Mar 2024 16:09:56 +0530 Subject: [PATCH 4/4] construct SQL query with the synthetic table --- src/execution.ts | 5 ++-- src/sqlGeneration.ts | 64 +++++++++++++++++++++++++++++++++----------- 2 files changed, 51 insertions(+), 18 deletions(-) diff --git a/src/execution.ts b/src/execution.ts index c6a5c9e..56b5919 100644 --- a/src/execution.ts +++ b/src/execution.ts @@ -1,7 +1,7 @@ import * as sdk from "@hasura/ndc-sdk-typescript"; import * as schema from "./schema"; import * as sql from "./sqlGeneration"; -import { Database } from "@azure/cosmos"; +import { Database, SqlQuerySpec } from "@azure/cosmos"; import { runSQLQuery } from "./cosmosDb"; @@ -149,7 +149,8 @@ function parseQueryRequest(collectionsSchema: schema.CollectionsSchema, queryReq let sqlGenCtx: sql.SqlQueryContext = { select: requestedFields, from: fromClause, - isAggregateQuery + isAggregateQuery, + selectAsValue: false, }; if (queryRequest.query.limit != null) { diff --git a/src/sqlGeneration.ts b/src/sqlGeneration.ts index bb1cd44..6e29783 100644 --- a/src/sqlGeneration.ts +++ b/src/sqlGeneration.ts @@ -75,7 +75,9 @@ export type ArrayJoinClause = { export type JoinClause = ArrayJoinClause; // TODO: Handle the case of `JOIN ((SELECT VALUE t FROM t IN p.tags WHERE t.name IN ("winter", "fall")))`, if needed. export type SqlQueryContext = { - select: SelectColumns, // TODO: Handle `SELECT VALUE` and `SELECT DISTINCT` here itself? If there is a need for it. + select: SelectColumns, + /* Set to `true` to prevent the wrapping of the results into another JSON object. */ + selectAsValue: boolean, from?: FromClause | null, join?: JoinClause[] | null, predicate?: sdk.Expression | null, @@ -97,12 +99,12 @@ function formatJoinClause(joinClause: JoinClause): string { let joinTarget = joinClause.arrayJoinTarget.kind === 'containerExpression' ? joinClause.arrayJoinTarget.containerExpression - : constructSqlQuery(joinClause.arrayJoinTarget.sqlExpression, joinClause.joinIdentifier); + : constructSqlQuery(joinClause.arrayJoinTarget.sqlExpression, joinClause.joinIdentifier, null); - return `${joinClause.arrayJoinTarget} in ${joinTarget}` + return `${joinClause.joinIdentifier} in (${joinTarget})` } -function constructSqlQuery(sqlQueryParts: SqlQueryContext, fromContainerAlias: string): cosmos.SqlQuerySpec { +function constructSqlQuery(sqlQueryParts: SqlQueryContext, fromContainerAlias: string, queryVariables: QueryVariables): cosmos.SqlQuerySpec { let selectColumns = formatSelectColumns(sqlQueryParts.select); let fromClause = @@ -114,23 +116,52 @@ function constructSqlQuery(sqlQueryParts: SqlQueryContext, fromContainerAlias: s let predicateParameters: SqlParameters = {}; let utilisedVariables: VariablesMappings = {}; // This will be used to add the join mappings to the where expression. + let parameters: cosmos.SqlParameter[] = []; + + + if (sqlQueryParts.predicate != null && sqlQueryParts.predicate != undefined) { const whereExp = visitExpression(predicateParameters, utilisedVariables, sqlQueryParts.predicate, fromContainerAlias); - // TODO: incorporate the `predicateParameters` obtained above. - whereClause = `WHERE ${whereExp}`; - } + whereClause = `WHERE ${whereExp}` - let joinClause = null; + parameters = serializeSqlParameters(predicateParameters); + + console.log("utilised vairables are ", JSON.stringify(utilisedVariables, null, 2)); + + + if (Object.keys(utilisedVariables).length > 0) { + if (queryVariables === null || queryVariables === undefined) { + throw new sdk.BadRequest(`The variables (${JSON.stringify(Object.values(utilisedVariables))}) were referenced in the variable, but their values were not provided`) + } else { + + console.log("query variables as JSON value", JSON.stringify(queryVariables as cosmos.JSONValue, null, 2)); + parameters.push({ + name: '@vars', + value: queryVariables as cosmos.JSONValue + }); + } - if (sqlQueryParts.join !== null && sqlQueryParts.join !== undefined) { - joinClause = "JOIN " + sqlQueryParts.join?.map(joinClause => formatJoinClause(joinClause)).join("\nJOIN ") + } } + let joinClause = null; + + if (Object.keys(utilisedVariables).length > 0) { + let variablesJoinTarget: ArrayJoinTarget = { + kind: 'containerExpression', + containerExpression: 'SELECT VALUE @vars' + }; + let joinExp: JoinClause = { + joinIdentifier: "vars", + arrayJoinTarget: variablesJoinTarget, + }; + joinClause = `JOIN ${formatJoinClause(joinExp)}` + } let orderByClause = null; @@ -152,7 +183,7 @@ function constructSqlQuery(sqlQueryParts: SqlQueryContext, fromContainerAlias: s } const query = - `SELECT ${selectColumns} + `SELECT ${sqlQueryParts.selectAsValue ? 'VALUE' : ''} ${selectColumns} ${fromClause ? 'FROM ' + fromClause : ''} ${joinClause ?? ''} ${whereClause ?? ''} @@ -160,17 +191,18 @@ function constructSqlQuery(sqlQueryParts: SqlQueryContext, fromContainerAlias: s ${offsetClause ? 'OFFSET ' + offsetClause : ''} ${limitClause ? 'LIMIT ' + limitClause : ''}`; + return { query, - parameters: serializeSqlParameters(predicateParameters) + parameters } } export function generateSqlQuerySpec(sqlGenCtx: SqlQueryContext, containerName: string, queryVariables: QueryVariables): SqlQuerySpec { - const querySpec = constructSqlQuery(sqlGenCtx, `root_${containerName}`); + const querySpec = constructSqlQuery(sqlGenCtx, `root_${containerName}`, queryVariables); - console.log(JSON.stringify(querySpec, null, 2)); + console.log(querySpec.query, JSON.stringify(querySpec.parameters, null, 2)); return querySpec @@ -315,8 +347,8 @@ export function visitComparisonValue(parameters: SqlParameters, variables: Varia case 'column': throw new sdk.NotSupported("Column comparisons are not supported in field predicates yet"); case 'variable': - variables[comparisonTarget] = `vars.${target.name} ` - return `vars.${target.name} ` + variables[comparisonTarget] = `vars["${target.name}"]` + return `vars["${target.name}"]` } }