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

Modular types #100

Open
wants to merge 1 commit 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
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)}`;
78 changes: 56 additions & 22 deletions src/db/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,13 @@ 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 ===

/** Allows injection of additional structures to zapatos */
export interface StructureMap {
}

/**
* Compiles to `DEFAULT` for use in `INSERT`/`UPDATE` queries.
*/
Expand Down Expand Up @@ -73,6 +70,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 +188,43 @@ 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'] | any[] | readonly any[]> | 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'] | any[] | readonly any[]>
| GenericSQLStructure['Table']
| GenericSQLStructure['Schema']
| GenericSQLStructure['Whereable']
| Column<GenericSQLStructure>;

export type SQLStructure = { [Key in keyof StructureMap]: StructureMap[Key] }[keyof StructureMap];

export type SQL = SQLForStructure<SQLStructure>;

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

Expand All @@ -212,14 +237,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 +273,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 +320,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 +335,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 +414,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 +431,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