Skip to content

Commit

Permalink
Modular types
Browse files Browse the repository at this point in the history
  • Loading branch information
Javier committed Aug 11, 2021
1 parent e2cbc70 commit d59e486
Show file tree
Hide file tree
Showing 9 changed files with 656 additions and 388 deletions.
8 changes: 8 additions & 0 deletions db.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@ export const constraint = mod.constraint;
export const count = mod.count;
export const deletes = mod.deletes;
export const doNothing = mod.doNothing;
export const genericAvg = mod.genericAvg;
export const genericCount = mod.genericCount;
export const genericMax = mod.genericMax;
export const genericMin = mod.genericMin;
export const genericSelectExactlyOne = mod.genericSelectExactlyOne;
export const genericSum = mod.genericSum;
export const getConfig = mod.getConfig;
export const insert = mod.insert;
export const isDatabaseError = mod.isDatabaseError;
Expand All @@ -43,6 +49,8 @@ export const setConfig = mod.setConfig;
export const sql = mod.sql;
export const strict = mod.strict;
export const sum = mod.sum;
export const table = mod.table;
export const tables = mod.tables;
export const toBuffer = mod.toBuffer;
export const toDate = mod.toDate;
export const toString = mod.toString;
Expand Down
76 changes: 38 additions & 38 deletions src/db/conditions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
Parameter,
param,
sql,
SQL,
GenericSQL,
self,
vals,
} from './core';
Expand All @@ -20,47 +20,47 @@ import { mapWithSeparator } from './utils';

const conditionalParam = (a: any) => a instanceof SQLFragment || a instanceof ParentColumn || a instanceof Parameter ? a : param(a);

export const isNull = sql<SQL, boolean>`${self} IS NULL`;
export const isNotNull = sql<SQL, boolean>`${self} IS NOT NULL`;
export const isTrue = sql<SQL, boolean>`${self} IS TRUE`;
export const isNotTrue = sql<SQL, boolean>`${self} IS NOT TRUE`;
export const isFalse = sql<SQL, boolean>`${self} IS FALSE`;
export const isNotFalse = sql<SQL, boolean>`${self} IS NOT FALSE`;
export const isUnknown = sql<SQL, boolean>`${self} IS UNKNOWN`;
export const isNotUnknown = sql<SQL, boolean>`${self} IS NOT UNKNOWN`;
export const isNull = sql<GenericSQL, boolean>`${self} IS NULL`;
export const isNotNull = sql<GenericSQL, boolean>`${self} IS NOT NULL`;
export const isTrue = sql<GenericSQL, boolean>`${self} IS TRUE`;
export const isNotTrue = sql<GenericSQL, boolean>`${self} IS NOT TRUE`;
export const isFalse = sql<GenericSQL, boolean>`${self} IS FALSE`;
export const isNotFalse = sql<GenericSQL, boolean>`${self} IS NOT FALSE`;
export const isUnknown = sql<GenericSQL, boolean>`${self} IS UNKNOWN`;
export const isNotUnknown = sql<GenericSQL, boolean>`${self} IS NOT UNKNOWN`;

export const isDistinctFrom = <T>(a: T) => sql<SQL, boolean, T>`${self} IS DISTINCT FROM ${conditionalParam(a)}`;
export const isNotDistinctFrom = <T>(a: T) => sql<SQL, boolean, T>`${self} IS NOT DISTINCT FROM ${conditionalParam(a)}`;
export const isDistinctFrom = <T>(a: T) => sql<GenericSQL, boolean, T>`${self} IS DISTINCT FROM ${conditionalParam(a)}`;
export const isNotDistinctFrom = <T>(a: T) => sql<GenericSQL, boolean, T>`${self} IS NOT DISTINCT FROM ${conditionalParam(a)}`;

