Skip to content

Commit bbf5171

Browse files
committed
Add support to custom reusable SQL fragments that the returning value can be optional or required depending on the provided arguments
1 parent ec66251 commit bbf5171

File tree

16 files changed

+1094
-7
lines changed

16 files changed

+1094
-7
lines changed

docs/queries/sql-fragments.md

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ class DBConnection extends PostgreSqlConnection<'DBConnection'> {
5555
}
5656
```
5757

58-
You will define the function `bitwiseShiftLeft` that receives two `int` as argument and returns an `int`; this arguments can be numbers or elements in the database that represents integer numbers. If you create the argument using the function `valueArg` instead of the `arg` function, the defined function only will accept values but not elements of the database. You can use the defined function as a regular database function in your query.
58+
You will define the function `bitwiseShiftLeft` that receives two `int` as argument and returns an `int`; these arguments can be numbers or elements in the database that represent integer numbers. If you create the argument using the function `valueArg` instead of the `arg` function, the defined function will accept values only but not database elements. You can use the defined function as a regular database function in your query.
5959

6060
```ts
6161
const bitwiseMovements = 1;
@@ -115,7 +115,7 @@ class DBConnection extends PostgreSqlConnection<'DBConnection'> {
115115
}
116116
```
117117

118-
You will define the function `bitwiseShiftLeft` that receives two `int` as argument and returns an `int`; this arguments can be numbers or elements in the database that represents integer numbers. If you create the argument using the function `valueArg` instead of the `arg` function, the defined function only will accept values but not elements of the database. You can use the defined function as a regular database function in your query.
118+
You will define the function `bitwiseShiftLeft` that receives two `int` as argument and returns an `int`; these arguments can be numbers or elements in the database that represent integer numbers. If you create the argument using the function `valueArg` instead of the `arg` function, the defined function will accept values only but not database elements. You can use the defined function as a regular database function in your query.
119119

120120
```ts
121121
const noValue = null
@@ -148,6 +148,60 @@ const companiesUsingCustomFunctionFragment: Promise<{
148148
}[]>
149149
```
150150
151+
## Select with custom reusable SQL fragment (maybe optional)
152+
153+
You can define functions in your connection that create custom reusable SQL fragments that detect if the returned value is required or optional based on the provided arguments, that give you the possibility to do some operations or functions not included in ts-sql-query allowing to have overloaded version where the returned type can be required or optional.
154+
155+
If you define your connection like:
156+
157+
```ts
158+
import { PostgreSqlConnection } from "ts-sql-query/connections/PostgreSqlConnection";
159+
160+
class DBConnection extends PostgreSqlConnection<'DBConnection'> {
161+
162+
bitwiseShiftLeft = this.buildFragmentWithMaybeOptionalArgs(
163+
this.arg('int', 'optional'),
164+
this.arg('int', 'optional')
165+
).as((left, right) => {
166+
// The fragment here is: ${left} << ${right}
167+
// Could be another fragment like a function call: myFunction(${left}, ${right})
168+
return this.fragmentWithType('int', 'optional').sql`${left} << ${right}`
169+
})
170+
}
171+
```
172+
173+
You will define the function `bitwiseShiftLeft` that receives two `int` as argument and returns an `int`; these arguments can be numbers or elements in the database that represent integer numbers. If you create the argument using the function `valueArg` instead of the `arg` function, the defined function will accept values only but not database elements. You can use the defined function as a regular database function in your query. The function will return an optional value if any of the provided arguments when invoked is optional; otherwise, the return type will be marked as required. **Note**: All arguments that can be optional must be marked as optional; the return fragment must be marked as optional.
174+
175+
```ts
176+
const bitwiseMovements = null;
177+
const multiplier = 2;
178+
179+
const companiesUsingCustomFunctionFragment = connection.selectFrom(tCompany)
180+
.select({
181+
id: tCompany.id,
182+
name: tCompany.name,
183+
idMultiplyBy2: connection.bitwiseShiftLeft(tCompany.id, bitwiseMovements)
184+
})
185+
.executeSelectMany();
186+
```
187+
188+
The executed query is:
189+
```sql
190+
select id as id, name as name, id << $1 as idMultiplyBy2
191+
from company
192+
```
193+
194+
The parameters are: `[ null, 2 ]`
195+
196+
The result type is:
197+
```tsx
198+
const companiesUsingCustomFunctionFragment: Promise<{
199+
id: number;
200+
name: string;
201+
idMultiplyBy2?: number;
202+
}[]>
203+
```
204+
151205
## Raw SQL
152206
153207
ts-sql-query offers you to write raw sql to extends and customize the generated queries in several places

docs/supported-operations.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -796,6 +796,7 @@ interface Connection {
796796
*/
797797
buildFragmentWithArgs(...argumentDefinitions: Argument[]): FragmentBuilder
798798
buildFragmentWithArgsIfValue(...argumentDefinitions: Argument[]): FragmentBuilderIfValue
799+
buildFragmentWithMaybeOptionalArgs(...argumentDefinitions: Argument[]): FragmentBuilderMaybeOptional
799800

800801
/**
801802
* Return the same special neutral boolean mark returned by the IfValue functions when there is no value
@@ -884,6 +885,18 @@ interface FragmentBuilderIfValue {
884885
as(impl: (...args: AnyValueSource[]) => AnyValueSource): (...args: any) => BooleanValueSource
885886
}
886887

888+
interface FragmentBuilderMaybeOptional {
889+
/*
890+
* The impl function will receive the proper ValueSource type according to the argument definition.
891+
* The nunber of arguments is the same specified in the function buildFragmentWithArgs (up to 5 arguments).
892+
* The arguments of the returned function will have the proper parameters type.
893+
* The function will return an optional value if any of the provided arguments when invoked is optional;
894+
* otherwise, the return type will be marked as required.
895+
* All arguments that can be optional must be marked as optional; the return fragment must be marked as optional.
896+
*/
897+
as(impl: (...args: AnyValueSource[]) => AnyValueSource): (...args: any) => AnyValueSource
898+
}
899+
887900
interface Sequence<T> {
888901
nextValue(): T
889902
currentValue(): T

src/connections/AbstractConnection.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import type { TypeAdapter, DefaultTypeAdapter } from "../TypeAdapter"
1010
import type { int, double, LocalDate, LocalTime, LocalDateTime, stringInt, stringDouble, uuid } from "ts-extended-types"
1111
import type { QueryRunner } from "../queryRunners/QueryRunner"
1212
import type { IConnection } from "../utils/IConnection"
13-
import type { BooleanFragmentExpression, StringIntFragmentExpression, StringNumberFragmentExpression, IntFragmentExpression, NumberFragmentExpression, StringDoubleFragmentExpression, DoubleFragmentExpression, TypeSafeStringFragmentExpression, StringFragmentExpression, LocalDateFragmentExpression, DateFragmentExpression, LocalTimeFragmentExpression, TimeFragmentExpression, LocalDateTimeFragmentExpression, DateTimeFragmentExpression, EqualableFragmentExpression, ComparableFragmentExpression, FragmentBuilder1TypeSafe, FragmentBuilder0, FragmentBuilder1TypeUnsafe, FragmentBuilder2TypeSafe, FragmentBuilder2TypeUnsafe, FragmentBuilder3TypeSafe, FragmentBuilder3TypeUnsafe, FragmentBuilder4TypeSafe, FragmentBuilder4TypeUnsafe, FragmentBuilder5TypeSafe, FragmentBuilder5TypeUnsafe, FragmentBuilder0IfValue, FragmentBuilder1IfValueTypeSafe, FragmentBuilder1IfValueTypeUnsafe, FragmentBuilder2IfValueTypeSafe, FragmentBuilder2IfValueTypeUnsafe, FragmentBuilder3IfValueTypeSafe, FragmentBuilder3IfValueTypeUnsafe, FragmentBuilder4IfValueTypeSafe, FragmentBuilder4IfValueTypeUnsafe, FragmentBuilder5IfValueTypeSafe, FragmentBuilder5IfValueTypeUnsafe, BigintFragmentExpression, TypeSafeBigintFragmentExpression, TypeSafeUuidFragmentExpression, UuidFragmentExpression, CustomIntFragmentExpression, CustomDoubleFragmentExpression, CustomUuidFragmentExpression, CustomLocalDateFragmentExpression, CustomLocalTimeFragmentExpression, CustomLocalDateTimeFragmentExpression } from "../expressions/fragment"
13+
import type { BooleanFragmentExpression, StringIntFragmentExpression, StringNumberFragmentExpression, IntFragmentExpression, NumberFragmentExpression, StringDoubleFragmentExpression, DoubleFragmentExpression, TypeSafeStringFragmentExpression, StringFragmentExpression, LocalDateFragmentExpression, DateFragmentExpression, LocalTimeFragmentExpression, TimeFragmentExpression, LocalDateTimeFragmentExpression, DateTimeFragmentExpression, EqualableFragmentExpression, ComparableFragmentExpression, FragmentBuilder1TypeSafe, FragmentBuilder0, FragmentBuilder1TypeUnsafe, FragmentBuilder2TypeSafe, FragmentBuilder2TypeUnsafe, FragmentBuilder3TypeSafe, FragmentBuilder3TypeUnsafe, FragmentBuilder4TypeSafe, FragmentBuilder4TypeUnsafe, FragmentBuilder5TypeSafe, FragmentBuilder5TypeUnsafe, FragmentBuilder0IfValue, FragmentBuilder1IfValueTypeSafe, FragmentBuilder1IfValueTypeUnsafe, FragmentBuilder2IfValueTypeSafe, FragmentBuilder2IfValueTypeUnsafe, FragmentBuilder3IfValueTypeSafe, FragmentBuilder3IfValueTypeUnsafe, FragmentBuilder4IfValueTypeSafe, FragmentBuilder4IfValueTypeUnsafe, FragmentBuilder5IfValueTypeSafe, FragmentBuilder5IfValueTypeUnsafe, BigintFragmentExpression, TypeSafeBigintFragmentExpression, TypeSafeUuidFragmentExpression, UuidFragmentExpression, CustomIntFragmentExpression, CustomDoubleFragmentExpression, CustomUuidFragmentExpression, CustomLocalDateFragmentExpression, CustomLocalTimeFragmentExpression, CustomLocalDateTimeFragmentExpression, FragmentBuilderMaybeOptional0, FragmentBuilderMaybeOptional1TypeSafe, FragmentBuilderMaybeOptional1TypeUnsafe, FragmentBuilderMaybeOptional2TypeSafe, FragmentBuilderMaybeOptional2TypeUnsafe, FragmentBuilderMaybeOptional3TypeSafe, FragmentBuilderMaybeOptional3TypeUnsafe, FragmentBuilderMaybeOptional4TypeSafe, FragmentBuilderMaybeOptional4TypeUnsafe, FragmentBuilderMaybeOptional5TypeSafe, FragmentBuilderMaybeOptional5TypeUnsafe } from "../expressions/fragment"
1414
import type { AnyDB, TypeSafeDB, TypeUnsafeDB } from "../databases"
1515
import { InsertQueryBuilder } from "../queryBuilders/InsertQueryBuilder"
1616
import { UpdateQueryBuilder } from "../queryBuilders/UpdateQueryBuilder"
@@ -20,7 +20,7 @@ import { SqlOperationStatic0ValueSource, SqlOperationStatic1ValueSource, Aggrega
2020
import { DefaultImpl } from "../expressions/Default"
2121
import { SelectQueryBuilder } from "../queryBuilders/SelectQueryBuilder"
2222
import ChainedError from "chained-error"
23-
import { FragmentQueryBuilder, FragmentFunctionBuilder, FragmentFunctionBuilderIfValue } from "../queryBuilders/FragmentQueryBuilder"
23+
import { FragmentQueryBuilder, FragmentFunctionBuilder, FragmentFunctionBuilderIfValue, FragmentFunctionBuilderMaybeOptional } from "../queryBuilders/FragmentQueryBuilder"
2424
import { attachSource, attachTransactionSource } from "../utils/attachSource"
2525
import { database, outerJoinAlias, outerJoinTableOrView, tableOrView, tableOrViewRef, type, valueSourceTypeName, valueType } from "../utils/symbols"
2626
import { callDeferredFunctions, callDeferredFunctionsStoppingOnError, isPromise, UnwrapPromiseTuple } from "../utils/PromiseProvider"
@@ -907,6 +907,21 @@ export abstract class AbstractConnection<DB extends AnyDB> implements IConnectio
907907
return new FragmentFunctionBuilderIfValue(this as any, args) // make this protected fields as public
908908
}
909909

910+
protected buildFragmentWithMaybeOptionalArgs(): FragmentBuilderMaybeOptional0<DB>
911+
protected buildFragmentWithMaybeOptionalArgs<A1 extends Argument<any, any, any, any>>(this: IConnection<TypeSafeDB>, a1: A1): FragmentBuilderMaybeOptional1TypeSafe<DB, A1>
912+
protected buildFragmentWithMaybeOptionalArgs<A1 extends Argument<any, any, any, any>>(this: IConnection<TypeUnsafeDB>, a1: A1): FragmentBuilderMaybeOptional1TypeUnsafe<DB, A1>
913+
protected buildFragmentWithMaybeOptionalArgs<A1 extends Argument<any, any, any, any>, A2 extends Argument<any, any, any, any>>(this: IConnection<TypeSafeDB>, a1: A1, a2: A2): FragmentBuilderMaybeOptional2TypeSafe<DB, A1, A2>
914+
protected buildFragmentWithMaybeOptionalArgs<A1 extends Argument<any, any, any, any>, A2 extends Argument<any, any, any, any>>(this: IConnection<TypeUnsafeDB>, a1: A1, a2: A2): FragmentBuilderMaybeOptional2TypeUnsafe<DB, A1, A2>
915+
protected buildFragmentWithMaybeOptionalArgs<A1 extends Argument<any, any, any, any>, A2 extends Argument<any, any, any, any>, A3 extends Argument<any, any, any, any>>(this: IConnection<TypeSafeDB>, a1: A1, a2: A2, a3: A3): FragmentBuilderMaybeOptional3TypeSafe<DB, A1, A2, A3>
916+
protected buildFragmentWithMaybeOptionalArgs<A1 extends Argument<any, any, any, any>, A2 extends Argument<any, any, any, any>, A3 extends Argument<any, any, any, any>>(this: IConnection<TypeUnsafeDB>, a1: A1, a2: A2, a3: A3): FragmentBuilderMaybeOptional3TypeUnsafe<DB, A1, A2, A3>
917+
protected buildFragmentWithMaybeOptionalArgs<A1 extends Argument<any, any, any, any>, A2 extends Argument<any, any, any, any>, A3 extends Argument<any, any, any, any>, A4 extends Argument<any, any, any, any>>(this: IConnection<TypeSafeDB>, a1: A1, a2: A2, a3: A3, a4: A4): FragmentBuilderMaybeOptional4TypeSafe<DB, A1, A2, A3, A4>
918+
protected buildFragmentWithMaybeOptionalArgs<A1 extends Argument<any, any, any, any>, A2 extends Argument<any, any, any, any>, A3 extends Argument<any, any, any, any>, A4 extends Argument<any, any, any, any>>(this: IConnection<TypeUnsafeDB>, a1: A1, a2: A2, a3: A3, a4: A4): FragmentBuilderMaybeOptional4TypeUnsafe<DB, A1, A2, A3, A4>
919+
protected buildFragmentWithMaybeOptionalArgs<A1 extends Argument<any, any, any, any>, A2 extends Argument<any, any, any, any>, A3 extends Argument<any, any, any, any>, A4 extends Argument<any, any, any, any>, A5 extends Argument<any, any, any, any>>(this: IConnection<TypeSafeDB>, a1: A1, a2: A2, a3: A3, a4: A4, a5: A5): FragmentBuilderMaybeOptional5TypeSafe<DB, A1, A2, A3, A4, A5>
920+
protected buildFragmentWithMaybeOptionalArgs<A1 extends Argument<any, any, any, any>, A2 extends Argument<any, any, any, any>, A3 extends Argument<any, any, any, any>, A4 extends Argument<any, any, any, any>, A5 extends Argument<any, any, any, any>>(this: IConnection<TypeUnsafeDB>, a1: A1, a2: A2, a3: A3, a4: A4, a5: A5): FragmentBuilderMaybeOptional5TypeUnsafe<DB, A1, A2, A3, A4, A5>
921+
protected buildFragmentWithMaybeOptionalArgs(...args: Argument<any, any, any, any>[]): any {
922+
return new FragmentFunctionBuilderMaybeOptional(this as any, args)
923+
}
924+
910925
rawFragment(sql: TemplateStringsArray, ...params: Array<ValueSourceOfDB<DB> | IExecutableSelectQuery<DB, any, any, any> | IExecutableInsertQuery<any, any> | IExecutableUpdateQuery<any, any> | IExecutableDeleteQuery<any, any>>): RawFragment<DB> {
911926
return new RawFragmentImpl(sql, params)
912927
}

src/examples/documentation/MariaDB-modern.ts

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,15 @@ class DBConnection extends MariaDBConnection<'DBConnection'> {
2222
return this.fragmentWithType('int', 'required').sql`${left} << ${right}`
2323
})
2424

25+
bitwiseShiftLeft2 = this.buildFragmentWithMaybeOptionalArgs(
26+
this.arg('int', 'optional'),
27+
this.arg('int', 'optional')
28+
).as((left, right) => {
29+
// The fragment here is: ${left} << ${right}
30+
// Could be another fragment like a function call: myFunction(${left}, ${right})
31+
return this.fragmentWithType('int', 'optional').sql`${left} << ${right}`
32+
})
33+
2534
valuePlusOneEqualsIfValue = this.buildFragmentWithArgsIfValue(
2635
this.arg('int', 'required'),
2736
this.valueArg('int', 'optional')
@@ -531,6 +540,75 @@ async function main() {
531540
.executeSelectMany()
532541

533542
assertEquals(companiesUsingCustomFunctionFragment, result)
543+
544+
/* *** Preparation ************************************************************/
545+
546+
result = [{id: 1, name: 'John'}]
547+
expectedResult.push(result)
548+
expectedQuery.push(`select id as id, name as name, id << ? as idMultiplyBy2 from company where (id * ?) = (id << ?)`)
549+
expectedParams.push(`[null,2,null]`)
550+
expectedType.push(`selectManyRows`)
551+
552+
/* *** Example ****************************************************************/
553+
554+
const bitwiseMovements2 = null
555+
//const multiplier = 2
556+
const companiesUsingCustomFunctionFragment2 = await connection.selectFrom(tCompany)
557+
.where(tCompany.id.multiply(multiplier).equals(connection.bitwiseShiftLeft2(tCompany.id, bitwiseMovements2)))
558+
.select({
559+
id: tCompany.id,
560+
name: tCompany.name,
561+
idMultiplyBy2: connection.bitwiseShiftLeft2(tCompany.id, bitwiseMovements2)
562+
})
563+
.executeSelectMany()
564+
565+
assertEquals(companiesUsingCustomFunctionFragment2, result)
566+
567+
/* *** Preparation ************************************************************/
568+
569+
result = [{id: 1, name: 'John', idMultiplyBy2: 2}]
570+
expectedResult.push(result)
571+
expectedQuery.push(`select id as id, name as name, id << ? as idMultiplyBy2 from company where (id * ?) = (id << ?)`)
572+
expectedParams.push(`[1,2,1]`)
573+
expectedType.push(`selectManyRows`)
574+
575+
/* *** Example ****************************************************************/
576+
577+
const bitwiseMovements3 = 1
578+
//const multiplier = 2
579+
const companiesUsingCustomFunctionFragment3 = await connection.selectFrom(tCompany)
580+
.where(tCompany.id.multiply(multiplier).equals(connection.bitwiseShiftLeft2(tCompany.id, bitwiseMovements3)))
581+
.select({
582+
id: tCompany.id,
583+
name: tCompany.name,
584+
idMultiplyBy2: connection.bitwiseShiftLeft2(tCompany.id, bitwiseMovements3)
585+
})
586+
.executeSelectMany()
587+
588+
assertEquals(companiesUsingCustomFunctionFragment3, result)
589+
590+
/* *** Preparation ************************************************************/
591+
592+
result = [{id: 1, name: 'John'}]
593+
expectedResult.push(result)
594+
expectedQuery.push(`select id as id, name as name, id << ? as idMultiplyBy2 from company where (id * ?) = (id << ?)`)
595+
expectedParams.push(`[1,2,1]`)
596+
expectedType.push(`selectManyRows`)
597+
598+
/* *** Example ****************************************************************/
599+
600+
const bitwiseMovements4 = 1
601+
//const multiplier = 2
602+
const companiesUsingCustomFunctionFragment4 = await connection.selectFrom(tCompany)
603+
.where(tCompany.id.multiply(multiplier).equals(connection.bitwiseShiftLeft2(tCompany.id, bitwiseMovements4)))
604+
.select({
605+
id: tCompany.id,
606+
name: tCompany.name,
607+
idMultiplyBy2: connection.bitwiseShiftLeft2(tCompany.id.asOptional(), bitwiseMovements4)
608+
})
609+
.executeSelectMany()
610+
611+
assertEquals(companiesUsingCustomFunctionFragment4, result)
534612

535613
/* *** Preparation ************************************************************/
536614

0 commit comments

Comments
 (0)