diff --git a/src/column/array-util/constructor/from-column-map.ts b/src/column/array-util/constructor/from-column-map.ts index 731f60e9..38b01406 100644 --- a/src/column/array-util/constructor/from-column-map.ts +++ b/src/column/array-util/constructor/from-column-map.ts @@ -21,3 +21,19 @@ export function fromColumnMap< } return result as FromColumnMap; } + +export function fromColumnMapArray< + ColumnMapT extends ColumnMap +> ( + columnMapArr : readonly ColumnMapT[] +) : ( + FromColumnMap +) { + const result : IColumn[] = []; + for (const columnMap of columnMapArr) { + for (const columnAlias of Object.keys(columnMap)) { + result.push(columnMap[columnAlias]); + } + } + return result as FromColumnMap; +} diff --git a/src/design-pattern-table-per-type/table-per-type-impl.ts b/src/design-pattern-table-per-type/table-per-type-impl.ts index f43c4e98..5aa80550 100644 --- a/src/design-pattern-table-per-type/table-per-type-impl.ts +++ b/src/design-pattern-table-per-type/table-per-type-impl.ts @@ -1,9 +1,10 @@ import {ITablePerType, TablePerTypeData, InsertableTablePerType} from "./table-per-type"; import * as TablePerTypeUtil from "./util"; import {TableWithPrimaryKey} from "../table"; -import {SelectConnection, ExecutionUtil, IsolableInsertOneConnection} from "../execution"; +import {SelectConnection, ExecutionUtil, IsolableInsertOneConnection, IsolableUpdateConnection} from "../execution"; import {WhereDelegate} from "../where-clause"; -import {OnlyKnownProperties} from "../type-util"; +import {OnlyKnownProperties, StrictUnion} from "../type-util"; +import {CandidateKey_NonUnion} from "../candidate-key"; export class TablePerType implements ITablePerType { readonly childTable : DataT["childTable"]; @@ -79,4 +80,24 @@ export class TablePerType implements ITablePerTy row ); } + + updateAndFetchOneByCandidateKey< + CandidateKeyT extends StrictUnion>, + AssignmentMapT extends TablePerTypeUtil.CustomAssignmentMap + > ( + connection : IsolableUpdateConnection, + /** + * @todo Try and recall why I wanted `AssertNonUnion<>` + * I didn't write compile-time tests for it... + */ + candidateKey : CandidateKeyT,// & AssertNonUnion, + assignmentMapDelegate : TablePerTypeUtil.AssignmentMapDelegate + ) : Promise> { + return TablePerTypeUtil.updateAndFetchOneByCandidateKey( + this, + connection, + candidateKey, + assignmentMapDelegate + ); + } } diff --git a/src/design-pattern-table-per-type/util/execution/absorb-row.ts b/src/design-pattern-table-per-type/util/execution/absorb-row.ts new file mode 100644 index 00000000..f7438ce7 --- /dev/null +++ b/src/design-pattern-table-per-type/util/execution/absorb-row.ts @@ -0,0 +1,77 @@ +import {BuiltInExprUtil} from "../../../built-in-expr"; +import {DataTypeUtil} from "../../../data-type"; +import {ITable} from "../../../table"; + +/** + * @todo Better name + * + * Adds properties from `row` to `result`. + * + * If a property from `row` already exists on `result`, + * we use `table` to check if the values on both objects are equal. + * + * If they are not equal, an `Error` is thrown. + */ +export function absorbRow ( + result : Record, + table : ITable, + row : Record +) { + for (const columnAlias of Object.keys(row)) { + /** + * This is guaranteed to be a value expression. + */ + const newValue = row[columnAlias]; + + if (Object.prototype.hasOwnProperty.call(result, columnAlias)) { + /** + * This `curValue` could be a non-value expression. + * We only want value expressions. + */ + const curValue = result[columnAlias]; + + if (BuiltInExprUtil.isAnyNonValueExpr(curValue)) { + /** + * Add this new value to the `result` + * so we can use it to update rows of tables + * further down the inheritance hierarchy. + */ + result[columnAlias] = newValue; + continue; + } + + if (curValue === newValue) { + /** + * They are equal, do nothing. + */ + continue; + } + /** + * We need some custom equality checking logic + */ + if (!DataTypeUtil.isNullSafeEqual( + table.columns[columnAlias], + /** + * This may throw + */ + table.columns[columnAlias].mapper( + `${table.alias}.${columnAlias}`, + curValue + ), + newValue + )) { + /** + * @todo Custom `Error` type + */ + throw new Error(`All columns with the same name in an inheritance hierarchy must have the same value; mismatch found for ${table.alias}.${columnAlias}`); + } + } else { + /** + * Add this new value to the `result` + * so we can use it to update rows of tables + * further down the inheritance hierarchy. + */ + result[columnAlias] = newValue; + } + } +} diff --git a/src/design-pattern-table-per-type/util/execution/index.ts b/src/design-pattern-table-per-type/util/execution/index.ts index 142e42e9..008aa4fb 100644 --- a/src/design-pattern-table-per-type/util/execution/index.ts +++ b/src/design-pattern-table-per-type/util/execution/index.ts @@ -1,3 +1,5 @@ export * from "./fetch-one"; export * from "./insert-and-fetch"; export * from "./insert-row"; +export * from "./update-and-fetch-one-by-candidate-key"; +export * from "./update-and-fetch-one-impl"; diff --git a/src/design-pattern-table-per-type/util/execution/insert-and-fetch.ts b/src/design-pattern-table-per-type/util/execution/insert-and-fetch.ts index dee1b2e2..599ab949 100644 --- a/src/design-pattern-table-per-type/util/execution/insert-and-fetch.ts +++ b/src/design-pattern-table-per-type/util/execution/insert-and-fetch.ts @@ -4,11 +4,10 @@ import {IsolableInsertOneConnection, ExecutionUtil} from "../../../execution"; import {Identity, OnlyKnownProperties, omitOwnEnumerable} from "../../../type-util"; import {ColumnAlias, ColumnType, implicitAutoIncrement, generatedColumnAliases, findTableWithGeneratedColumnAlias} from "../query"; import {CustomExprUtil} from "../../../custom-expr"; -import {DataTypeUtil} from "../../../data-type"; -import {BuiltInExprUtil} from "../../../built-in-expr"; import {TableUtil} from "../../../table"; import {expr} from "../../../expr"; import {UsedRefUtil} from "../../../used-ref"; +import {absorbRow} from "./absorb-row"; export type InsertAndFetchRow< TptT extends InsertableTablePerType @@ -117,63 +116,7 @@ export async function insertAndFetch< connection, result as never ); - for (const columnAlias of Object.keys(fetchedRow)) { - /** - * This is guaranteed to be a value expression. - */ - const newValue = fetchedRow[columnAlias]; - - if (Object.prototype.hasOwnProperty.call(result, columnAlias)) { - /** - * This `curValue` could be a non-value expression. - * We only want value expressions. - */ - const curValue = result[columnAlias]; - - if (BuiltInExprUtil.isAnyNonValueExpr(curValue)) { - /** - * Add this new value to the `rawInsertRow` - * so we can use it to insert rows to tables - * further down the inheritance hierarchy. - */ - result[columnAlias] = newValue; - continue; - } - - if (curValue === newValue) { - /** - * They are equal, do nothing. - */ - continue; - } - /** - * We need some custom equality checking logic - */ - if (!DataTypeUtil.isNullSafeEqual( - table.columns[columnAlias], - /** - * This may throw - */ - table.columns[columnAlias].mapper( - `${table.alias}.${columnAlias}`, - curValue - ), - newValue - )) { - /** - * @todo Custom `Error` type - */ - throw new Error(`All columns with the same name in an inheritance hierarchy must have the same value; mismatch found for ${table.alias}.${columnAlias}`); - } - } else { - /** - * Add this new value to the `rawInsertRow` - * so we can use it to insert rows to tables - * further down the inheritance hierarchy. - */ - result[columnAlias] = newValue; - } - } + absorbRow(result, table, fetchedRow); } return result; diff --git a/src/design-pattern-table-per-type/util/execution/invoke-assignment-delegate.ts b/src/design-pattern-table-per-type/util/execution/invoke-assignment-delegate.ts new file mode 100644 index 00000000..80e111f8 --- /dev/null +++ b/src/design-pattern-table-per-type/util/execution/invoke-assignment-delegate.ts @@ -0,0 +1,92 @@ +import {ITablePerType} from "../../table-per-type"; +import {CustomAssignmentMap, AssignmentMapDelegate} from "./update-and-fetch-one-by-candidate-key"; +import {IsolableSelectConnection, ExecutionUtil} from "../../../execution"; +import {ColumnRefUtil} from "../../../column-ref"; +import {ColumnUtil, ColumnArrayUtil} from "../../../column"; +import {from, From} from "../execution-impl"; +import {WhereDelegate} from "../../../where-clause"; +import {isMutableColumnAlias, columnMapper} from "../query"; +import {BuiltInExprUtil} from "../../../built-in-expr"; +import {expr, ExprUtil} from "../../../expr"; +import {DataTypeUtil} from "../../../data-type"; + +/** + * Not meant to be called externally. + * + * @todo Better name + */ +export async function invokeAssignmentDelegate< + TptT extends ITablePerType, + AssignmentMapT extends CustomAssignmentMap +> ( + tpt : TptT, + connection : IsolableSelectConnection, + whereDelegate : WhereDelegate["fromClause"]>, + assignmentMapDelegate : AssignmentMapDelegate +) : Promise> { + const columns = ColumnRefUtil.fromColumnArray< + ColumnUtil.FromColumnMap< + | TptT["childTable"]["columns"] + | TptT["parentTables"][number]["columns"] + >[] + >( + ColumnArrayUtil.fromColumnMapArray< + | TptT["childTable"]["columns"] + | TptT["parentTables"][number]["columns"] + >( + [ + tpt.childTable.columns, + ...tpt.parentTables.map(parentTable => parentTable.columns) + ] + ) + ); + /** + * May contain extra properties that are not mutable columns, + * or even columns at all. + */ + const rawAssignmentMap = assignmentMapDelegate(columns); + + const columnAliasArr = Object.keys(rawAssignmentMap); + if (columnAliasArr.length == 0) { + return {}; + } + + const query = from(tpt) + .where(whereDelegate as any) + .select(() => columnAliasArr + .filter(columnAlias => isMutableColumnAlias(tpt, columnAlias)) + .map(columnAlias => { + const customExpr = rawAssignmentMap[columnAlias as keyof typeof rawAssignmentMap] as any; + if (BuiltInExprUtil.isAnyNonValueExpr(customExpr)) { + /** + * We have a non-value expression + */ + return expr( + { + mapper : DataTypeUtil.intersect( + columnMapper(tpt, columnAlias), + BuiltInExprUtil.mapper(customExpr) + ), + usedRef : BuiltInExprUtil.usedRef(customExpr), + }, + BuiltInExprUtil.buildAst(customExpr) + ).as(columnAlias); + } else { + /** + * We have a value expression + */ + return ExprUtil.fromRawExprNoUsedRefInput( + columnMapper(tpt, columnAlias), + customExpr + ).as(columnAlias); + } + }) as any + ); + /** + * Should only contain value expressions now. + */ + return ExecutionUtil.fetchOne( + query as any, + connection + ) as Promise>; +} diff --git a/src/design-pattern-table-per-type/util/execution/update-and-fetch-one-by-candidate-key.ts b/src/design-pattern-table-per-type/util/execution/update-and-fetch-one-by-candidate-key.ts new file mode 100644 index 00000000..0b826b3b --- /dev/null +++ b/src/design-pattern-table-per-type/util/execution/update-and-fetch-one-by-candidate-key.ts @@ -0,0 +1,133 @@ +import {ITablePerType} from "../../table-per-type"; +import {CandidateKey_NonUnion} from "../../../candidate-key"; +import {StrictUnion, Identity, pickOwnEnumerable} from "../../../type-util"; +import {IsolableUpdateConnection, ExecutionUtil} from "../../../execution"; +import {MutableColumnAlias, ColumnType, ColumnAlias} from "../query"; +import {CustomExpr_MapCorrelatedOrUndefined, CustomExprUtil} from "../../../custom-expr"; +import {ColumnRefUtil} from "../../../column-ref"; +import {ColumnUtil} from "../../../column"; +import * as ExprLib from "../../../expr-library"; +import {updateAndFetchOneImpl, UpdateAndFetchOneResult} from "./update-and-fetch-one-impl"; +import {invokeAssignmentDelegate} from "./invoke-assignment-delegate"; + +export type CustomAssignmentMap< + TptT extends ITablePerType +> = + Identity< + & { + readonly [columnAlias in MutableColumnAlias]? : ( + CustomExpr_MapCorrelatedOrUndefined< + ( + | TptT["childTable"]["columns"] + | TptT["parentTables"][number]["columns"] + ), + ColumnType + > + ) + } + & { + readonly [ + columnAlias in Exclude< + ColumnAlias, + MutableColumnAlias + > + ]? : undefined + } + > +; + +export type AssignmentMapDelegate< + TptT extends ITablePerType, + AssignmentMapT extends CustomAssignmentMap +> = + ( + columns : ColumnRefUtil.FromColumnArray< + ColumnUtil.FromColumnMap< + | TptT["childTable"]["columns"] + | TptT["parentTables"][number]["columns"] + >[] + > + ) => AssignmentMapT +; + +export type UpdatedAndFetchedRow< + TptT extends ITablePerType, + AssignmentMapT extends CustomAssignmentMap +> = + Identity<{ + readonly [columnAlias in ColumnAlias] : ( + columnAlias extends keyof AssignmentMapT ? + ( + undefined extends AssignmentMapT[columnAlias] ? + ColumnType : + CustomExprUtil.TypeOf< + AssignmentMapT[columnAlias] + > + ) : + ColumnType + ) + }> +; + +export type UpdateAndFetchOneReturnType< + TptT extends ITablePerType, + AssignmentMapT extends CustomAssignmentMap +> = + Identity< + UpdateAndFetchOneResult< + UpdatedAndFetchedRow + > + > +; + +export async function updateAndFetchOneByCandidateKey< + TptT extends ITablePerType, + CandidateKeyT extends StrictUnion>, + AssignmentMapT extends CustomAssignmentMap +> ( + tpt : TptT, + connection : IsolableUpdateConnection, + /** + * @todo Try and recall why I wanted `AssertNonUnion<>` + * I didn't write compile-time tests for it... + */ + candidateKey : CandidateKeyT,// & AssertNonUnion, + assignmentMapDelegate : AssignmentMapDelegate +) : Promise> { + return connection.transactionIfNotInOne(async (connection) : Promise> => { + const cleanedAssignmentMap = await invokeAssignmentDelegate( + tpt, + connection, + () => ExprLib.eqCandidateKey( + tpt.childTable, + candidateKey + ), + assignmentMapDelegate + ); + /** + * @todo If `result` contains any primaryKey values, + * then we will need to fetch the **current** primaryKey values, + * before any `UPDATE` statements are executed. + * + * This function breaks if we try to update values + * of columns that are foreign keys. + * + * I do not want to disable foreign key checks. + */ + const updateAndFetchChildResult = await ExecutionUtil.updateAndFetchOneByCandidateKey( + tpt.childTable, + connection, + candidateKey, + () => pickOwnEnumerable( + cleanedAssignmentMap, + tpt.childTable.mutableColumns + ) + ); + return updateAndFetchOneImpl( + tpt, + connection, + cleanedAssignmentMap, + updateAndFetchChildResult + ) as Promise>; + }); +} diff --git a/src/design-pattern-table-per-type/util/execution/update-and-fetch-one-impl.ts b/src/design-pattern-table-per-type/util/execution/update-and-fetch-one-impl.ts new file mode 100644 index 00000000..e15ef2a0 --- /dev/null +++ b/src/design-pattern-table-per-type/util/execution/update-and-fetch-one-impl.ts @@ -0,0 +1,106 @@ +import * as tm from "type-mapping"; +import {ITablePerType} from "../../table-per-type"; +import {pickOwnEnumerable} from "../../../type-util"; +import {IsolableUpdateConnection, ExecutionUtil} from "../../../execution"; +import {TableWithPrimaryKey} from "../../../table"; +import {UpdateOneResult} from "../../../execution/util"; +import {absorbRow} from "./absorb-row"; + +export interface UpdateAndFetchOneResult { + updateOneResults : ( + & UpdateOneResult + & { + table : TableWithPrimaryKey, + } + )[], + + //Alias for affectedRows + foundRowCount : bigint; + + /** + * You cannot trust this number for SQLite. + * SQLite thinks that all found rows are updated, even if you set `x = x`. + * + * @todo Consider renaming this to `unreliableUpdatedRowCount`? + */ + //Alias for changedRows + updatedRowCount : bigint; + + /** + * May be the duplicate row count, or some other value. + */ + warningCount : bigint; + + row : RowT, +} + +/** + * Not meant to be called externally + */ +export async function updateAndFetchOneImpl< + TptT extends ITablePerType +> ( + tpt : TptT, + connection : IsolableUpdateConnection, + cleanedAssignmentMap : Record, + updateAndFetchChildResult : ExecutionUtil.UpdateAndFetchOneResult +) : Promise>> { + return connection.transactionIfNotInOne(async (connection) : Promise>> => { + const updateOneResults : UpdateAndFetchOneResult["updateOneResults"] = [ + { + ...updateAndFetchChildResult, + table : tpt.childTable, + }, + ]; + let updatedRowCount : bigint = updateAndFetchChildResult.updatedRowCount; + let warningCount : bigint = updateAndFetchChildResult.warningCount; + + const result : Record = updateAndFetchChildResult.row; + + /** + * We use `.reverse()` here to `UPDATE` the parents + * as we go up the inheritance hierarchy. + */ + for(const parentTable of [...tpt.parentTables].reverse()) { + const updateAndFetchParentResult = await ExecutionUtil.updateAndFetchOneByPrimaryKey( + parentTable, + connection, + /** + * The `result` should contain the primary key values we are interested in + */ + result, + () => pickOwnEnumerable( + cleanedAssignmentMap, + parentTable.mutableColumns + ) + ); + updateOneResults.push({ + ...updateAndFetchParentResult, + table : parentTable, + }); + updatedRowCount = tm.BigIntUtil.add( + updatedRowCount, + updateAndFetchParentResult.updatedRowCount + ); + warningCount = tm.BigIntUtil.add( + warningCount, + updateAndFetchParentResult.warningCount + ); + + absorbRow(result, parentTable, updateAndFetchParentResult.row); + } + + return { + updateOneResults, + /** + * +1 for the `childTable`. + */ + foundRowCount : tm.BigInt(tpt.parentTables.length + 1), + updatedRowCount, + + warningCount, + + row : result as Record, + }; + }); +} diff --git a/src/execution/util/operation-update/update-and-fetch-one-by-candidate-key.ts b/src/execution/util/operation-update/update-and-fetch-one-by-candidate-key.ts index bed40fe6..7fb725fd 100644 --- a/src/execution/util/operation-update/update-and-fetch-one-by-candidate-key.ts +++ b/src/execution/util/operation-update/update-and-fetch-one-by-candidate-key.ts @@ -2,7 +2,7 @@ import {ITable, TableUtil} from "../../../table"; import {IsolableUpdateConnection, SelectConnection} from "../../connection"; import {AssignmentMapDelegate, CustomAssignmentMap} from "../../../update"; import {CandidateKey_NonUnion, CandidateKeyUtil, CandidateKey_Input} from "../../../candidate-key"; -import {StrictUnion, AssertNonUnion, Identity} from "../../../type-util"; +import {StrictUnion, Identity} from "../../../type-util"; import {UpdateOneResult, updateOne} from "./update-one"; import {BuiltInExprUtil} from "../../../built-in-expr"; import {CustomExprUtil, CustomExpr_MapCorrelatedOrUndefined} from "../../../custom-expr"; @@ -89,7 +89,7 @@ export async function __updateAndFetchOneByCandidateKeyHelper< > ( table : TableT, connection : SelectConnection, - candidateKey : CandidateKeyT & AssertNonUnion, + candidateKey : CandidateKeyT,// & AssertNonUnion, assignmentMapDelegate : AssignmentMapDelegate ) : Promise< | { @@ -190,7 +190,7 @@ export async function updateAndFetchOneByCandidateKey< > ( table : TableT, connection : IsolableUpdateConnection, - candidateKey : CandidateKeyT & AssertNonUnion, + candidateKey : CandidateKeyT,// & AssertNonUnion, assignmentMapDelegate : AssignmentMapDelegate ) : Promise> { return connection.transactionIfNotInOne(async (connection) : Promise> => { diff --git a/src/execution/util/operation-update/update-and-fetch-zero-or-one-by-candidate-key.ts b/src/execution/util/operation-update/update-and-fetch-zero-or-one-by-candidate-key.ts index cb71d87e..e9dcb70c 100644 --- a/src/execution/util/operation-update/update-and-fetch-zero-or-one-by-candidate-key.ts +++ b/src/execution/util/operation-update/update-and-fetch-zero-or-one-by-candidate-key.ts @@ -3,7 +3,7 @@ import {ITable, TableUtil} from "../../../table"; import {IsolableUpdateConnection} from "../../connection"; import {AssignmentMapDelegate, CustomAssignmentMap} from "../../../update"; import {CandidateKey_NonUnion} from "../../../candidate-key"; -import {StrictUnion, AssertNonUnion} from "../../../type-util"; +import {StrictUnion} from "../../../type-util"; import {UpdateOneResult} from "./update-one"; import * as ExprLib from "../../../expr-library"; import {NotFoundUpdateResult, updateZeroOrOne} from "./update-zero-or-one"; @@ -28,7 +28,7 @@ export async function updateAndFetchZeroOrOneByCandidateKey< > ( table : TableT, connection : IsolableUpdateConnection, - candidateKey : CandidateKeyT & AssertNonUnion, + candidateKey : CandidateKeyT,// & AssertNonUnion, assignmentMapDelegate : AssignmentMapDelegate ) : Promise> { return connection.transactionIfNotInOne(async (connection) : Promise> => { diff --git a/src/expr-library/control-flow/coalesce.ts b/src/expr-library/control-flow/coalesce.ts index 1fa459a4..6d0d31d0 100644 --- a/src/expr-library/control-flow/coalesce.ts +++ b/src/expr-library/control-flow/coalesce.ts @@ -1,57 +1,11 @@ import * as tm from "type-mapping"; import {AnyBuiltInExpr, BuiltInExprUtil} from "../../built-in-expr"; -import {PopFront} from "../../tuple-util"; import {ExprUtil} from "../../expr"; import {IExpr} from "../../expr/expr"; import {operatorNode2ToN} from "../../ast/operator-node/util"; import {OperatorType} from "../../operator-type"; +import {TypeOfCoalesce} from "./type-of-coalesce"; -/** - * `COALESCE()` with zero args is just the `NULL` constant. - */ -export type TypeOfCoalesce = - { - 0 : ( - /** - * Can't perform fancy computation with a regular array - */ - BuiltInExprUtil.TypeOf - ), - 1 : ( - /** - * Either the tuple started empty or we have exhausted - * all elements and not found a non-nullable arg. - */ - ResultT - ), - 2 : ( - /** - * This argument is nullable, keep looking - */ - TypeOfCoalesce< - PopFront, - ( - | ResultT - | BuiltInExprUtil.TypeOf - ) - > - ), - 3 : ( - /** - * We have found our non-nullable argument - */ - BuiltInExprUtil.TypeOf|Exclude - ), - }[ - number extends ArgsT["length"] ? - 0 : - 0 extends ArgsT["length"] ? - 1 : - null extends BuiltInExprUtil.TypeOf ? - 2 : - 3 - ] -; export type CoalesceExpr = ExprUtil.Intersect< TypeOfCoalesce, diff --git a/src/expr-library/control-flow/if-null.ts b/src/expr-library/control-flow/if-null.ts index 26b809b2..01cfe408 100644 --- a/src/expr-library/control-flow/if-null.ts +++ b/src/expr-library/control-flow/if-null.ts @@ -1,9 +1,10 @@ //import * as tm from "type-mapping"; import {AnyBuiltInExpr, BuiltInExprUtil} from "../../built-in-expr"; -import {CoalesceExpr, TypeOfCoalesce, coalesceMapper} from "./coalesce"; +import {CoalesceExpr, coalesceMapper} from "./coalesce"; import {ExprUtil} from "../../expr"; import {OperatorNodeUtil} from "../../ast"; import {OperatorType} from "../../operator-type"; +import {TypeOfCoalesce} from "./type-of-coalesce"; export function ifNull< Arg0T extends AnyBuiltInExpr, diff --git a/src/expr-library/control-flow/index.ts b/src/expr-library/control-flow/index.ts index 764cee90..ee3d8b12 100644 --- a/src/expr-library/control-flow/index.ts +++ b/src/expr-library/control-flow/index.ts @@ -6,3 +6,4 @@ export * from "./coalesce"; export * from "./if-null"; export * from "./if"; export * from "./null-if"; +export * from "./type-of-coalesce"; diff --git a/src/expr-library/control-flow/type-of-coalesce.ts b/src/expr-library/control-flow/type-of-coalesce.ts new file mode 100644 index 00000000..b5d9d737 --- /dev/null +++ b/src/expr-library/control-flow/type-of-coalesce.ts @@ -0,0 +1,237 @@ +import {AnyBuiltInExpr, BuiltInExprUtil} from "../../built-in-expr"; +import {PopFront} from "../../tuple-util"; + +// /** +// * `COALESCE()` with zero args is just the `NULL` constant. +// */ +// export type TypeOfCoalesceDeprecated = +// { +// 0 : ( +// /** +// * Can't perform fancy computation with a regular array +// */ +// BuiltInExprUtil.TypeOf +// ), +// 1 : ( +// /** +// * Either the tuple started empty or we have exhausted +// * all elements and not found a non-nullable arg. +// */ +// ResultT +// ), +// 2 : ( +// /** +// * This argument is nullable, keep looking +// */ +// TypeOfCoalesceDeprecated< +// PopFront, +// ( +// | ResultT +// | BuiltInExprUtil.TypeOf +// ) +// > +// ), +// 3 : ( +// /** +// * We have found our non-nullable argument +// */ +// BuiltInExprUtil.TypeOf|Exclude +// ), +// }[ +// number extends ArgsT["length"] ? +// 0 : +// 0 extends ArgsT["length"] ? +// 1 : +// null extends BuiltInExprUtil.TypeOf ? +// 2 : +// 3 +// ] +// ; + +import {MaxDepth, DecrementMaxDepth} from "../../tuple-util/trampoline-util"; + +/** + * The state of our `TypeOfCoalesce<>` algorithm. + */ +interface TypeOfCoalesce_State { + /** + * Are we done computing? + */ + done : boolean, + /** + * The tuple to coalesce. + * Should be an empty tuple if we are `done`. + */ + arr : readonly AnyBuiltInExpr[], + /** + * The result. + * If we are not `done`, it will only contain a **partial** result. + */ + result : unknown, +}; + +/** + * Performs `8` iterations of our `TypeOfCoalesce<>` algorithm. + * It looks a lot like our naive implementation. + * + * The difference is that we only do `8` recursive iterations (to prevent going over the max depth). + * We also return a `TypeOfCoalesce_State`. + */ +type TypeOfCoalesce_Bounce< + ArrT extends readonly AnyBuiltInExpr[], + ResultT extends unknown, + MaxDepthT extends number=MaxDepth +> = + { + /** + * Can't perform fancy computation with a regular array + */ + 0 : { done : true, arr : ArrT, result : BuiltInExprUtil.TypeOf }, + /** + * Either the tuple started empty or we have exhausted + * all elements and not found a non-nullable arg. + */ + 1 : { done : true, arr : ArrT, result : ResultT }, + /** + * We ran out of `MaxDepthT` and haven't completed the computation. + */ + 2 : { + done : false, + arr : PopFront, + result : ( + | ResultT + | BuiltInExprUtil.TypeOf + ), + }, + /** + * Keep trying to compute the type. + */ + /** + * This argument is nullable, keep looking + */ + 3 : TypeOfCoalesce_Bounce< + PopFront, + ( + | ResultT + | BuiltInExprUtil.TypeOf + ), + DecrementMaxDepth + >, + /** + * Keep trying to compute the type. + */ + /** + * We have found our non-nullable argument + */ + 4 : { done : true, arr : ArrT, result : BuiltInExprUtil.TypeOf|Exclude } + }[ + number extends ArrT["length"] ? + 0 : + ArrT["length"] extends 0 ? + 1 : + MaxDepthT extends 0 ? + 2 : + null extends BuiltInExprUtil.TypeOf ? + 3 : + 4 + ] +; + +/** + * If we are `done`, we don't need to compute anything else. + * + * Performs up to `8` iterations of our `TypeOfCoalesce<>` algorithm. + */ +type TypeOfCoalesce_Bounce1 = + StateT["done"] extends true ? + /** + * Reuse the `StateT` type. + * Creating fewer unnecessary types is better. + */ + StateT : + /** + * Iterate. + */ + TypeOfCoalesce_Bounce +; + +/** + * Calls `TypeOfCoalesce_Bounce1<>` 8 times. + * + * So, this supports coalescing a tuple less than length `8*8 = 64` + * + * There is no real reason why the limit was set to `64`. + * It could have easily been higher or lower. + * + * However, if you are coalescing really large tuples while using this + * library, you must either be writing **really** large SQL queries + * or are doing something wrong. + */ +type TypeOfCoalesce_Trampoline = + TypeOfCoalesce_Bounce1<{ done : false, arr : ArrT, result : ResultT }> extends infer S0 ? + ( + TypeOfCoalesce_Bounce1> extends infer S1 ? + ( + TypeOfCoalesce_Bounce1> extends infer S2 ? + ( + TypeOfCoalesce_Bounce1> extends infer S3 ? + ( + TypeOfCoalesce_Bounce1> extends infer S4 ? + ( + TypeOfCoalesce_Bounce1> extends infer S5 ? + ( + TypeOfCoalesce_Bounce1> extends infer S6 ? + ( + TypeOfCoalesce_Bounce1> extends infer S7 ? + ( + S7 + ) : + never + ) : + never + ) : + never + ) : + never + ) : + never + ) : + never + ) : + never + ): + never +; + +/** + * `COALESCE()` with zero args is just the `NULL` constant. + */ +/** + * Coalesces a tuple. + * + * ```ts + * //type Result = 1|2|3 + * type Result = TypeOfCoalesce<[1|null, 2|null, 3, 4, 5|null, 6]> + * ``` + * + * This supports coalescing a tuple less than length `8*8 = 64` + * + * There is no real reason why the limit was set to `64`. + * It could have easily been higher or lower. + * + * However, if you are coalescing really large tuples while using this + * library, you must either be writing **really** large SQL queries + * or are doing something wrong. + */ +export type TypeOfCoalesce = + TypeOfCoalesce_Trampoline extends { + done : infer DoneT, + result : infer R, + } ? + ( + DoneT extends true ? + R : + never + ) : + never +; diff --git a/src/table/table-impl.ts b/src/table/table-impl.ts index cb5986bf..20e49561 100644 --- a/src/table/table-impl.ts +++ b/src/table/table-impl.ts @@ -1237,7 +1237,7 @@ export class Table implements ITable { AssignmentMapT extends ExecutionUtil.UpdateAndFetchOneByCandidateKeyAssignmentMap > ( connection : IsolableUpdateConnection, - candidateKey : CandidateKeyT & AssertNonUnion, + candidateKey : CandidateKeyT,// & AssertNonUnion, assignmentMapDelegate : AssignmentMapDelegate ) : Promise> { return ExecutionUtil.updateAndFetchOneByCandidateKey< @@ -1306,7 +1306,7 @@ export class Table implements ITable { AssignmentMapT extends ExecutionUtil.UpdateAndFetchOneByCandidateKeyAssignmentMap > ( connection : IsolableUpdateConnection, - candidateKey : CandidateKeyT & AssertNonUnion, + candidateKey : CandidateKeyT,// & AssertNonUnion, assignmentMapDelegate : AssignmentMapDelegate ) : Promise> { return ExecutionUtil.updateAndFetchZeroOrOneByCandidateKey< diff --git a/test/run-time/input/design-pattern-table-per-type/update-and-fetch-one-by-primary-key/basic-app-key-example-server.ts b/test/run-time/input/design-pattern-table-per-type/update-and-fetch-one-by-primary-key/basic-app-key-example-server.ts new file mode 100644 index 00000000..3ec55233 --- /dev/null +++ b/test/run-time/input/design-pattern-table-per-type/update-and-fetch-one-by-primary-key/basic-app-key-example-server.ts @@ -0,0 +1,96 @@ +import * as tape from "tape"; +import * as tsql from "../../../../../dist"; +import {Pool} from "../../sql-web-worker/promise.sql"; +import {SqliteWorker} from "../../sql-web-worker/worker.sql"; +import {createAppKeyTableSql, serverAppKeyTpt} from "../app-key-example"; + +tape(__filename, async (t) => { + const pool = new Pool(new SqliteWorker()); + + await pool.acquire(async (connection) => { + await connection.exec(createAppKeyTableSql); + + await serverAppKeyTpt.insertAndFetch( + connection, + { + appId : BigInt(1), + key : "server", + createdAt : new Date(1), + disabledAt : new Date(2), + ipAddress : "ip", + trustProxy : false, + } + ).then((insertResult) => { + t.deepEqual( + insertResult, + { + appKeyId: BigInt(1), + appKeyTypeId: BigInt(1), + ipAddress : "ip", + trustProxy : false, + appId: BigInt(1), + key: "server", + createdAt: new Date(1), + disabledAt: new Date(2), + } + ); + }); + + await serverAppKeyTpt.fetchOne( + connection, + (columns) => tsql.eq( + columns.serverAppKey.appKeyId, + BigInt(1) + ) + ).orUndefined( + ).then((fetchOneResult) => { + t.deepEqual( + fetchOneResult, + { + appKeyId: BigInt(1), + appKeyTypeId: BigInt(1), + ipAddress : "ip", + trustProxy : false, + appId: BigInt(1), + key: "server", + createdAt: new Date(1), + disabledAt: new Date(2), + } + ); + }); + + + await serverAppKeyTpt.updateAndFetchOneByCandidateKey( + connection, + { + appKeyId : BigInt(1), + }, + () => { + return { + ipAddress : "ip2", + trustProxy : true, + key : "server2", + disabledAt : new Date(4), + }; + } + ).then((updateAndFetchOneResult) => { + //console.log(updateAndFetchOneResult.updateOneResults); + t.deepEqual( + updateAndFetchOneResult.row, + { + appKeyId: BigInt(1), + appKeyTypeId: BigInt(1), + ipAddress : "ip2", + trustProxy : true, + appId: BigInt(1), + key: "server2", + createdAt: new Date(1), + disabledAt: new Date(4), + } + ); + }); + + }); + + t.end(); +}); diff --git a/test/run-time/input/design-pattern-table-per-type/update-and-fetch-one-by-primary-key/empty-update-app-key-example-server.ts b/test/run-time/input/design-pattern-table-per-type/update-and-fetch-one-by-primary-key/empty-update-app-key-example-server.ts new file mode 100644 index 00000000..864b44ad --- /dev/null +++ b/test/run-time/input/design-pattern-table-per-type/update-and-fetch-one-by-primary-key/empty-update-app-key-example-server.ts @@ -0,0 +1,96 @@ +import * as tape from "tape"; +import * as tsql from "../../../../../dist"; +import {Pool} from "../../sql-web-worker/promise.sql"; +import {SqliteWorker} from "../../sql-web-worker/worker.sql"; +import {createAppKeyTableSql, serverAppKeyTpt} from "../app-key-example"; + +tape(__filename, async (t) => { + const pool = new Pool(new SqliteWorker()); + + await pool.acquire(async (connection) => { + await connection.exec(createAppKeyTableSql); + + await serverAppKeyTpt.insertAndFetch( + connection, + { + appId : BigInt(1), + key : "server", + createdAt : new Date(1), + disabledAt : new Date(2), + ipAddress : "ip", + trustProxy : false, + } + ).then((insertResult) => { + t.deepEqual( + insertResult, + { + appKeyId: BigInt(1), + appKeyTypeId: BigInt(1), + ipAddress : "ip", + trustProxy : false, + appId: BigInt(1), + key: "server", + createdAt: new Date(1), + disabledAt: new Date(2), + } + ); + }); + + await serverAppKeyTpt.fetchOne( + connection, + (columns) => tsql.eq( + columns.serverAppKey.appKeyId, + BigInt(1) + ) + ).orUndefined( + ).then((fetchOneResult) => { + t.deepEqual( + fetchOneResult, + { + appKeyId: BigInt(1), + appKeyTypeId: BigInt(1), + ipAddress : "ip", + trustProxy : false, + appId: BigInt(1), + key: "server", + createdAt: new Date(1), + disabledAt: new Date(2), + } + ); + }); + + + await serverAppKeyTpt.updateAndFetchOneByCandidateKey( + connection, + { + appKeyId : BigInt(1), + }, + () => { + return { + }; + } + ).then((updateAndFetchOneResult) => { + //console.log(updateAndFetchOneResult.updateOneResults); + t.deepEqual( + updateAndFetchOneResult.updatedRowCount, + BigInt(0) + ); + t.deepEqual( + updateAndFetchOneResult.row, + { + appKeyId: BigInt(1), + appKeyTypeId: BigInt(1), + ipAddress : "ip", + trustProxy : false, + appId: BigInt(1), + key: "server", + createdAt: new Date(1), + disabledAt: new Date(2), + } + ); + }); + + }); + + t.end(); +}); diff --git a/test/run-time/input/design-pattern-table-per-type/update-and-fetch-one-by-primary-key/partial-update-app-key-example-server.ts b/test/run-time/input/design-pattern-table-per-type/update-and-fetch-one-by-primary-key/partial-update-app-key-example-server.ts new file mode 100644 index 00000000..f40580d0 --- /dev/null +++ b/test/run-time/input/design-pattern-table-per-type/update-and-fetch-one-by-primary-key/partial-update-app-key-example-server.ts @@ -0,0 +1,95 @@ +import * as tape from "tape"; +import * as tsql from "../../../../../dist"; +import {Pool} from "../../sql-web-worker/promise.sql"; +import {SqliteWorker} from "../../sql-web-worker/worker.sql"; +import {createAppKeyTableSql, serverAppKeyTpt} from "../app-key-example"; + +tape(__filename, async (t) => { + const pool = new Pool(new SqliteWorker()); + + await pool.acquire(async (connection) => { + await connection.exec(createAppKeyTableSql); + + await serverAppKeyTpt.insertAndFetch( + connection, + { + appId : BigInt(1), + key : "server", + createdAt : new Date(1), + disabledAt : new Date(2), + ipAddress : "ip", + trustProxy : false, + } + ).then((insertResult) => { + t.deepEqual( + insertResult, + { + appKeyId: BigInt(1), + appKeyTypeId: BigInt(1), + ipAddress : "ip", + trustProxy : false, + appId: BigInt(1), + key: "server", + createdAt: new Date(1), + disabledAt: new Date(2), + } + ); + }); + + await serverAppKeyTpt.fetchOne( + connection, + (columns) => tsql.eq( + columns.serverAppKey.appKeyId, + BigInt(1) + ) + ).orUndefined( + ).then((fetchOneResult) => { + t.deepEqual( + fetchOneResult, + { + appKeyId: BigInt(1), + appKeyTypeId: BigInt(1), + ipAddress : "ip", + trustProxy : false, + appId: BigInt(1), + key: "server", + createdAt: new Date(1), + disabledAt: new Date(2), + } + ); + }); + + + await serverAppKeyTpt.updateAndFetchOneByCandidateKey( + connection, + { + appKeyId : BigInt(1), + }, + () => { + return { + ipAddress : null, + key : "server2", + disabledAt : new Date(4), + }; + } + ).then((updateAndFetchOneResult) => { + //console.log(updateAndFetchOneResult.updateOneResults); + t.deepEqual( + updateAndFetchOneResult.row, + { + appKeyId: BigInt(1), + appKeyTypeId: BigInt(1), + ipAddress : null, + trustProxy : false, + appId: BigInt(1), + key: "server2", + createdAt: new Date(1), + disabledAt: new Date(4), + } + ); + }); + + }); + + t.end(); +}); diff --git a/test/run-time/input/design-pattern-table-per-type/update-and-fetch-one-by-primary-key/with-column-reference-app-key-example-server.ts b/test/run-time/input/design-pattern-table-per-type/update-and-fetch-one-by-primary-key/with-column-reference-app-key-example-server.ts new file mode 100644 index 00000000..cd90952b --- /dev/null +++ b/test/run-time/input/design-pattern-table-per-type/update-and-fetch-one-by-primary-key/with-column-reference-app-key-example-server.ts @@ -0,0 +1,110 @@ +import * as tape from "tape"; +import * as tsql from "../../../../../dist"; +import {Pool} from "../../sql-web-worker/promise.sql"; +import {SqliteWorker} from "../../sql-web-worker/worker.sql"; +import {createAppKeyTableSql, serverAppKeyTpt} from "../app-key-example"; + +tape(__filename, async (t) => { + const pool = new Pool(new SqliteWorker()); + + await pool.acquire(async (connection) => { + await connection.exec(createAppKeyTableSql); + + await serverAppKeyTpt.insertAndFetch( + connection, + { + appId : BigInt(1), + key : "server", + createdAt : new Date(1), + disabledAt : new Date(2), + ipAddress : "ip", + trustProxy : false, + } + ).then((insertResult) => { + t.deepEqual( + insertResult, + { + appKeyId: BigInt(1), + appKeyTypeId: BigInt(1), + ipAddress : "ip", + trustProxy : false, + appId: BigInt(1), + key: "server", + createdAt: new Date(1), + disabledAt: new Date(2), + } + ); + }); + + await serverAppKeyTpt.fetchOne( + connection, + (columns) => tsql.eq( + columns.serverAppKey.appKeyId, + BigInt(1) + ) + ).orUndefined( + ).then((fetchOneResult) => { + t.deepEqual( + fetchOneResult, + { + appKeyId: BigInt(1), + appKeyTypeId: BigInt(1), + ipAddress : "ip", + trustProxy : false, + appId: BigInt(1), + key: "server", + createdAt: new Date(1), + disabledAt: new Date(2), + } + ); + }); + + + await serverAppKeyTpt.updateAndFetchOneByCandidateKey( + connection, + { + appKeyId : BigInt(1), + }, + columns => { + return { + ipAddress : tsql.concat( + tsql.coalesce(columns.serverAppKey.ipAddress, ""), + "-x" + ), + trustProxy : tsql.not(columns.serverAppKey.trustProxy), + key : tsql.concat( + tsql.coalesce(columns.serverAppKey.ipAddress, ""), + "-", + columns.appKey.key, + "-y" + ), + disabledAt : tsql.timestampAddMillisecond( + tsql.coalesce( + columns.appKey.disabledAt, + new Date(0) + ), + 5 + ), + }; + } + ).then((updateAndFetchOneResult) => { + console.log(updateAndFetchOneResult.updateOneResults); + t.deepEqual( + updateAndFetchOneResult.row, + { + appKeyId: BigInt(1), + appKeyTypeId: BigInt(1), + ipAddress : "ip-x", + trustProxy : true, + appId: BigInt(1), + key: "ip-server-y", + createdAt: new Date(1), + disabledAt: new Date(7), + } + ); + }); + + }); + + t.end(); +}); diff --git a/test/sqlite-sqlfier.ts b/test/sqlite-sqlfier.ts index 213bdcf8..59970da3 100644 --- a/test/sqlite-sqlfier.ts +++ b/test/sqlite-sqlfier.ts @@ -36,6 +36,7 @@ import { TypeHint, Parentheses, pascalStyleEscapeString, + parentheses, } from "../dist"; import {LiteralValueType, LiteralValueNodeUtil} from "../dist/ast/literal-value-node"; @@ -737,6 +738,30 @@ export const sqliteSqlfier : Sqlfier = { Date and Time Functions https://dev.mysql.com/doc/refman/8.0/en/date-and-time-functions.html */ + [OperatorType.TIMESTAMPADD_MILLISECOND] : ({operands}) => functionCall( + "strftime", + [ + pascalStyleEscapeString("%Y-%m-%d %H:%M:%f"), + operands[0], + insertBetween( + [ + parentheses( + insertBetween( + [ + operands[1], + "1000e0" + ], + "/" + ), + //canUnwrap + false + ), + pascalStyleEscapeString(" second") + ], + "||" + ) + ] + ), [OperatorType.TIMESTAMPADD_DAY] : ({operands}) => functionCall( "strftime", [