Skip to content

Commit

Permalink
Merge pull request #17 from singlestore-labs/PLAT-7159
Browse files Browse the repository at this point in the history
Added tests, fixed bugs
  • Loading branch information
AdalbertMemSQL authored Sep 10, 2024
2 parents 2afe315 + 823fcba commit 6de29ac
Show file tree
Hide file tree
Showing 4,426 changed files with 296,311 additions and 69 deletions.
The diff you're trying to view is too large. We only load the first 3000 changed files.
13 changes: 11 additions & 2 deletions src/sql/AliasGenerator.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,16 @@
export class AliasGenerator {
abstract class AliasGenerator {
private aliasCounter: number = 0
protected abstract prefix: string

newAlias() {
return `table_${++this.aliasCounter}`;
return `${this.prefix}_${++this.aliasCounter}`;
}
}

export class RowsetAliasGenerator extends AliasGenerator {
prefix = "rowset"
}

export class ColumnAliasGenerator extends AliasGenerator {
prefix = "column"
}
166 changes: 99 additions & 67 deletions src/sql/SingleStoreQueryBuilder.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Aggregate, BadRequest, ComparisonTarget, ComparisonValue, ExistsInCollection, Expression, Field, OrderBy, OrderByElement, PathElement, Query, Relationship } from "@hasura/ndc-sdk-typescript"
import { Aggregate, BadRequest, ComparisonTarget, ComparisonValue, ExistsInCollection, Expression, Field, NotSupported, OrderBy, OrderByElement, PathElement, Query, Relationship } from "@hasura/ndc-sdk-typescript"
import { Configuration } from ".."
import { SingleStoreQuery } from "./SingleStoreQuery"
import { AliasGenerator } from "./AliasGenerator"
import { RowsetAliasGenerator, ColumnAliasGenerator } from "./AliasGenerator"

export class SingleStoreQueryBuilder {
configuration: Configuration
Expand All @@ -12,11 +12,13 @@ export class SingleStoreQueryBuilder {
parentRelationship: Relationship | null
aggregateRowsToObject = true
defaultOrderByColumn: string | null = null
aliasGenerator: AliasGenerator = new AliasGenerator()
rowsetAliasGenerator: RowsetAliasGenerator = new RowsetAliasGenerator()
columnAliasGenerator: ColumnAliasGenerator = new ColumnAliasGenerator()

subqueries: { [k: string]: SingleStoreQuery } = {}
orderByElementToSubqueryAlias: Map<OrderByElement, string> = new Map<OrderByElement, string>()
comparisonTargetToSubqueryAlias: Map<ComparisonTarget, string> = new Map<ComparisonTarget, string>()
orderByElementAlias: Map<OrderByElement, string> = new Map<OrderByElement, string>()
sqlParts: string[] = []
parameters: any[] = []

Expand Down Expand Up @@ -46,7 +48,7 @@ export class SingleStoreQueryBuilder {
if (this.aggregateRowsToObject) {
this.defaultOrderByColumn = "row"
}
this.sqlParts.push(this.selectFields(query.fields))
this.sqlParts.push(this.selectFields(query.fields, query.order_by))
} else {
throw new BadRequest("Neither aggregates nor fields are specified")
}
Expand Down Expand Up @@ -106,30 +108,67 @@ export class SingleStoreQueryBuilder {
}
}

private selectFields(fields: { [k: string]: Field }): string {
return `SELECT JSON_BUILD_OBJECT(${Object.entries(fields)
.map(([fieldName, field]): string => {
switch (field.type) {
case "column":
// TODO: escape
return `'${fieldName}', ${this.collection}.${field.column}`
case "relationship":
const tableName = this.aliasGenerator.newAlias()
const relationship = this.relationships[field.relationship]
this.subqueries[tableName] = new SingleStoreQueryBuilder(
this.configuration,
this.variables,
relationship.target_collection,
this.relationships,
this.collection,
relationship
).build(field.query)
private visitSelectField(field: Field): string {
switch (field.type) {
case "column":
// TODO: escape
return `${this.collection}.${field.column}`
case "relationship":
const tableName = this.rowsetAliasGenerator.newAlias()
const relationship = this.relationships[field.relationship]

// TODO: escape
return `'${fieldName}', ${tableName}.data`
this.subqueries[tableName] = new SingleStoreQueryBuilder(
this.configuration,
this.variables,
relationship.target_collection,
this.relationships,
this.collection,
relationship
).build(field.query)

// TODO: escape
return `${tableName}.data`
}
}

private selectFields(fields: { [k: string]: Field }, orderBy: OrderBy | null | undefined): string {
const selectElements = []

if (this.aggregateRowsToObject) {
selectElements.push(`JSON_BUILD_OBJECT(${Object.entries(fields)
.map(([fieldName, field]): string => {
const column = this.visitSelectField(field)
return `'${fieldName}', ${column}`
}).join(", ")}) AS row`)
} else {
Object.entries(fields).forEach(([fieldName, field]) => {
const column = this.visitSelectField(field)
selectElements.push(`${column} AS ${fieldName}`)
})
}

orderBy?.elements.forEach(element => {
const alias = this.columnAliasGenerator.newAlias()
this.orderByElementAlias.set(element, alias)

if (element.target.path.length > 0) {
// TODO escape
selectElements.push(`${this.orderByElementToSubqueryAlias.get(element)}.order_expr AS ${alias}`);
} else {
switch (element.target.type) {
case 'column':
// TODO escape
selectElements.push(`${this.collection}.${element.target.name} AS ${alias}`);
break
case 'single_column_aggregate':
throw new BadRequest("Empty path for single_column_aggregate orderby element");
case 'star_count_aggregate':
throw new BadRequest("Empty path for star_column_aggregate orderby element");
}
}).join(", ")}) AS row`
}
})

