Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add value in lateral and sub-query support #153

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions db.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export const isDatabaseError = mod.isDatabaseError;
export const mapWithSeparator = mod.mapWithSeparator;
export const max = mod.max;
export const min = mod.min;
export const nested = mod.nested;
export const param = mod.param;
export const parent = mod.parent;
export const raw = mod.raw;
Expand Down
4 changes: 2 additions & 2 deletions src/db/conditions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,8 @@ export const reImatch = <T extends string>(a: T) => sql<SQL, boolean | null, T>`
export const notReMatch = <T extends string>(a: T) => sql<SQL, boolean | null, T>`${self} !~ ${conditionalParam(a)}`;
export const notReImatch = <T extends string>(a: T) => sql<SQL, boolean | null, T>`${self} !~* ${conditionalParam(a)}`;

export const isIn = <T>(a: readonly T[]) => a.length > 0 ? sql<SQL, boolean | null, T>`${self} IN (${vals(a)})` : sql`false`;
export const isNotIn = <T>(a: readonly T[]) => a.length > 0 ? sql<SQL, boolean | null, T>`${self} NOT IN (${vals(a)})` : sql`true`;
export const isIn = <T>(a: readonly T[]) => 'run' in a ? sql`${self} IN ${a}` : a.length > 0 ? sql<SQL, boolean | null, T>`${self} IN (${vals(a)})` : sql`false`;
export const isNotIn = <T>(a: readonly T[]) => 'run' in a ? sql`${self} NOT IN ${a}` : a.length > 0 ? sql<SQL, boolean | null, T>`${self} NOT IN (${vals(a)})` : sql`true`;

export const or = <T>(...conditions: SQLFragment<any, T>[] | Whereable[]) => sql<SQL, boolean | null, T>`(${mapWithSeparator(conditions, sql` OR `, c => c)})`;
export const and = <T>(...conditions: SQLFragment<any, T>[] | Whereable[]) => sql<SQL, boolean | null, T>`(${mapWithSeparator(conditions, sql` AND `, c => c)})`;
Expand Down
135 changes: 100 additions & 35 deletions src/db/shortcuts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -408,7 +408,10 @@ export interface SelectOptionsForTable<
offset?: number;
withTies?: boolean;
columns?: C;
column?: ColumnForTable<T>;
array?: ColumnForTable<T>;
extras?: E;
extra?: SQLFragment<any>;
groupBy?: ColumnForTable<T> | ColumnForTable<T>[] | SQLFragment<any>;
having?: WhereableForTable<T> | SQLFragment<any>;
lateral?: L;
Expand All @@ -427,7 +430,7 @@ type SelectReturnTypeForTable<
L extends SQLFragment<any> ? RunResultForSQLFragment<L> :
never);

export enum SelectResultMode { Many, One, ExactlyOne, Numeric }
export enum SelectResultMode { Many, One, ExactlyOne, Numeric, Boolean, Number, String, BooleanArray, NumberArray, StringArray }

export type FullSelectReturnTypeForTable<
T extends Table,
Expand All @@ -437,11 +440,17 @@ export type FullSelectReturnTypeForTable<
M extends SelectResultMode,
> =
{
[SelectResultMode.Many]: SelectReturnTypeForTable<T, C, L, E>[];
[SelectResultMode.ExactlyOne]: SelectReturnTypeForTable<T, C, L, E>;
[SelectResultMode.One]: SelectReturnTypeForTable<T, C, L, E> | undefined;
[SelectResultMode.Numeric]: number;
}[M];
[SelectResultMode.Many]: SelectReturnTypeForTable<T, C, L, E>[];
[SelectResultMode.ExactlyOne]: SelectReturnTypeForTable<T, C, L, E>;
[SelectResultMode.One]: SelectReturnTypeForTable<T, C, L, E> | undefined;
[SelectResultMode.Numeric]: number;
[SelectResultMode.Boolean]: boolean;
[SelectResultMode.Number]: number;
[SelectResultMode.String]: string;
[SelectResultMode.BooleanArray]: boolean[];
[SelectResultMode.NumberArray]: number[];
[SelectResultMode.StringArray]: string[];
}[M];

export interface SelectSignatures {
<T extends Table,
Expand Down Expand Up @@ -478,18 +487,21 @@ export class NotExactlyOneError extends Error {
* or `all`
* @param options Options object. Keys (all optional) are:
* * `columns` — an array of column names: only these columns will be returned
* * `order` – an array of `OrderSpec` objects, such as
* `{ by: 'column', direction: 'ASC' }`
* * `column` — a single column name for nested queries
* * `order` – an array of `OrderSpec` objects, such as
* `{ by: 'column', direction: 'ASC' }`
* * `limit` and `offset` – numbers: apply this limit and offset to the query
* * `lateral` — either an object mapping keys to nested `select`/`selectOne`/
* `count` queries to be `LATERAL JOIN`ed, or a single `select`/`selectOne`/
* `count` query whose result will be passed through directly as the result of
* the containing query
* * `alias` — table alias (string): required if using `lateral` to join a table
* to itself
* * `extras` — an object mapping key(s) to `SQLFragment`s, so that derived
* * `extras` — an object mapping key(s) to `SQLFragment`s, so that derived
* * `extra` — a single extra name for nested queries
* * `array` — a single column name to be concatenated with array_agg in nested queries
* quantities can be included in the JSON result
* @param mode (Used internally by `selectOne` and `count`)
* @param mode (Used internally by `selectOne`, `count` and sub-queries)
*/
export const select: SelectSignatures = function (
table: Table,
Expand All @@ -498,26 +510,32 @@ export const select: SelectSignatures = function (
mode: SelectResultMode = SelectResultMode.Many,
aggregate: string = 'count',
) {

const
limit1 = mode === SelectResultMode.One || mode === SelectResultMode.ExactlyOne,
const limit1 =
mode === SelectResultMode.Boolean ||
mode === SelectResultMode.Number ||
mode === SelectResultMode.String ||
mode === SelectResultMode.One ||
mode === SelectResultMode.ExactlyOne,
allOptions = limit1 ? { ...options, limit: 1 } : options,
alias = allOptions.alias || table,
{ distinct, groupBy, having, lateral, columns, extras } = allOptions,
{ distinct, groupBy, having, lateral, columns, column, extras, extra, array } = allOptions,
lock = allOptions.lock === undefined || Array.isArray(allOptions.lock) ? allOptions.lock : [allOptions.lock],
order = allOptions.order === undefined || Array.isArray(allOptions.order) ? allOptions.order : [allOptions.order],
tableAliasSQL = alias === table ? [] : sql<string>` AS ${alias}`,
distinctSQL = !distinct ? [] : sql` DISTINCT${distinct instanceof SQLFragment || typeof distinct === 'string' ? sql` ON (${distinct})` :
Array.isArray(distinct) ? sql` ON (${cols(distinct)})` : []}`,
colsSQL = lateral instanceof SQLFragment ? [] :
mode === SelectResultMode.Numeric ?
colsSQL = lateral instanceof SQLFragment || extra ? [] :
mode === SelectResultMode.Numeric ?
(columns ? sql`${raw(aggregate)}(${cols(columns)})` : sql`${raw(aggregate)}(${alias}.*)`) :
SQLForColumnsOfTable(columns, alias as Table),
colsExtraSQL = lateral instanceof SQLFragment || mode === SelectResultMode.Numeric ? [] : SQLForExtras(extras),
colsLateralSQL = lateral === undefined || mode === SelectResultMode.Numeric ? [] :
lateral instanceof SQLFragment ? sql`"lateral_passthru".result` :
array ? sql`array_agg(${array})` :
column ? sql`${column}` :
SQLForColumnsOfTable(columns as Column[], alias as Table),
colsExtraSQL = lateral instanceof SQLFragment || mode === SelectResultMode.Numeric ? [] : extra ? sql`${extra}` : SQLForExtras(extras),
colsLateralSQL =
lateral === undefined || mode === SelectResultMode.Numeric ? [] :
lateral instanceof SQLFragment ? sql`"lateral_passthru".result` :
sql` || jsonb_build_object(${mapWithSeparator(
Object.keys(lateral).sort(), sql`, `, k => sql`${param(k)}::text, "lateral_${raw(k)}".result`)})`,
Object.keys(lateral).sort(), sql`, `, k => sql`${param(k)}::text, "lateral_${raw(k)}".result`)})`,
allColsSQL = sql`${colsSQL}${colsExtraSQL}${colsLateralSQL}`,
whereSQL = where === all ? [] : sql` WHERE ${where}`,
groupBySQL = !groupBy ? [] : sql` GROUP BY ${groupBy instanceof SQLFragment || typeof groupBy === 'string' ? groupBy : cols(groupBy)}`,
Expand Down Expand Up @@ -551,10 +569,13 @@ export const select: SelectSignatures = function (

const
rowsQuery = sql<SQL, any>`SELECT${distinctSQL} ${allColsSQL} AS result FROM ${table}${tableAliasSQL}${lateralSQL}${whereSQL}${groupBySQL}${havingSQL}${orderSQL}${limitSQL}${offsetSQL}${lockSQL}`,
query = mode !== SelectResultMode.Many ? rowsQuery :
// we need the aggregate to sit in a sub-SELECT in order to keep ORDER and LIMIT working as usual
sql<SQL, any>`SELECT coalesce(jsonb_agg(result), '[]') AS result FROM (${rowsQuery}) AS ${raw(`"sq_${alias}"`)}`;

query = mode !== SelectResultMode.Many &&
mode !== SelectResultMode.BooleanArray &&
mode !== SelectResultMode.NumberArray &&
mode !== SelectResultMode.StringArray ? rowsQuery :
column || array ? sql<SQL, any>`${rowsQuery}` :
// we need the aggregate to sit in a sub-SELECT in order to keep ORDER and LIMIT working as usual
sql<SQL, any>`SELECT coalesce(jsonb_agg(result), '[]') AS result FROM (${rowsQuery}) AS ${raw(`"sq_${alias}"`)}`;
query.runResultTransform =

mode === SelectResultMode.Numeric ?
Expand All @@ -568,7 +589,7 @@ export const select: SelectSignatures = function (
if (result === undefined) throw new NotExactlyOneError(query, 'One result expected but none returned (hint: check `.query.compile()` on this Error)');
return result;
} :
// SelectResultMode.One or SelectResultMode.Many
// SelectResultMode.One or SelectResultMode.Many or types of subqueries results
(qr) => qr.rows[0]?.result;

return query;
Expand All @@ -584,11 +605,26 @@ export interface SelectOneSignatures {
L extends LateralOption<C, E>,
E extends ExtrasOption<T>,
A extends string,
M extends SelectResultMode = SelectResultMode.One
>(
table: T,
where: WhereableForTable<T> | SQLFragment<any> | AllType,
options?: SelectOptionsForTable<T, C, L, E, A>,
mode?: null
): SQLFragment<FullSelectReturnTypeForTable<T, C, L, E, M>>;
<
T extends Table,
C extends ColumnsOption<T>,
L extends LateralOption<C, E>,
E extends ExtrasOption<T>,
A extends string,
M extends SelectResultMode
>(
table: T,
where: WhereableForTable<T> | SQLFragment<any> | AllType,
options?: SelectOptionsForTable<T, C, L, E, A>,
): SQLFragment<FullSelectReturnTypeForTable<T, C, L, E, SelectResultMode.One>>;
mode?: M
): SQLFragment<FullSelectReturnTypeForTable<T, C, L, E, M>>;
}