export const eq = <T>(a: T) => sql<SQL, boolean | null, T>`${self} = ${conditionalParam(a)}`;
export const ne = <T>(a: T) => sql<SQL, boolean | null, T>`${self} <> ${conditionalParam(a)}`;
export const gt = <T>(a: T) => sql<SQL, boolean | null, T>`${self} > ${conditionalParam(a)}`;
export const gte = <T>(a: T) => sql<SQL, boolean | null, T>`${self} >= ${conditionalParam(a)}`;
export const lt = <T>(a: T) => sql<SQL, boolean | null, T>`${self} < ${conditionalParam(a)}`;
export const lte = <T>(a: T) => sql<SQL, boolean | null, T>`${self} <= ${conditionalParam(a)}`;
export const eq = <T>(a: T) => sql<GenericSQL, boolean | null, T>`${self} = ${conditionalParam(a)}`;
export const ne = <T>(a: T) => sql<GenericSQL, boolean | null, T>`${self} <> ${conditionalParam(a)}`;
export const gt = <T>(a: T) => sql<GenericSQL, boolean | null, T>`${self} > ${conditionalParam(a)}`;
export const gte = <T>(a: T) => sql<GenericSQL, boolean | null, T>`${self} >= ${conditionalParam(a)}`;
export const lt = <T>(a: T) => sql<GenericSQL, boolean | null, T>`${self} < ${conditionalParam(a)}`;
export const lte = <T>(a: T) => sql<GenericSQL, boolean | null, T>`${self} <= ${conditionalParam(a)}`;

export const between = <T>(a: T, b: T) => sql<SQL, boolean | null, T> `${self} BETWEEN (${conditionalParam(a)}) AND (${conditionalParam(b)})`;
export const betweenSymmetric = <T>(a: T, b: T) => sql<SQL, boolean | null, T> `${self} BETWEEN SYMMETRIC (${conditionalParam(a)}) AND (${conditionalParam(b)})`;
export const notBetween = <T>(a: T, b: T) => sql<SQL, boolean | null, T> `${self} NOT BETWEEN (${conditionalParam(a)}) AND (${conditionalParam(b)})`;
export const notBetweenSymmetric = <T>(a: T, b: T) => sql<SQL, boolean | null, T> `${self} NOT BETWEEN SYMMETRIC (${conditionalParam(a)}) AND (${conditionalParam(b)})`;
export const between = <T>(a: T, b: T) => sql<GenericSQL, boolean | null, T> `${self} BETWEEN (${conditionalParam(a)}) AND (${conditionalParam(b)})`;
export const betweenSymmetric = <T>(a: T, b: T) => sql<GenericSQL, boolean | null, T> `${self} BETWEEN SYMMETRIC (${conditionalParam(a)}) AND (${conditionalParam(b)})`;
export const notBetween = <T>(a: T, b: T) => sql<GenericSQL, boolean | null, T> `${self} NOT BETWEEN (${conditionalParam(a)}) AND (${conditionalParam(b)})`;
export const notBetweenSymmetric = <T>(a: T, b: T) => sql<GenericSQL, boolean | null, T> `${self} NOT BETWEEN SYMMETRIC (${conditionalParam(a)}) AND (${conditionalParam(b)})`;

