Skip to content

Commit 42f12f2

Browse files
authored
Story - Use prepared statements with numbered parameters (#19)
* Initial commit * version bump
1 parent 6989c02 commit 42f12f2

File tree

9 files changed

+104
-49
lines changed

9 files changed

+104
-49
lines changed

package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "ts-pg-orm",
3-
"version": "5.0.0",
3+
"version": "5.0.1",
44
"description": "Typescript PostgreSQL ORM",
55
"main": "dist/index.js",
66
"types": "dist/index.d.ts",
@@ -53,8 +53,8 @@
5353
"access": "public"
5454
},
5555
"dependencies": {
56-
"@samhuk/data-filter": "^1.0.10",
57-
"@samhuk/data-query": "^1.1.7",
56+
"@samhuk/data-filter": "^1.1.1",
57+
"@samhuk/data-query": "^1.2.1",
5858
"simple-pg-client": "^1.0.5"
5959
},
6060
"devDependencies": {

src/store/count/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ export const count = async (
3333

3434
const sql = sqlParts.filter(s => s != null).join('\n')
3535

36-
const row = await db.queryGetFirstRow(sql)
36+
const row = await db.queryGetFirstRow(sql, queryInfo?.values)
3737
const countRaw = (row as any).exact_count
3838
if (countRaw == null)
3939
return null

src/store/delete/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ export const deleteBase = async (
5252

5353
const sql = sqlParts.filter(s => s != null).join('\n')
5454

55-
const result = await db.query(sql)
55+
const result = await db.query(sql, queryInfo?.values)
5656

5757
switch (returnMode) {
5858
case ReturnMode.RETURN_COUNT:

src/store/exists/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,6 @@ export const exists = async (
2525

2626
const sql = sqlParts.filter(s => s != null).join('\n')
2727

28-
const row = await db.queryGetFirstRow(sql)
28+
const row = await db.queryGetFirstRow(sql, queryInfo?.values)
2929
return (row as any).exists === true
3030
}

src/store/get/index.ts

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -18,17 +18,17 @@ const createColumnsSqlForGetWithNoRelations = (
1818
const createGetSingleWithNoRelationsSql = (
1919
options: AnyGetFunctionOptions<false>,
2020
dataFormat: DataFormat,
21-
): string => {
21+
): { sql: string, values: any[] } => {
2222
const columnsSql = createColumnsSqlForGetWithNoRelations(options, dataFormat)
2323

2424
const whereClause = options.filter != null
2525
? createDataFilter(options.filter).toSql({ transformer: node => ({ left: dataFormat.sql.cols[node.field] }) })
2626
: null
2727
const rootSelectSql = `select ${columnsSql} from ${dataFormat.sql.tableName}`
28-
if (whereClause == null)
29-
return `${rootSelectSql} limit 1`
28+
if (whereClause?.sql == null)
29+
return { sql: `${rootSelectSql} limit 1`, values: [] }
3030

31-
return `${rootSelectSql} where ${whereClause} limit 1`
31+
return { sql: `${rootSelectSql} where ${whereClause.sql} limit 1`, values: whereClause.values }
3232
}
3333

3434
const getSingleWithNoRelations = async (
@@ -38,28 +38,29 @@ const getSingleWithNoRelations = async (
3838
): Promise<any> => {
3939
const sql = createGetSingleWithNoRelationsSql(options, dataFormat)
4040

41-
const row = await db.queryGetFirstRow(sql)
41+
const row = await db.queryGetFirstRow(sql.sql, sql.values)
4242

4343
return objectPropsToCamelCase(row)
4444
}
4545

4646
const createGetMultipleWithNoRelationsSql = (
4747
options: AnyGetFunctionOptions<true>,
4848
dataFormat: DataFormat,
49-
): string => {
49+
): { sql: string, values: any[] } => {
5050
const columnsSql = createColumnsSqlForGetWithNoRelations(options, dataFormat)
5151

5252
const querySql = options.query != null
5353
? createDataQuery(options.query).toSql({
54+
includeWhereWord: true,
5455
filterTransformer: node => ({ left: dataFormat.sql.cols[node.field] }),
5556
sortingTransformer: node => ({ left: dataFormat.sql.cols[node.field] }),
5657
})
5758
: null
5859
const rootSelectSql = `select ${columnsSql} from ${dataFormat.sql.tableName}`
59-
if (querySql == null)
60-
return `${rootSelectSql} limit 1`
60+
if (querySql?.whereOrderByLimitOffset == null)
61+
return { sql: rootSelectSql, values: [] }
6162

62-
return `${rootSelectSql} ${querySql.whereOrderByLimitOffset}`
63+
return { sql: `${rootSelectSql} ${querySql.whereOrderByLimitOffset}`, values: querySql.values }
6364
}
6465

6566
const getMultipleWithNoRelations = async (
@@ -69,9 +70,9 @@ const getMultipleWithNoRelations = async (
6970
): Promise<any> => {
7071
const sql = createGetMultipleWithNoRelationsSql(options, dataFormat)
7172

72-
const rows = await db.queryGetRows(sql)
73+
const rows = await db.queryGetRows(sql.sql)
7374

74-
return rows.map(r => objectPropsToCamelCase(r))
75+
return rows.map(r => objectPropsToCamelCase(r), sql.values)
7576
}
7677

7778
export const getSingle = async (

src/store/get/queryPlan/queryNodeToSql.ts

Lines changed: 73 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { QueryNode, DataNode, QueryNodeSql } from './types'
99
type DataNodeQueryInfo = {
1010
whereClause?: string
1111
orderByLimitOffset?: string
12+
values: any[]
1213
}
1314

1415
const isQueryNodeRangeConstrained = (queryNode: QueryNode) => {
@@ -26,11 +27,14 @@ const isQueryNodeRangeConstrained = (queryNode: QueryNode) => {
2627
*
2728
* If the data node does not have any filter, this will return `null`.
2829
*/
29-
const createWhereClauseOfDataNode = (dataNode: DataNode<false>): string | null => {
30+
const createWhereClauseOfDataNode = (dataNode: DataNode<false>, parameterStartIndex: number): { sql: string | null, values: any[] } | null => {
3031
const dataFilterNodeOrGroup = dataNode.options.filter
3132
return dataFilterNodeOrGroup != null
3233
? createDataFilter(dataFilterNodeOrGroup)
33-
.toSql({ transformer: node => ({ left: dataNode.fieldsInfo.fieldToFullyQualifiedColumnName[node.field] }) })
34+
.toSql({
35+
transformer: node => ({ left: dataNode.fieldsInfo.fieldToFullyQualifiedColumnName[node.field] }),
36+
parameterStartIndex,
37+
})
3438
: null
3539
}
3640

@@ -44,48 +48,64 @@ const createWhereClauseOfDataNode = (dataNode: DataNode<false>): string | null =
4448
* If the data node has a query, but is missing certain parts of the data query, then the where
4549
* clause and/or the order-by-limit-offset statement can be null.
4650
*/
47-
const createQueryInfoOfDataNode = (dataNode: DataNode<true>): DataNodeQueryInfo | null => {
51+
const createQueryInfoOfDataNode = (dataNode: DataNode<true>, parameterStartIndex: number): DataNodeQueryInfo | null => {
4852
const dataQueryRecord = dataNode.options.query
4953
if (dataQueryRecord != null) {
5054
const dataQuerySql = createDataQuery(dataQueryRecord).toSql({
5155
sortingTransformer: node => ({ left: dataNode.fieldsInfo.fieldToFullyQualifiedColumnName[node.field] }),
5256
filterTransformer: node => ({ left: dataNode.fieldsInfo.fieldToFullyQualifiedColumnName[node.field] }),
5357
includeWhereWord: false,
58+
parameterStartIndex,
5459
})
5560
return {
5661
whereClause: dataQuerySql.where,
5762
orderByLimitOffset: dataQuerySql.orderByLimitOffset,
63+
values: dataQuerySql.values,
5864
}
5965
}
6066
return null
6167
}
6268

63-
const createDataNodeQueryInfo = (dataNode: DataNode): DataNodeQueryInfo | null => (
64-
dataNode.isPlural
65-
? createQueryInfoOfDataNode(dataNode as DataNode<true>)
66-
: {
67-
whereClause: createWhereClauseOfDataNode(dataNode as DataNode<false>),
68-
orderByLimitOffset: 'limit 1',
69-
}
70-
)
69+
const createDataNodeQueryInfo = (dataNode: DataNode, parameterStartIndex: number): DataNodeQueryInfo | null => {
70+
if (dataNode.isPlural)
71+
return createQueryInfoOfDataNode(dataNode as DataNode<true>, parameterStartIndex)
72+
73+
const whereClause = createWhereClauseOfDataNode(dataNode as DataNode<false>, parameterStartIndex)
74+
75+
return {
76+
whereClause: whereClause?.sql,
77+
orderByLimitOffset: 'limit 1',
78+
values: whereClause?.values ?? [],
79+
}
80+
}
7181

7282
/**
7383
* Converts all of the non-root (therefore *non-plural*) data nodes within `queryNode` into a
7484
* series of new-line-separated `left join` sql statements, with each data node's filter applied
7585
* alongside each left join as a where clause (minus the actual "where" word since joins just
7686
* need an " and " separator).
7787
*/
78-
const createLeftJoinsSql = (queryNode: QueryNode) => (
79-
Object.values(queryNode.nonRootDataNodes).map(dataNode => {
88+
const createLeftJoinsSql = (queryNode: QueryNode, parameterStartIndex: number): { sql: string, values: any[] } => {
89+
const values: any[] = []
90+
let _parameterStartIndex = parameterStartIndex
91+
92+
const sql = Object.values(queryNode.nonRootDataNodes).map(dataNode => {
8093
const linkedColumnName = dataNode.dataFormat.sql.cols[dataNode.fieldRef.field]
8194
const parentLinkedColumnName = dataNode.parent.dataFormat.sql.cols[dataNode.parentFieldRef.field]
82-
const whereClause = createWhereClauseOfDataNode(dataNode)
95+
const whereClause = createWhereClauseOfDataNode(dataNode, _parameterStartIndex)
96+
if (whereClause != null) {
97+
_parameterStartIndex += whereClause.values.length
98+
values.push(...whereClause.values)
99+
}
83100
// E.g. left join "userImage" "1" on "1"."user_id" = "0"."id" and "1"."date_deleted" is not null\n
84101
return filterForNotNullAndEmpty([
85102
`left join ${dataNode.dataFormat.sql.tableName} ${dataNode.tableAlias} on ${dataNode.tableAlias}.${linkedColumnName} = ${dataNode.parent.tableAlias}.${parentLinkedColumnName}`,
86-
whereClause,
103+
whereClause?.sql,
87104
]).join(' and ')
88-
}).join('\n'))
105+
}).join('\n')
106+
107+
return { sql, values }
108+
}
89109

90110
const createLinkedFieldWhereClause = (
91111
queryNode: QueryNode,
@@ -141,6 +161,8 @@ const createLinkedFieldWhereClause = (
141161
export const toSql = (queryNode: QueryNode, linkedFieldValues: any[]) => {
142162
const isManyToMany = queryNode.rootDataNode.relation?.type === RelationType.MANY_TO_MANY
143163
const isRangeConstrained = isQueryNodeRangeConstrained(queryNode) && linkedFieldValues?.length > 0
164+
const values: any[] = []
165+
let parameterStartIndex = 1
144166

145167
const sqlParts: string[] = ['select']
146168

@@ -272,12 +294,17 @@ export const toSql = (queryNode: QueryNode, linkedFieldValues: any[]) => {
272294
}
273295

274296
// -- Root data node query SQL
275-
const rootDataNodeQueryInfo = createDataNodeQueryInfo(rootDataNode)
297+
const rootDataNodeQueryInfo = createDataNodeQueryInfo(rootDataNode, parameterStartIndex)
276298
if (rootDataNodeQueryInfo?.whereClause != null)
277299
sqlParts.push(`and ${rootDataNodeQueryInfo.whereClause}`)
278300
if (rootDataNodeQueryInfo?.orderByLimitOffset != null)
279301
sqlParts.push(rootDataNodeQueryInfo.orderByLimitOffset)
280302

303+
if (rootDataNodeQueryInfo != null) {
304+
parameterStartIndex += rootDataNodeQueryInfo.values.length
305+
values.push(...rootDataNodeQueryInfo.values)
306+
}
307+
281308
// -- As ... sql
282309
let asSql: string
283310
if (isManyToMany)
@@ -291,9 +318,12 @@ export const toSql = (queryNode: QueryNode, linkedFieldValues: any[]) => {
291318
// ----------------------------------------------------------------------------------
292319
// -- To-one related data left joins
293320
// ----------------------------------------------------------------------------------
294-
const leftJoinsSql = createLeftJoinsSql(queryNode)
295-
if (leftJoinsSql != null && leftJoinsSql.length > 0)
296-
sqlParts.push(leftJoinsSql)
321+
const leftJoinsSql = createLeftJoinsSql(queryNode, parameterStartIndex)
322+
if (leftJoinsSql != null && leftJoinsSql.sql.length > 0)
323+
sqlParts.push(leftJoinsSql.sql)
324+
325+
parameterStartIndex += leftJoinsSql.values.length
326+
values.push(...leftJoinsSql.values)
297327

298328
// ----------------------------------------------------------------------------------
299329
// -- Linked field values where clause and (possibly) root data node query
@@ -304,7 +334,7 @@ export const toSql = (queryNode: QueryNode, linkedFieldValues: any[]) => {
304334
sqlParts.push(`where ${linkedFieldValuesWhereClause}`)
305335
}
306336
else {
307-
const rootDataNodeQueryInfo = createDataNodeQueryInfo(rootDataNode)
337+
const rootDataNodeQueryInfo = createDataNodeQueryInfo(rootDataNode, parameterStartIndex)
308338
const rootDataNodeTotalWhereClauseSegments = [linkedFieldValuesWhereClause, rootDataNodeQueryInfo?.whereClause].filter(s => s != null)
309339
if (rootDataNodeTotalWhereClauseSegments.length > 0) {
310340
const rootDataNodeTotalWhereClause = rootDataNodeTotalWhereClauseSegments.join(' and ')
@@ -313,9 +343,14 @@ export const toSql = (queryNode: QueryNode, linkedFieldValues: any[]) => {
313343

314344
if (rootDataNodeQueryInfo?.orderByLimitOffset != null)
315345
sqlParts.push(rootDataNodeQueryInfo.orderByLimitOffset)
346+
347+
if (rootDataNodeQueryInfo != null) {
348+
parameterStartIndex += rootDataNodeQueryInfo.values.length
349+
values.push(...rootDataNodeQueryInfo.values)
350+
}
316351
}
317352

318-
return sqlParts.join('\n')
353+
return { sql: sqlParts.join('\n'), values }
319354
}
320355

321356
export const queryNodeToSql = <TIsPlural extends boolean = boolean>(
@@ -325,27 +360,39 @@ export const queryNodeToSql = <TIsPlural extends boolean = boolean>(
325360
let queryNodeSql: QueryNodeSql
326361
let currentLinkedFieldValues = linkedFieldValues
327362

363+
const initialSql = toSql(queryNode, currentLinkedFieldValues)
364+
365+
const updateSql = () => {
366+
const newSql = toSql(queryNode, currentLinkedFieldValues)
367+
queryNodeSql.sql = newSql.sql
368+
queryNodeSql.values = newSql.values
369+
}
370+
328371
return queryNodeSql = {
329-
sql: toSql(queryNode, currentLinkedFieldValues),
372+
sql: initialSql.sql,
373+
values: initialSql.values,
330374
updateLinkedFieldValues: (newLinkedFieldValues: any[] | null) => {
331375
currentLinkedFieldValues = newLinkedFieldValues
332-
queryNodeSql.sql = toSql(queryNode, currentLinkedFieldValues)
376+
377+
updateSql()
333378
},
334379
modifyRootDataNodeDataFilter: newDataFilter => {
335380
if (queryNode.rootDataNode.isPlural)
336381
throw new Error('Cannot update root data node data filter, it is plural. Try calling modifyRootDataNodeDataQuery().')
337382

338383
const _nonPluralRootDataNode = queryNode.rootDataNode as DataNode<false>
339384
_nonPluralRootDataNode.options.filter = newDataFilter
340-
queryNodeSql.sql = toSql(queryNode, currentLinkedFieldValues)
385+
386+
updateSql()
341387
},
342388
modifyRootDataNodeDataQuery: newDataQuery => {
343389
if (!queryNode.rootDataNode.isPlural)
344390
throw new Error('Cannot update root data node data query, it is non-plural. Try calling modifyRootDataNodeDataFilter().')
345391

346392
const _pluralRootDataNode = queryNode.rootDataNode as DataNode<true>
347393
_pluralRootDataNode.options.query = newDataQuery
348-
queryNodeSql.sql = toSql(queryNode, currentLinkedFieldValues)
394+
395+
updateSql()
349396
},
350397
}
351398
}

src/store/get/queryPlan/queryPlan.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,15 @@ const createQueryNodeSql = (
1313
queryNode: QueryNode,
1414
linkedFieldValues: any[] | null,
1515
queryNodeSqlDict: { [queryNodeId: number]: QueryNodeSql },
16-
): string => {
16+
): { sql: string, values: any[] } => {
1717
/* Try and find any pre-existing QueryNodeSql for the current node.
1818
* If it exists, update the linked field values-dependant part of it
1919
* and then return the SQL text.
2020
*/
2121
const preExistingQueryNodeSql = queryNodeSqlDict[queryNode.id]
2222
if (preExistingQueryNodeSql != null) {
2323
preExistingQueryNodeSql.updateLinkedFieldValues(linkedFieldValues)
24-
return preExistingQueryNodeSql.sql
24+
return { sql: preExistingQueryNodeSql.sql, values: [] } // TODO This is not used anymore, for now.
2525
}
2626

2727
/* Else (if pre-existing QueryNodeSql does not exist), then convert
@@ -30,7 +30,7 @@ const createQueryNodeSql = (
3030
*/
3131
const queryNodeSqlObj = queryNode.toSql(linkedFieldValues)
3232
queryNodeSqlDict[queryNode.id] = queryNodeSqlObj
33-
return queryNodeSqlObj.sql
33+
return { sql: queryNodeSqlObj.sql, values: queryNodeSqlObj.values }
3434
}
3535

3636
/**
@@ -71,7 +71,7 @@ const executeQueryNode = async (
7171
*/
7272
const queryNodeSql = createQueryNodeSql(queryNode, linkedFieldValues, queryNodeSqlDict)
7373
// Execute sql with db service
74-
const _rows: any[] = await db.queryGetRows(queryNodeSql)
74+
const _rows: any[] = await db.queryGetRows(queryNodeSql.sql, queryNodeSql.values)
7575
const rows = _rows ?? []
7676
// Store the results in the state object
7777
results[queryNode.id] = rows

src/store/get/queryPlan/types.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,10 @@ export type QueryNodeSql<
177177
* SQL query text
178178
*/
179179
sql: string
180+
/**
181+
* The values for any numbered parameters (as part of a prepared statement)
182+
*/
183+
values: any[]
180184
/**
181185
* Updates the linked field values-dependant part of the SQL query text.
182186
*/

0 commit comments

Comments
 (0)