/**
Expand All @@ -598,17 +634,17 @@ export interface SelectOneSignatures {
* @param table The table to select from
* @param where A `Whereable` or `SQLFragment` defining the rows to be selected,
* or `all`
* @param options Options object. See documentation for `select` for details.
* @param mode Type of the value returned by a subquery, default to SelectResultMode.One
*/
export const selectOne: SelectOneSignatures = function (table, where, options = {}) {
// you might argue that 'selectOne' offers little that you can't get with
// destructuring assignment and plain 'select'
export const selectOne: SelectOneSignatures = function (table, where, options = {}, mode) {
// you might argue that 'selectOne' offers little that you can't get with
// destructuring assignment and plain 'select'
// -- e.g.let[x] = async select(...).run(pool); -- but something worth having
// is '| undefined' in the return signature, because the result of indexing
// never includes undefined (until 4.1 and --noUncheckedIndexedAccess)
// (see https://github.com/Microsoft/TypeScript/issues/13778)

return select(table, where, options, SelectResultMode.One);
return select(table, where, options, mode ?? SelectResultMode.One);
};


Expand All @@ -621,11 +657,26 @@ export interface SelectExactlyOneSignatures {
L extends LateralOption<C, E>,
E extends ExtrasOption<T>,
A extends string,
M extends SelectResultMode = SelectResultMode.ExactlyOne
>(
table: T,
where: WhereableForTable<T> | SQLFragment<any> | AllType,
options?: SelectOptionsForTable<T, C, L, E, A>,
): SQLFragment<FullSelectReturnTypeForTable<T, C, L, E, SelectResultMode.ExactlyOne>>;
mode?: null
): SQLFragment<FullSelectReturnTypeForTable<T, C, L, E, M>>;
<
T extends Table,
C extends ColumnsOption<T>,
L extends LateralOption<C, E>,
E extends ExtrasOption<T>,
A extends string,
M extends SelectResultMode
>(
table: T,
where: WhereableForTable<T> | SQLFragment<any> | AllType,
options?: SelectOptionsForTable<T, C, L, E, A>,
mode?: M
): SQLFragment<FullSelectReturnTypeForTable<T, C, L, E, M>>;
}