export const like = <T extends string>(a: T) => sql<SQL, boolean | null, T>`${self} LIKE ${conditionalParam(a)}`;
export const notLike = <T extends string>(a: T) => sql<SQL, boolean | null, T>`${self} NOT LIKE ${conditionalParam(a)}`;
export const ilike = <T extends string>(a: T) => sql<SQL, boolean | null, T>`${self} ILIKE ${conditionalParam(a)}`;
export const notIlike = <T extends string>(a: T) => sql<SQL, boolean | null, T>`${self} NOT ILIKE ${conditionalParam(a)}`;
export const similarTo = <T extends string>(a: T) => sql<SQL, boolean | null, T>`${self} SIMILAR TO ${conditionalParam(a)}`;
export const notSimilarTo = <T extends string>(a: T) => sql<SQL, boolean | null, T>`${self} NOT SIMILAR TO ${conditionalParam(a)}`;
export const reMatch = <T extends string>(a: T) => sql<SQL, boolean | null, T>`${self} ~ ${conditionalParam(a)}`;
export const reImatch = <T extends string>(a: T) => sql<SQL, boolean | null, T>`${self} ~* ${conditionalParam(a)}`;
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 like = <T extends string>(a: T) => sql<GenericSQL, boolean | null, T>`${self} LIKE ${conditionalParam(a)}`;
export const notLike = <T extends string>(a: T) => sql<GenericSQL, boolean | null, T>`${self} NOT LIKE ${conditionalParam(a)}`;
export const ilike = <T extends string>(a: T) => sql<GenericSQL, boolean | null, T>`${self} ILIKE ${conditionalParam(a)}`;
export const notIlike = <T extends string>(a: T) => sql<GenericSQL, boolean | null, T>`${self} NOT ILIKE ${conditionalParam(a)}`;
export const similarTo = <T extends string>(a: T) => sql<GenericSQL, boolean | null, T>`${self} SIMILAR TO ${conditionalParam(a)}`;
export const notSimilarTo = <T extends string>(a: T) => sql<GenericSQL, boolean | null, T>`${self} NOT SIMILAR TO ${conditionalParam(a)}`;
export const reMatch = <T extends string>(a: T) => sql<GenericSQL, boolean | null, T>`${self} ~ ${conditionalParam(a)}`;
export const reImatch = <T extends string>(a: T) => sql<GenericSQL, boolean | null, T>`${self} ~* ${conditionalParam(a)}`;
export const notReMatch = <T extends string>(a: T) => sql<GenericSQL, boolean | null, T>`${self} !~ ${conditionalParam(a)}`;
export const notReImatch = <T extends string>(a: T) => sql<GenericSQL, 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[]) => a.length > 0 ? sql<GenericSQL, boolean | null, T>`${self} IN (${vals(a)})` : sql`false`;
export const isNotIn = <T>(a: readonly T[]) => a.length > 0 ? sql<GenericSQL, boolean | null, T>`${self} NOT IN (${vals(a)})` : sql`true`;

export const or = <T>(...conditions: SQLFragment<any, T>[]) => sql<SQL, boolean | null, T>`(${mapWithSeparator(conditions, sql` OR `, c => c)})`;
export const and = <T>(...conditions: SQLFragment<any, T>[]) => sql<SQL, boolean | null, T>`(${mapWithSeparator(conditions, sql` AND `, c => c)})`;
export const not = <T>(condition: SQLFragment<any, T>) => sql<SQL, boolean | null, T>`(NOT ${condition})`;
export const or = <T>(...conditions: SQLFragment<any, T>[]) => sql<GenericSQL, boolean | null, T>`(${mapWithSeparator(conditions, sql` OR `, c => c)})`;
export const and = <T>(...conditions: SQLFragment<any, T>[]) => sql<GenericSQL, boolean | null, T>`(${mapWithSeparator(conditions, sql` AND `, c => c)})`;
export const not = <T>(condition: SQLFragment<any, T>) => sql<GenericSQL, boolean | null, T>`(NOT ${condition})`;

// things that aren't genuinely conditions
type IntervalUnit = 'microsecond' | 'millisecond' | 'second' | 'minute' | 'hour' | 'day' | 'week' | 'month' | 'year' | 'decade' | 'century' | 'millennium';
Expand All @@ -69,5 +69,5 @@ export const after = gt;
export const before = lt;

// these are really more operations than conditions, but we sneak them in here for now, for use e.g. in UPDATE queries
export const add = <T extends number | Date>(a: T) => sql<SQL, number, T>`${self} + ${conditionalParam(a)}`;
export const subtract = <T extends number | Date>(a: T) => sql<SQL, number, T>`${self} - ${conditionalParam(a)}`;
export const add = <T extends number | Date>(a: T) => sql<GenericSQL, number, T>`${self} + ${conditionalParam(a)}`;
export const subtract = <T extends number | Date>(a: T) => sql<GenericSQL, number, T>`${self} - ${conditionalParam(a)}`;
83 changes: 61 additions & 22 deletions src/db/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,17 @@ import { performance } from 'perf_hooks';
import { getConfig, SQLQuery } from './config';
import { isPOJO, NoInfer } from './utils';

