diff --git a/db.mjs b/db.mjs index b2d7f27..17411ed 100644 --- a/db.mjs +++ b/db.mjs @@ -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; diff --git a/src/db/conditions.ts b/src/db/conditions.ts index 9b91897..07292ca 100644 --- a/src/db/conditions.ts +++ b/src/db/conditions.ts @@ -55,8 +55,8 @@ export const reImatch = (a: T) => sql` export const notReMatch = (a: T) => sql`${self} !~ ${conditionalParam(a)}`; export const notReImatch = (a: T) => sql`${self} !~* ${conditionalParam(a)}`; -export const isIn = (a: readonly T[]) => a.length > 0 ? sql`${self} IN (${vals(a)})` : sql`false`; -export const isNotIn = (a: readonly T[]) => a.length > 0 ? sql`${self} NOT IN (${vals(a)})` : sql`true`; +export const isIn = (a: readonly T[]) => 'run' in a ? sql`${self} IN ${a}` : a.length > 0 ? sql`${self} IN (${vals(a)})` : sql`false`; +export const isNotIn = (a: readonly T[]) => 'run' in a ? sql`${self} NOT IN ${a}` : a.length > 0 ? sql`${self} NOT IN (${vals(a)})` : sql`true`; export const or = (...conditions: SQLFragment[] | Whereable[]) => sql`(${mapWithSeparator(conditions, sql` OR `, c => c)})`; export const and = (...conditions: SQLFragment[] | Whereable[]) => sql`(${mapWithSeparator(conditions, sql` AND `, c => c)})`; diff --git a/src/db/shortcuts.ts b/src/db/shortcuts.ts index ffc40ff..79639be 100644 --- a/src/db/shortcuts.ts +++ b/src/db/shortcuts.ts @@ -408,7 +408,10 @@ export interface SelectOptionsForTable< offset?: number; withTies?: boolean; columns?: C; + column?: ColumnForTable; + array?: ColumnForTable; extras?: E; + extra?: SQLFragment; groupBy?: ColumnForTable | ColumnForTable[] | SQLFragment; having?: WhereableForTable | SQLFragment; lateral?: L; @@ -427,7 +430,7 @@ type SelectReturnTypeForTable< L extends SQLFragment ? RunResultForSQLFragment : 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, @@ -437,11 +440,17 @@ export type FullSelectReturnTypeForTable< M extends SelectResultMode, > = { - [SelectResultMode.Many]: SelectReturnTypeForTable[]; - [SelectResultMode.ExactlyOne]: SelectReturnTypeForTable; - [SelectResultMode.One]: SelectReturnTypeForTable | undefined; - [SelectResultMode.Numeric]: number; - }[M]; + [SelectResultMode.Many]: SelectReturnTypeForTable[]; + [SelectResultMode.ExactlyOne]: SelectReturnTypeForTable; + [SelectResultMode.One]: SelectReturnTypeForTable | 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 { ` 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)}`, @@ -551,10 +569,13 @@ export const select: SelectSignatures = function ( const rowsQuery = sql`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`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`${rowsQuery}` : + // we need the aggregate to sit in a sub-SELECT in order to keep ORDER and LIMIT working as usual + sql`SELECT coalesce(jsonb_agg(result), '[]') AS result FROM (${rowsQuery}) AS ${raw(`"sq_${alias}"`)}`; query.runResultTransform = mode === SelectResultMode.Numeric ? @@ -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; @@ -584,11 +605,26 @@ export interface SelectOneSignatures { L extends LateralOption, E extends ExtrasOption, A extends string, + M extends SelectResultMode = SelectResultMode.One + >( + table: T, + where: WhereableForTable | SQLFragment | AllType, + options?: SelectOptionsForTable, + mode?: null + ): SQLFragment>; + < + T extends Table, + C extends ColumnsOption, + L extends LateralOption, + E extends ExtrasOption, + A extends string, + M extends SelectResultMode >( table: T, where: WhereableForTable | SQLFragment | AllType, options?: SelectOptionsForTable, - ): SQLFragment>; + mode?: M + ): SQLFragment>; } /** @@ -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); }; @@ -621,11 +657,26 @@ export interface SelectExactlyOneSignatures { L extends LateralOption, E extends ExtrasOption, A extends string, + M extends SelectResultMode = SelectResultMode.ExactlyOne >( table: T, where: WhereableForTable | SQLFragment | AllType, options?: SelectOptionsForTable, - ): SQLFragment>; + mode?: null + ): SQLFragment>; + < + T extends Table, + C extends ColumnsOption, + L extends LateralOption, + E extends ExtrasOption, + A extends string, + M extends SelectResultMode + >( + table: T, + where: WhereableForTable | SQLFragment | AllType, + options?: SelectOptionsForTable, + mode?: M + ): SQLFragment>; } /** @@ -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); }; @@ -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: >( + frag: T +) => RunResultForSQLFragment = function >( + frag: T +): RunResultForSQLFragment { + return sql`(${frag})` as RunResultForSQLFragment; +};