/**
Expand All @@ -637,10 +688,11 @@ export interface SelectExactlyOneSignatures {
* @param where A `Whereable` or `SQLFragment` defining the rows to be selected,
* or `all`
* @param options Options object. See documentation for `select` for details.
* @param mode Type of the value returned by a subquery, default to SelectResultMode.ExactlyOne
*/

export const selectExactlyOne: SelectExactlyOneSignatures = function (table, where, options = {}) {
return select(table, where, options, SelectResultMode.ExactlyOne);
export const selectExactlyOne: SelectExactlyOneSignatures = function (table, where, options = {}, mode) {
return select(table, where, options, mode ?? SelectResultMode.ExactlyOne);
};


Expand Down Expand Up @@ -722,3 +774,16 @@ export const min: NumericAggregateSignatures = function (table, where, options?)
export const max: NumericAggregateSignatures = function (table, where, options?) {
return select(table, where, options, SelectResultMode.Numeric, 'max');
};

/**
* Transforms an `SQLFragment` into a sub-query to obtain a value instead of an object
* @param frag The `SQLFragment` to be transformed
* @returns The value of type T result
*/
export const nested: <T extends SQLFragment<any>>(
frag: T
) => RunResultForSQLFragment<T> = function <T extends SQLFragment<any>>(
frag: T
): RunResultForSQLFragment<T> {
return sql`(${frag})` as RunResultForSQLFragment<T>;
};