import type {
Updatable,
Whereable,
Table,
Column,
} from 'zapatos/schema';


// === symbols, types, wrapper classes and shortcuts ===

declare global {
namespace Zapatos {
/** Injects additional structures on Zapatos.StructureMap */
interface StructureMap {
}
}
}

/**
* Compiles to `DEFAULT` for use in `INSERT`/`UPDATE` queries.
*/
Expand Down Expand Up @@ -73,6 +74,8 @@ export type NumberRangeString = RangeString<number | ''>;
*/
export type ByteArrayString = `\\x${string}`;

export type Column <S extends GenericSQLStructure> = Exclude<keyof S['Selectable'], number | symbol>;

/**
* Make a function `STRICT` in the Postgres sense — where it's an alias for
* `RETURNS NULL ON NULL INPUT` — with appropriate typing.
Expand Down Expand Up @@ -189,17 +192,44 @@ export function vals<T>(x: T) { return new ColumnValues<T>(x); }
* Compiles to the name of the column it wraps in the table of the parent query.
* @param value The column name
*/
export class ParentColumn<T extends Column = Column> { constructor(public value: T) { } }
export class ParentColumn<T extends Column<GenericSQLStructure> = Column<SQLStructure>> { constructor(public value: T) { } }
/**
* Returns a `ParentColumn` instance, wrapping a column name, which compiles to
* that column name of the table of the parent query.
*/
export function parent<T extends Column = Column>(x: T) { return new ParentColumn<T>(x); }
export function parent<T extends Column<GenericSQLStructure> = Column<SQLStructure>>(x: T) { return new ParentColumn<T>(x); }


export type GenericSQLExpression = SQLFragment<any, any> | Parameter | DefaultType | DangerousRawString | SelfType;
export type SQLExpression = Table | ColumnNames<Updatable | (keyof Updatable)[]> | ColumnValues<Updatable | any[]> | Whereable | Column | GenericSQLExpression;
export type SQL = SQLExpression | SQLExpression[];

export interface GenericSQLStructure {
Schema: string;
Table: string;
Selectable: { [k: string]: any };
JSONSelectable: object;
Whereable: object;
Insertable: object;
Updatable: object;
UniqueIndex: string;
}

export type SQLExpressionForStructure <S extends GenericSQLStructure> = GenericSQLExpression | ColumnNames<S['Updatable'] | (keyof S['Updatable'])[] | readonly (keyof S['Updatable'])[]> | ColumnValues<S['Updatable']> | S['Table'] | S['Schema'] | S['Whereable'] | Column<S>;
export type SQLForStructure <S extends GenericSQLStructure> = SQLExpressionForStructure<S> | Array<SQLExpressionForStructure<S>>;

export type GenericSQL =
| GenericSQLExpression
| ColumnNames<GenericSQLStructure['Updatable']
| (keyof GenericSQLStructure['Updatable'])[]>
| ColumnValues<GenericSQLStructure['Updatable']>
| GenericSQLStructure['Table']
| GenericSQLStructure['Schema']
| GenericSQLStructure['Whereable']
| Column<GenericSQLStructure>;

// eslint-disable-next-line no-undef
export type SQLStructure = { [Key in keyof Zapatos.StructureMap]: Zapatos.StructureMap[Key] }[keyof Zapatos.StructureMap];

export type SQL = SQLForStructure<SQLStructure>;

export type Queryable = pg.ClientBase | pg.Pool;

