blob: fe91f0368745380ad364e5a2bf1c7c82115e1ab7 [file]
// Copyright (C) 2025 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import {arrayEquals} from '../../../../base/array_utils';
import type {SortDirection} from '../../../../base/comparison_utils';
// A column in the SQL query. It can be either be:
// - A column in the table (represented by a string).
// - An expression,computing the value based on other columns (represented by SqlExpression).
// - A derived column: `column` from `source.table`, with `source.joinOn` describing
// how `source.table` should be joined with the primary table (usually join on ID).
export type SqlColumn =
| string
| SqlExpression
| {
column: string;
source: SourceTable;
// Optional unique id for this column, which will be shown to the user (e.g. in column header and filters).
id?: string;
};
// A class representing a SQL column which is computed based on other columns.
export class SqlExpression {
// op: Operation, which takes the expressions for columns and returns a valid SQL expression.
// columns: List of columns that the operation references.
// id: optional unique id for this column, which will be shown to the user (e.g. in column header and filters).
constructor(
public op: (cols: string[]) => string,
public columns: SqlColumn[],
public id?: string,
) {
this.op = op;
}
}
// A source table for a derived SQL column, describining the additional table to be joined with the primary source table and the join contraints.
export type SourceTable = {
table: string;
joinOn: {[key: string]: SqlColumn};
// Whether more performant 'INNER JOIN' can be used instead of 'LEFT JOIN'.
// Special care should be taken to ensure that a) all rows exist in a target table, and b) the source is not null, otherwise the rows will be filtered out.
// false by default.
innerJoin?: boolean;
};
// A unique identifier for the SQL column.
export function sqlColumnId(column: SqlColumn): string {
// For table columns, use the column name as an id.
if (typeof column === 'string') {
return column;
}
// For expressions, use the specified id, or plug the ids of the columns into the expression.
if (column instanceof SqlExpression) {
if (column.id !== undefined) return column.id;
return `${column.op(column.columns.map(sqlColumnId))}`;
}
if (column.id !== undefined) {
return column.id;
}
// Special case: If the join is performed on a single column `id`, we can use a simpler representation (i.e. `table[id].column`).
if (arrayEquals(Object.keys(column.source.joinOn), ['id'])) {
return `${column.source.table}[${sqlColumnId(Object.values(column.source.joinOn)[0])}].${column.column}`;
}
// Otherwise, we need to list all the join constraints.
const lookup = Object.entries(column.source.joinOn)
.map(([key, value]): string => {
const valueStr = sqlColumnId(value);
if (key === valueStr) return key;
return `${key}=${sqlColumnId(value)}`;
})
.join(', ');
return `${column.source.table}[${lookup}].${column.column}`;
}
export function isSqlColumnEqual(a: SqlColumn, b: SqlColumn): boolean {
return sqlColumnId(a) === sqlColumnId(b);
}
// A column order clause, which specifies the column and the direction in which it should be sorted.
export interface ColumnOrderClause {
column: SqlColumn;
direction: SortDirection;
}