From 10fe97082e2e9ba8e66d48eec77eb02b9ee292a6 Mon Sep 17 00:00:00 2001 From: anyhowstep <5655961+AnyhowStep@users.noreply.github.com> Date: Fri, 13 Dec 2019 17:02:14 -0500 Subject: [PATCH 1/6] Added TablePerType.updateAndFetchOneByCandidateKey() --- .../array-util/constructor/from-column-map.ts | 16 + .../table-per-type-impl.ts | 25 +- .../util/execution/index.ts | 1 + .../util/execution/insert-and-fetch.ts | 4 +- .../update-and-fetch-one-by-candidate-key.ts | 323 ++++++++++++++++++ .../update-and-fetch-one-by-candidate-key.ts | 6 +- ...-and-fetch-zero-or-one-by-candidate-key.ts | 4 +- src/table/table-impl.ts | 4 +- .../basic-app-key-example-server.ts | 96 ++++++ 9 files changed, 468 insertions(+), 11 deletions(-) create mode 100644 src/design-pattern-table-per-type/util/execution/update-and-fetch-one-by-candidate-key.ts create mode 100644 test/run-time/input/design-pattern-table-per-type/update-and-fetch-one-by-primary-key/basic-app-key-example-server.ts 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/index.ts b/src/design-pattern-table-per-type/util/execution/index.ts index 142e42e9..3d714873 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,4 @@ export * from "./fetch-one"; export * from "./insert-and-fetch"; export * from "./insert-row"; +export * from "./update-and-fetch-one-by-candidate-key"; 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..b2a2479f 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 @@ -132,7 +132,7 @@ export async function insertAndFetch< if (BuiltInExprUtil.isAnyNonValueExpr(curValue)) { /** - * Add this new value to the `rawInsertRow` + * Add this new value to the `result` * so we can use it to insert rows to tables * further down the inheritance hierarchy. */ @@ -167,7 +167,7 @@ export async function insertAndFetch< } } else { /** - * Add this new value to the `rawInsertRow` + * Add this new value to the `result` * so we can use it to insert rows to tables * further down the inheritance hierarchy. */ 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..e5a82e35 --- /dev/null +++ b/src/design-pattern-table-per-type/util/execution/update-and-fetch-one-by-candidate-key.ts @@ -0,0 +1,323 @@ +import * as tm from "type-mapping"; +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, columnMapper, isMutableColumnAlias} from "../query"; +import {CustomExpr_MapCorrelatedOrUndefined, CustomExprUtil} from "../../../custom-expr"; +import {ColumnRefUtil} from "../../../column-ref"; +import {ColumnUtil, ColumnArrayUtil} from "../../../column"; +import {from} from "../execution-impl"; +import * as ExprLib from "../../../expr-library"; +import {BuiltInExprUtil} from "../../../built-in-expr"; +import {DataTypeUtil} from "../../../data-type"; +import {ExprUtil, expr} from "../../../expr"; +import {TableWithPrimaryKey} from "../../../table"; +import {UpdateOneResult} from "../../../execution/util"; + +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 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, +} + +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> { + 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 query = from(tpt) + .where(() => ExprLib.eqCandidateKey( + tpt.childTable, + candidateKey + ) as any) + .select(() => Object + .keys(rawAssignmentMap) + .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 : columnMapper(tpt, columnAlias), + 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. + */ + const cleanedAssignmentMap = await ExecutionUtil.fetchOne( + query as any, + connection + ) as Record; + + /** + * @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 + ) + ); + 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 + ); + const row = updateAndFetchParentResult.row; + + 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( + parentTable.columns[columnAlias], + /** + * This may throw + */ + parentTable.columns[columnAlias].mapper( + `${parentTable.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 ${parentTable.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; + } + } + } + + return { + updateOneResults, + /** + * +1 for the `childTable`. + */ + foundRowCount : tm.BigInt(tpt.parentTables.length + 1), + updatedRowCount, + + warningCount, + + row : result as UpdatedAndFetchedRow, + }; +} 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/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(); +}); From 629db73e8b9e8b554b6f36340815bc20c9387113 Mon Sep 17 00:00:00 2001 From: anyhowstep <5655961+AnyhowStep@users.noreply.github.com> Date: Fri, 13 Dec 2019 17:18:20 -0500 Subject: [PATCH 2/6] Added more tests --- .../update-and-fetch-one-by-candidate-key.ts | 85 ++++++++-------- .../empty-update-app-key-example-server.ts | 96 +++++++++++++++++++ .../partial-update-app-key-example-server.ts | 95 ++++++++++++++++++ 3 files changed, 237 insertions(+), 39 deletions(-) create mode 100644 test/run-time/input/design-pattern-table-per-type/update-and-fetch-one-by-primary-key/empty-update-app-key-example-server.ts create mode 100644 test/run-time/input/design-pattern-table-per-type/update-and-fetch-one-by-primary-key/partial-update-app-key-example-server.ts 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 index e5a82e35..2334339d 100644 --- 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 @@ -148,45 +148,52 @@ export async function updateAndFetchOneByCandidateKey< * or even columns at all. */ const rawAssignmentMap = assignmentMapDelegate(columns); - const query = from(tpt) - .where(() => ExprLib.eqCandidateKey( - tpt.childTable, - candidateKey - ) as any) - .select(() => Object - .keys(rawAssignmentMap) - .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 : columnMapper(tpt, columnAlias), - 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. - */ - const cleanedAssignmentMap = await ExecutionUtil.fetchOne( - query as any, - connection - ) as Record; + let processedAssignmentMap : Record|undefined = undefined; + if (Object.keys(rawAssignmentMap).length == 0) { + processedAssignmentMap = {}; + } else { + const query = from(tpt) + .where(() => ExprLib.eqCandidateKey( + tpt.childTable, + candidateKey + ) as any) + .select(() => Object + .keys(rawAssignmentMap) + .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 : columnMapper(tpt, columnAlias), + 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. + */ + processedAssignmentMap = await ExecutionUtil.fetchOne( + query as any, + connection + ) as Record; + } + + const cleanedAssignmentMap = processedAssignmentMap; /** * @todo If `result` contains any primaryKey values, 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(); +}); From eb9698ae86d902aa2bb1ae84b0e2fac9cf1d9fbe Mon Sep 17 00:00:00 2001 From: anyhowstep <5655961+AnyhowStep@users.noreply.github.com> Date: Fri, 13 Dec 2019 17:28:44 -0500 Subject: [PATCH 3/6] Moved to transaction --- .../update-and-fetch-one-by-candidate-key.ts | 309 +++++++++--------- 1 file changed, 156 insertions(+), 153 deletions(-) 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 index 2334339d..16033226 100644 --- 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 @@ -148,183 +148,186 @@ export async function updateAndFetchOneByCandidateKey< * or even columns at all. */ const rawAssignmentMap = assignmentMapDelegate(columns); - let processedAssignmentMap : Record|undefined = undefined; - if (Object.keys(rawAssignmentMap).length == 0) { - processedAssignmentMap = {}; - } else { - const query = from(tpt) - .where(() => ExprLib.eqCandidateKey( - tpt.childTable, - candidateKey - ) as any) - .select(() => Object - .keys(rawAssignmentMap) - .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 : columnMapper(tpt, columnAlias), - 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. - */ - processedAssignmentMap = await ExecutionUtil.fetchOne( - query as any, - connection - ) as Record; - } - const cleanedAssignmentMap = processedAssignmentMap; - - /** - * @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 - ) - ); - const updateOneResults : UpdateAndFetchOneResult["updateOneResults"] = [ - { - ...updateAndFetchChildResult, - table : tpt.childTable, - }, - ]; - let updatedRowCount : bigint = updateAndFetchChildResult.updatedRowCount; - let warningCount : bigint = updateAndFetchChildResult.warningCount; + return connection.transactionIfNotInOne(async (connection) : Promise> => { + let processedAssignmentMap : Record|undefined = undefined; + if (Object.keys(rawAssignmentMap).length == 0) { + processedAssignmentMap = {}; + } else { + const query = from(tpt) + .where(() => ExprLib.eqCandidateKey( + tpt.childTable, + candidateKey + ) as any) + .select(() => Object + .keys(rawAssignmentMap) + .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 : columnMapper(tpt, columnAlias), + 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. + */ + processedAssignmentMap = await ExecutionUtil.fetchOne( + query as any, + connection + ) as Record; + } - const result : Record = updateAndFetchChildResult.row; + const cleanedAssignmentMap = processedAssignmentMap; - /** - * 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, + /** + * @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, - /** - * The `result` should contain the primary key values we are interested in - */ - result, + candidateKey, () => pickOwnEnumerable( cleanedAssignmentMap, - parentTable.mutableColumns + tpt.childTable.mutableColumns ) ); - updateOneResults.push({ - ...updateAndFetchParentResult, - table : parentTable, - }); - updatedRowCount = tm.BigIntUtil.add( - updatedRowCount, - updateAndFetchParentResult.updatedRowCount - ); - warningCount = tm.BigIntUtil.add( - warningCount, - updateAndFetchParentResult.warningCount - ); - const row = updateAndFetchParentResult.row; + const updateOneResults : UpdateAndFetchOneResult["updateOneResults"] = [ + { + ...updateAndFetchChildResult, + table : tpt.childTable, + }, + ]; + let updatedRowCount : bigint = updateAndFetchChildResult.updatedRowCount; + let warningCount : bigint = updateAndFetchChildResult.warningCount; - for (const columnAlias of Object.keys(row)) { - /** - * This is guaranteed to be a value expression. - */ - const newValue = row[columnAlias]; + const result : Record = updateAndFetchChildResult.row; - if (Object.prototype.hasOwnProperty.call(result, columnAlias)) { + /** + * 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, /** - * This `curValue` could be a non-value expression. - * We only want value expressions. + * The `result` should contain the primary key values we are interested in */ - const curValue = result[columnAlias]; + result, + () => pickOwnEnumerable( + cleanedAssignmentMap, + parentTable.mutableColumns + ) + ); + updateOneResults.push({ + ...updateAndFetchParentResult, + table : parentTable, + }); + updatedRowCount = tm.BigIntUtil.add( + updatedRowCount, + updateAndFetchParentResult.updatedRowCount + ); + warningCount = tm.BigIntUtil.add( + warningCount, + updateAndFetchParentResult.warningCount + ); + const row = updateAndFetchParentResult.row; - 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; - } + for (const columnAlias of Object.keys(row)) { + /** + * This is guaranteed to be a value expression. + */ + const newValue = row[columnAlias]; - if (curValue === newValue) { + if (Object.prototype.hasOwnProperty.call(result, columnAlias)) { /** - * They are equal, do nothing. + * This `curValue` could be a non-value expression. + * We only want value expressions. */ - continue; - } - /** - * We need some custom equality checking logic - */ - if (!DataTypeUtil.isNullSafeEqual( - parentTable.columns[columnAlias], + 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; + } /** - * This may throw + * We need some custom equality checking logic */ - parentTable.columns[columnAlias].mapper( - `${parentTable.alias}.${columnAlias}`, - curValue - ), - newValue - )) { + if (!DataTypeUtil.isNullSafeEqual( + parentTable.columns[columnAlias], + /** + * This may throw + */ + parentTable.columns[columnAlias].mapper( + `${parentTable.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 ${parentTable.alias}.${columnAlias}`); + } + } else { /** - * @todo Custom `Error` type + * Add this new value to the `result` + * so we can use it to update rows of tables + * further down the inheritance hierarchy. */ - throw new Error(`All columns with the same name in an inheritance hierarchy must have the same value; mismatch found for ${parentTable.alias}.${columnAlias}`); + result[columnAlias] = newValue; } - } 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; } } - } - return { - updateOneResults, - /** - * +1 for the `childTable`. - */ - foundRowCount : tm.BigInt(tpt.parentTables.length + 1), - updatedRowCount, + return { + updateOneResults, + /** + * +1 for the `childTable`. + */ + foundRowCount : tm.BigInt(tpt.parentTables.length + 1), + updatedRowCount, - warningCount, + warningCount, - row : result as UpdatedAndFetchedRow, - }; + row : result as UpdatedAndFetchedRow, + }; + }); } From d6bab81ff67b1a9b838789317568d4b9c473079e Mon Sep 17 00:00:00 2001 From: anyhowstep <5655961+AnyhowStep@users.noreply.github.com> Date: Fri, 13 Dec 2019 17:41:29 -0500 Subject: [PATCH 4/6] Added badly named absorbRow() to factor out common code --- .../util/execution/absorb-row.ts | 77 +++++++++++++++++++ .../util/execution/insert-and-fetch.ts | 61 +-------------- .../update-and-fetch-one-by-candidate-key.ts | 61 +-------------- 3 files changed, 81 insertions(+), 118 deletions(-) create mode 100644 src/design-pattern-table-per-type/util/execution/absorb-row.ts 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/insert-and-fetch.ts b/src/design-pattern-table-per-type/util/execution/insert-and-fetch.ts index b2a2479f..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 `result` - * 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 `result` - * 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/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 index 16033226..e821083b 100644 --- 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 @@ -10,10 +10,10 @@ import {ColumnUtil, ColumnArrayUtil} from "../../../column"; import {from} from "../execution-impl"; import * as ExprLib from "../../../expr-library"; import {BuiltInExprUtil} from "../../../built-in-expr"; -import {DataTypeUtil} from "../../../data-type"; import {ExprUtil, expr} from "../../../expr"; import {TableWithPrimaryKey} from "../../../table"; import {UpdateOneResult} from "../../../execution/util"; +import {absorbRow} from "./absorb-row"; export type CustomAssignmentMap< TptT extends ITablePerType @@ -256,65 +256,8 @@ export async function updateAndFetchOneByCandidateKey< warningCount, updateAndFetchParentResult.warningCount ); - const row = updateAndFetchParentResult.row; - 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( - parentTable.columns[columnAlias], - /** - * This may throw - */ - parentTable.columns[columnAlias].mapper( - `${parentTable.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 ${parentTable.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; - } - } + absorbRow(result, parentTable, updateAndFetchParentResult.row); } return { From 13f54a304513da1618d27e1401301bbc8fb1bee0 Mon Sep 17 00:00:00 2001 From: anyhowstep <5655961+AnyhowStep@users.noreply.github.com> Date: Fri, 13 Dec 2019 18:04:41 -0500 Subject: [PATCH 5/6] Added updateAndFetchOneImpl() to share morecode --- .../util/execution/index.ts | 1 + .../update-and-fetch-one-by-candidate-key.ts | 95 ++-------------- .../execution/update-and-fetch-one-impl.ts | 106 ++++++++++++++++++ 3 files changed, 114 insertions(+), 88 deletions(-) create mode 100644 src/design-pattern-table-per-type/util/execution/update-and-fetch-one-impl.ts 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 3d714873..008aa4fb 100644 --- a/src/design-pattern-table-per-type/util/execution/index.ts +++ b/src/design-pattern-table-per-type/util/execution/index.ts @@ -2,3 +2,4 @@ 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/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 index e821083b..56c395d0 100644 --- 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 @@ -1,4 +1,3 @@ -import * as tm from "type-mapping"; import {ITablePerType} from "../../table-per-type"; import {CandidateKey_NonUnion} from "../../../candidate-key"; import {StrictUnion, Identity, pickOwnEnumerable} from "../../../type-util"; @@ -11,9 +10,7 @@ import {from} from "../execution-impl"; import * as ExprLib from "../../../expr-library"; import {BuiltInExprUtil} from "../../../built-in-expr"; import {ExprUtil, expr} from "../../../expr"; -import {TableWithPrimaryKey} from "../../../table"; -import {UpdateOneResult} from "../../../execution/util"; -import {absorbRow} from "./absorb-row"; +import {updateAndFetchOneImpl, UpdateAndFetchOneResult} from "./update-and-fetch-one-impl"; export type CustomAssignmentMap< TptT extends ITablePerType @@ -74,34 +71,6 @@ export type UpdatedAndFetchedRow< }> ; -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, -} - export type UpdateAndFetchOneReturnType< TptT extends ITablePerType, AssignmentMapT extends CustomAssignmentMap @@ -216,61 +185,11 @@ export async function updateAndFetchOneByCandidateKey< tpt.childTable.mutableColumns ) ); - 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 UpdatedAndFetchedRow, - }; + 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, + }; + }); +} From b22504b2642b3b34b58fde66f0cc0ded25488f38 Mon Sep 17 00:00:00 2001 From: anyhowstep <5655961+AnyhowStep@users.noreply.github.com> Date: Sat, 14 Dec 2019 19:23:50 -0500 Subject: [PATCH 6/6] Fixed TypeOfCoalesce<> max depth; Added invokeAssignmentDelegate() to factor out more shared code; Added tests --- .../execution/invoke-assignment-delegate.ts | 92 +++++++ .../update-and-fetch-one-by-candidate-key.ts | 86 +------ src/expr-library/control-flow/coalesce.ts | 48 +--- src/expr-library/control-flow/if-null.ts | 3 +- src/expr-library/control-flow/index.ts | 1 + .../control-flow/type-of-coalesce.ts | 237 ++++++++++++++++++ ...column-reference-app-key-example-server.ts | 110 ++++++++ test/sqlite-sqlfier.ts | 25 ++ 8 files changed, 480 insertions(+), 122 deletions(-) create mode 100644 src/design-pattern-table-per-type/util/execution/invoke-assignment-delegate.ts create mode 100644 src/expr-library/control-flow/type-of-coalesce.ts create mode 100644 test/run-time/input/design-pattern-table-per-type/update-and-fetch-one-by-primary-key/with-column-reference-app-key-example-server.ts 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 index 56c395d0..0b826b3b 100644 --- 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 @@ -2,15 +2,13 @@ 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, columnMapper, isMutableColumnAlias} from "../query"; +import {MutableColumnAlias, ColumnType, ColumnAlias} from "../query"; import {CustomExpr_MapCorrelatedOrUndefined, CustomExprUtil} from "../../../custom-expr"; import {ColumnRefUtil} from "../../../column-ref"; -import {ColumnUtil, ColumnArrayUtil} from "../../../column"; -import {from} from "../execution-impl"; +import {ColumnUtil} from "../../../column"; import * as ExprLib from "../../../expr-library"; -import {BuiltInExprUtil} from "../../../built-in-expr"; -import {ExprUtil, expr} from "../../../expr"; import {updateAndFetchOneImpl, UpdateAndFetchOneResult} from "./update-and-fetch-one-impl"; +import {invokeAssignmentDelegate} from "./invoke-assignment-delegate"; export type CustomAssignmentMap< TptT extends ITablePerType @@ -96,76 +94,16 @@ export async function updateAndFetchOneByCandidateKey< candidateKey : CandidateKeyT,// & AssertNonUnion, 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); - return connection.transactionIfNotInOne(async (connection) : Promise> => { - let processedAssignmentMap : Record|undefined = undefined; - if (Object.keys(rawAssignmentMap).length == 0) { - processedAssignmentMap = {}; - } else { - const query = from(tpt) - .where(() => ExprLib.eqCandidateKey( - tpt.childTable, - candidateKey - ) as any) - .select(() => Object - .keys(rawAssignmentMap) - .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 : columnMapper(tpt, columnAlias), - 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. - */ - processedAssignmentMap = await ExecutionUtil.fetchOne( - query as any, - connection - ) as Record; - } - - const cleanedAssignmentMap = processedAssignmentMap; - + 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, 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/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", [