Expand All @@ -212,14 +242,23 @@ export type Queryable = pg.ClientBase | pg.Pool;
* defines what type the `SQLFragment` produces, where relevant (i.e. when
* calling `.run(...)` on it, or using it as the value of an `extras` object).
*/
export function sql<
Interpolations = SQL,
RunResult = pg.QueryResult['rows'],
Constraint = never,
>(literals: TemplateStringsArray, ...expressions: NoInfer<Interpolations>[]) {
return new SQLFragment<RunResult, Constraint>(Array.prototype.slice.apply(literals), expressions);
interface SqlSignatures {
<
Interpolations = SQL,
RunResult = pg.QueryResult['rows'],
Constraint = never,
>(literals: TemplateStringsArray, ...expressions: NoInfer<Interpolations>[]): SQLFragment<RunResult, Constraint>;
<
Structure extends GenericSQLStructure = SQLStructure,
RunResult = pg.QueryResult['rows'],
Constraint = never,
>(literals: TemplateStringsArray, ...expressions: NoInfer<SQLForStructure<Structure>>[]): SQLFragment<RunResult, Constraint>;
}

export const sql: SqlSignatures = (literals: TemplateStringsArray, ...expressions: NoInfer<GenericSQL>[]) => {
return new SQLFragment<pg.QueryResult['rows'], never>(Array.prototype.slice.apply(literals), expressions);
};

let preparedNameSeq = 0;

export class SQLFragment<RunResult = pg.QueryResult['rows'], Constraint = never> {
Expand All @@ -239,7 +278,7 @@ export class SQLFragment<RunResult = pg.QueryResult['rows'], Constraint = never>
noop = false; // if true, bypass actually running the query unless forced to e.g. for empty INSERTs
noopResult: any; // if noop is true and DB is bypassed, what should be returned?

constructor(protected literals: string[], protected expressions: SQL[]) { }
constructor(protected literals: string[], protected expressions: GenericSQL[]) { }

/**
* Instruct Postgres to treat this as a prepared statement: see
Expand Down Expand Up @@ -286,7 +325,7 @@ export class SQLFragment<RunResult = pg.QueryResult['rows'], Constraint = never>
* that could be passed to the `pg` query function. Arguments are generally
* only passed when the function calls itself recursively.
*/
compile = (result: SQLQuery = { text: '', values: [] }, parentTable?: string, currentColumn?: Column) => {
compile = (result: SQLQuery = { text: '', values: [] }, parentTable?: string, currentColumn?: Column<GenericSQLStructure>) => {
if (this.parentTable) parentTable = this.parentTable;

if (this.noop) result.text += "/* marked no-op: won't hit DB unless forced -> */ ";
Expand All @@ -301,7 +340,7 @@ export class SQLFragment<RunResult = pg.QueryResult['rows'], Constraint = never>
return result;
};

compileExpression = (expression: SQL, result: SQLQuery = { text: '', values: [] }, parentTable?: string, currentColumn?: Column) => {
compileExpression = (expression: GenericSQL, result: SQLQuery = { text: '', values: [] }, parentTable?: string, currentColumn?: Column<GenericSQLStructure>) => {
if (this.parentTable) parentTable = this.parentTable;

if (expression instanceof SQLFragment) {
Expand Down Expand Up @@ -380,7 +419,7 @@ export class SQLFragment<RunResult = pg.QueryResult['rows'], Constraint = never>

} else {
const
columnNames = <Column[]>Object.keys(expression.value).sort(),
columnNames = <Column<GenericSQLStructure>[]>Object.keys(expression.value).sort(),
columnValues = columnNames.map(k => (<any>expression.value)[k]);

for (let i = 0, len = columnValues.length; i < len; i++) {
Expand All @@ -397,7 +436,7 @@ export class SQLFragment<RunResult = pg.QueryResult['rows'], Constraint = never>

} else if (typeof expression === 'object') {
// must be a Whereable object, so put together a WHERE clause
const columnNames = <Column[]>Object.keys(expression).sort();
const columnNames = <Column<GenericSQLStructure>[]>Object.keys(expression).sort();

if (columnNames.length) { // if the object is not empty
result.text += '(';
Expand Down
Loading

0 comments on commit d59e486

Please sign in to comment.