return `SELECT ${selectElements.join(", ")}`
}

private subqueriesFromOrderBy(orderBy?: OrderBy | null) {
Expand Down Expand Up @@ -157,35 +196,35 @@ export class SingleStoreQueryBuilder {
this.relationships,
this.collection,
rel,
false
target.type == "star_count_aggregate"
).build(query)

var tableName = this.aliasGenerator.newAlias()
var tableName = this.rowsetAliasGenerator.newAlias()
switch (target.type) {
case "column":
// TODO: escape
subquery = new SingleStoreQuery(
`SELECT ANY_VALUE(${tableName}.${target.name}) AS order_expr FROM ((${subquery.sql}) AS ${tableName})`,
`SELECT ANY_VALUE(${tableName}.${colName}) AS order_expr FROM((${subquery.sql}) AS ${tableName})`,
subquery.parameters
)
break;
case "single_column_aggregate":
// TODO: escape
subquery = new SingleStoreQuery(
`SELECT ${this.mapAggregate(target.function)}(${tableName}.${target.column}) AS order_expr FROM ((${subquery}) AS ${tableName})`,
`SELECT ${this.mapAggregate(target.function)}(${tableName}.${colName}) AS order_expr FROM((${subquery.sql}) AS ${tableName})`,
subquery.parameters
)
break;
case "star_count_aggregate":
// TODO: escape
subquery = new SingleStoreQuery(
`SELECT COUNT(*) AS order_expr FROM ((${subquery}) AS ${tableName})`,
`SELECT COUNT(*) AS order_expr FROM((${subquery.sql}) AS ${tableName})`,
subquery.parameters
)
break;
}

tableName = this.aliasGenerator.newAlias()
tableName = this.rowsetAliasGenerator.newAlias()
this.subqueries[tableName] = subquery
this.orderByElementToSubqueryAlias.set(element, tableName)
})
Expand All @@ -194,7 +233,7 @@ export class SingleStoreQueryBuilder {
private pathToQuery(path: PathElement[], fieldName: string): Query {
var res: Query = {
fields: {
fieldName: {
[fieldName]: {
type: "column",
column: fieldName
}
Expand All @@ -205,7 +244,7 @@ export class SingleStoreQueryBuilder {
for (let i = path.length - 1; i >= 1; i--) {
res = {
fields: {
fieldName: {
[fieldName]: {
type: "relationship",
query: res,
relationship: path[i].relationship,
Expand All @@ -227,7 +266,7 @@ export class SingleStoreQueryBuilder {
private join() {
Object.keys(this.subqueries).forEach(alias => {
const subquery = this.subqueries[alias]
this.parameters.push(subquery.parameters)
this.parameters = this.parameters.concat(subquery.parameters)
this.sqlParts.push(`LEFT OUTER JOIN LATERAL (
${subquery.sql}
) AS ${alias} ON TRUE`)
Expand Down Expand Up @@ -301,16 +340,16 @@ ${subquery.sql}
const relationship: Relationship = this.relationships[collectionInfo.relationship]

return `EXISTS (
SELECT 1 FROM ${relationship.target_collection}
SELECT 1 FROM ${relationship.target_collection}
${this.visitWhere(relationship.target_collection, expression.predicate, collection, relationship)}
LIMIT 1
)`
)`
case "unrelated":
return `EXISTS (
SELECT 1 FROM ${collectionInfo.collection}
${this.visitWhere(collection, expression.predicate, null, null)}
SELECT 1 FROM ${collectionInfo.collection}
${this.visitWhere(collectionInfo.collection, expression.predicate, null, null)}
LIMIT 1
)`
)`
default:
throw new BadRequest("Unknown exists type");
}
Expand All @@ -330,8 +369,7 @@ LIMIT 1
return `${collection}.${target.name}`;
}
case 'root_collection_column':
// TODO: escape
return `${this.collection}.${target.name}`;
throw new NotSupported("Referencing root collection is not supported")
}
}

Expand Down Expand Up @@ -396,14 +434,14 @@ LIMIT 1
false
).build(query)

var tableName = this.aliasGenerator.newAlias()
var tableName = this.rowsetAliasGenerator.newAlias()
subquery = new SingleStoreQuery(
// TODO escape
`SELECT ANY_VALUE(${tableName}.${target.name}) AS comp_expr FROM ((${subquery.sql}) AS ${tableName})`,
subquery.parameters
)

tableName = this.aliasGenerator.newAlias()
tableName = this.rowsetAliasGenerator.newAlias()
this.subqueries[tableName] = subquery
this.comparisonTargetToSubqueryAlias.set(target, tableName)
}
Expand All @@ -416,34 +454,26 @@ LIMIT 1
}

private orderBy(orderBy?: OrderBy | null) {
this.sqlParts.push(this.visitOrderBy(orderBy));
}

private visitOrderBy(orderBy?: OrderBy | null): string {
if (orderBy && orderBy.elements.length > 0) {
this.sqlParts.push(`ORDER BY ${orderBy.elements.map(element => {
return `ORDER BY ${orderBy.elements.map(element => {
const direction = element.order_direction === 'asc' ? 'ASC' : 'DESC';
if (element.target.path.length > 0) {
// TODO escape
return `${this.orderByElementToSubqueryAlias.get(element)}.order_expr ${direction} `;
} else {
switch (element.target.type) {
case 'column':
// TODO escape
return `${this.collection}.${element.target.name} ${direction} `;
case 'single_column_aggregate':
throw new BadRequest("Empty path for single_column_aggregate orderby element");
case 'star_count_aggregate':
throw new BadRequest("Empty path for star_column_aggregate orderby element");
}
}
}).join(", ")}`)
return `${this.orderByElementAlias.get(element)} ${direction} `;
}).join(", ")
} ${this.defaultOrderByColumn ? `, ${this.defaultOrderByColumn}` : ""} `
} else if (this.defaultOrderByColumn) {
this.sqlParts.push(`ORDER BY ${this.defaultOrderByColumn}`)
return `ORDER BY ${this.defaultOrderByColumn} `
} else {
return "";
}
}

private limit(limit?: number | null) {
if (limit) {
this.sqlParts.push(`LIMIT ${limit}`)
} else {
this.sqlParts.push(`LIMIT ${Number.MAX_SAFE_INTEGER}`)
}
}

Expand Down Expand Up @@ -533,9 +563,10 @@ LIMIT 1
* [ 1 ]
*/
build(query: Query): SingleStoreQuery {
this.select(query)
this.subqueriesFromOrderBy(query.order_by)
this.subqueriesFromExpression(query.predicate)

this.select(query)
this.from()
this.join()
this.where(query.predicate)
Expand All @@ -545,11 +576,12 @@ LIMIT 1

var sql = this.sqlParts.join('\n')

const table = this.rowsetAliasGenerator.newAlias()
if (this.aggregateRowsToObject) {
sql = `SELECT JSON_BUILD_OBJECT('rows', JSON_AGG(row)) AS data
FROM (
sql = `SELECT JSON_BUILD_OBJECT('rows', JSON_AGG(row ${this.visitOrderBy(query.order_by)})) AS data
FROM(
${sql}
)`
) AS ${table}`
}

return {
Expand Down
18 changes: 18 additions & 0 deletions test-snapshots/capabilities
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"version": "0.1.2",
"capabilities": {
"query": {
"aggregates": {},
"variables": {},
"explain": {}
},
"mutation": {
"transactional": {},
"explain": {}
},
"relationships": {
"relation_comparisons": {},
"order_by_aggregate": {}
}
}
}
5 changes: 5 additions & 0 deletions test-snapshots/query/10009cedee9a81be/expected.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[
{
"rows": []
}
]
Loading

0 comments on commit 6de29ac

Please sign in to comment.