143143 qkInsertReturning
144144 QueryBuilder = ref object
145145 head, fromm, join, values, where, groupby, having, orderby: string
146- limit, offset, returning: string
146+ limit, offset, returning, onConflict, onConflictWhere : string
147147 env: Env
148148 ctes: seq [CteDef ]
149149 cteBase: int
158158 insertedValues: seq [(string , NimNode )]
159159 # For SQLite: expression to return instead of last_insert_rowid()
160160 retExpr: NimNode
161+ onConflictTargetSet, onConflictActionSet, onConflictIsDoUpdate, onConflictWhereSet: bool
161162
162163# Execute a non-row SQL statement strictly (errors on failure)
163164template execNoRowsStrict * (sqlStmt: string ) =
@@ -183,12 +184,14 @@ template execNoRowsLoose(sqlStmt: string) =
183184proc newQueryBuilder (): QueryBuilder {.compileTime .} =
184185 QueryBuilder (head: " " , fromm: " " , join: " " , values: " " , where: " " ,
185186 groupby: " " , having: " " , orderby: " " , limit: " " , offset: " " ,
186- returning: " " ,
187+ returning: " " , onConflict: " " , onConflictWhere: " " ,
187188 env: @ [], ctes: @ [], cteBase: 0 , kind: qkNone, params: @ [],
188189 retType: newNimNode (nnkTupleTy), singleRow: false ,
189190 retTypeIsJson: false , retNames: @ [],
190191 coln: 0 , qmark: 0 , aliasGen: 1 , colAliases: @ [],
191- insertedValues: @ [], retExpr: newEmptyNode ())
192+ insertedValues: @ [], retExpr: newEmptyNode (),
193+ onConflictTargetSet: false , onConflictActionSet: false ,
194+ onConflictIsDoUpdate: false , onConflictWhereSet: false )
192195
193196proc getAlias (q: QueryBuilder ; tabIndex: int ): string =
194197 result = tableNames[tabIndex][0 ] & $ q.aliasGen
@@ -361,7 +364,8 @@ proc isQueryClause(name: string): bool {.compileTime.} =
361364 of " with" , " select" , " distinct" , " insert" , " update" , " replace" , " delete" ,
362365 " where" , " join" , " innerjoin" , " outerjoin" , " leftjoin" , " leftouterjoin" ,
363366 " rightjoin" , " rightouterjoin" , " fulljoin" , " fullouterjoin" , " crossjoin" ,
364- " groupby" , " orderby" , " having" , " limit" , " offset" , " returning" , " produce" :
367+ " groupby" , " orderby" , " having" , " limit" , " offset" , " returning" , " produce" ,
368+ " onconflict" , " donothing" , " doupdate" :
365369 result = true
366370 else :
367371 result = false
@@ -1019,8 +1023,14 @@ proc tableSel(n: NimNode; q: QueryBuilder) =
10191023
10201024
10211025proc queryh (n: NimNode ; q: QueryBuilder ) =
1026+ var n = n
1027+ if n.kind == nnkCall:
1028+ let c = newNimNode (nnkCommand)
1029+ for i in 0 ..< n.len:
1030+ c.add n[i]
1031+ n = c
10221032 expectKind n, nnkCommand
1023- let kind = nodeName (n[0 ])
1033+ let kind = nodeName (n[0 ]). toLowerAscii ()
10241034 case kind
10251035 of " with" :
10261036 expectLen n, 2
@@ -1090,20 +1100,41 @@ proc queryh(n: NimNode; q: QueryBuilder) =
10901100 tableSel (n[1 ], q)
10911101 of " where" :
10921102 expectLen n, 2
1093- let t = cond (n[1 ], q.where, q.params, DbType (kind: dbBool), q)
1094- checkBool (t, n)
1103+ if q.kind in {qkInsert, qkInsertReturning}:
1104+ if not q.onConflictTargetSet or not q.onConflictActionSet or not q.onConflictIsDoUpdate:
1105+ macros.error " 'where' for insert is only supported after 'onconflict(...)' and 'doupdate(...)'" , n
1106+ if q.onConflictWhereSet:
1107+ macros.error " conflict update 'where' can only be specified once" , n
1108+ var conflictWhere = " "
1109+ # In PostgreSQL upsert WHERE, bare column names are ambiguous between
1110+ # target table and EXCLUDED. Resolve bare identifiers against target table.
1111+ let oldKind = q.kind
1112+ let oldEnv = q.env
1113+ if q.env.len > 0 :
1114+ let source = q.env[^ 1 ][0 ]
1115+ q.kind = qkSelect
1116+ q.env = @ [(source, sourceName (q, source))]
1117+ let t = cond (n[1 ], conflictWhere, q.params, DbType (kind: dbBool), q)
1118+ q.kind = oldKind
1119+ q.env = oldEnv
1120+ checkBool (t, n)
1121+ q.onConflictWhere = " where " & conflictWhere
1122+ q.onConflictWhereSet = true
1123+ else :
1124+ let t = cond (n[1 ], q.where, q.params, DbType (kind: dbBool), q)
1125+ checkBool (t, n)
10951126 of " join" , " innerjoin" , " outerjoin" , " leftjoin" , " leftouterjoin" ,
10961127 " rightjoin" , " rightouterjoin" , " fulljoin" , " fullouterjoin" , " crossjoin" :
10971128 q.join.add " \L " & joinKeyword (kind)
10981129 expectLen n, 2
1099- let cmd = n[1 ]
1100- if kind == " crossjoin" and cmd .kind == nnkCommand and cmd .len == 2 and
1101- cmd [1 ].kind == nnkCommand and cmd [1 ].len == 2 and $ cmd [1 ][0 ] == " on" :
1130+ let joinClause = n[1 ]
1131+ if kind == " crossjoin" and joinClause .kind == nnkCommand and joinClause .len == 2 and
1132+ joinClause [1 ].kind == nnkCommand and joinClause [1 ].len == 2 and $ joinClause [1 ][0 ] == " on" :
11021133 macros.error " crossjoin does not support an on clause" , n
1103- if cmd .kind == nnkCommand and cmd .len == 2 and
1104- cmd [1 ].kind == nnkCommand and cmd [1 ].len == 2 and $ cmd [1 ][0 ] == " on" and
1105- cmd [0 ].kind == nnkCall:
1106- let tab = $ cmd [0 ][0 ]
1134+ if joinClause .kind == nnkCommand and joinClause .len == 2 and
1135+ joinClause [1 ].kind == nnkCommand and joinClause [1 ].len == 2 and $ joinClause [1 ][0 ] == " on" and
1136+ joinClause [0 ].kind == nnkCall:
1137+ let tab = $ joinClause [0 ][0 ]
11071138 let tabIndex = sourceLookup (q, tab)
11081139 if tabIndex < 0 :
11091140 macros.error " unknown table name: " & tab & " from: " & fmtTableList (tableNames), n
@@ -1114,17 +1145,17 @@ proc queryh(n: NimNode; q: QueryBuilder) =
11141145 var oldEnv = q.env
11151146 q.env = @ [(tabIndex, alias)]
11161147 q.kind = qkJoin
1117- tableSel (cmd [0 ], q)
1148+ tableSel (joinClause [0 ], q)
11181149 swap q.env, oldEnv
1119- let onn = cmd [1 ][1 ]
1150+ let onn = joinClause [1 ][1 ]
11201151 q.join.add " on "
11211152 oldEnv = q.env
11221153 q.env.add ((tabIndex, alias))
11231154 let t = cond (onn, q.join, q.params, DbType (kind: dbBool), q)
11241155 swap q.env, oldEnv
11251156 checkBool (t, onn)
1126- elif cmd .kind == nnkCall:
1127- let tab = $ cmd [0 ]
1157+ elif joinClause .kind == nnkCall:
1158+ let tab = $ joinClause [0 ]
11281159 let tabIndex = sourceLookup (q, tab)
11291160 if tabIndex < 0 :
11301161 macros.error " unknown table name: " & tab & " from: " & fmtTableList (tableNames), n[1 ][0 ]
@@ -1175,6 +1206,64 @@ proc queryh(n: NimNode; q: QueryBuilder) =
11751206 expectLen n, 2
11761207 let t = cond (n[1 ], q.offset, q.params, DbType (kind: dbInt), q)
11771208 checkInt (t, n[1 ])
1209+ of " onconflict" :
1210+ if q.kind notin {qkInsert, qkInsertReturning}:
1211+ macros.error " 'onconflict' only possible within 'insert'" , n
1212+ if q.onConflictTargetSet:
1213+ macros.error " 'onconflict' can only be specified once" , n
1214+ if n.len < 2 :
1215+ macros.error " 'onconflict' expects one or more columns" , n
1216+ q.onConflict = " \L on conflict ("
1217+ for i in 1 ..< n.len:
1218+ let col = n[i]
1219+ let colname = nodeName (col)
1220+ if colname.len == 0 :
1221+ macros.error " 'onconflict' columns must be identifiers" , col
1222+ if lookup (" " , colname, q).kind == dbUnknown:
1223+ macros.error " unknown column name: " & colname, col
1224+ if i > 1 :
1225+ q.onConflict.add " , "
1226+ escIdent (q.onConflict, colname)
1227+ q.onConflict.add " )"
1228+ q.onConflictTargetSet = true
1229+ of " donothing" :
1230+ if q.kind notin {qkInsert, qkInsertReturning}:
1231+ macros.error " 'donothing' only possible within 'insert'" , n
1232+ if not q.onConflictTargetSet:
1233+ macros.error " 'donothing' requires a preceding 'onconflict' clause" , n
1234+ if q.onConflictActionSet:
1235+ macros.error " conflict action already set; choose only one of 'donothing' or 'doupdate'" , n
1236+ expectLen n, 1
1237+ q.onConflict.add " do nothing"
1238+ q.onConflictActionSet = true
1239+ q.onConflictIsDoUpdate = false
1240+ of " doupdate" :
1241+ if q.kind notin {qkInsert, qkInsertReturning}:
1242+ macros.error " 'doupdate' only possible within 'insert'" , n
1243+ if not q.onConflictTargetSet:
1244+ macros.error " 'doupdate' requires a preceding 'onconflict' clause" , n
1245+ if q.onConflictActionSet:
1246+ macros.error " conflict action already set; choose only one of 'donothing' or 'doupdate'" , n
1247+ if n.len < 2 :
1248+ macros.error " 'doupdate' expects assignments like doupdate(col = value)" , n
1249+ q.onConflict.add " do update set "
1250+ for i in 1 ..< n.len:
1251+ let assignment = n[i]
1252+ if assignment.kind != nnkExprEqExpr:
1253+ macros.error " 'doupdate' expects assignments like doupdate(col = value)" , assignment
1254+ let colname = nodeName (assignment[0 ])
1255+ if colname.len == 0 :
1256+ macros.error " 'doupdate' assignments must target a column identifier" , assignment[0 ]
1257+ let coltype = lookup (" " , colname, q)
1258+ if coltype.kind == dbUnknown:
1259+ macros.error " unknown column name: " & colname, assignment[0 ]
1260+ if i > 1 :
1261+ q.onConflict.add " , "
1262+ escIdent (q.onConflict, colname)
1263+ q.onConflict.add " = "
1264+ discard cond (assignment[1 ], q.onConflict, q.params, coltype, q)
1265+ q.onConflictActionSet = true
1266+ q.onConflictIsDoUpdate = true
11781267 of " returning" :
11791268 if q.kind != qkInsert:
11801269 macros.error " 'returning' only possible within 'insert'"
@@ -1217,6 +1306,10 @@ proc queryh(n: NimNode; q: QueryBuilder) =
12171306 macros.error " unknown query component " & repr (n), n
12181307
12191308proc queryAsString (q: QueryBuilder , n: NimNode ): string =
1309+ if q.onConflictTargetSet and not q.onConflictActionSet:
1310+ macros.error " 'onconflict' requires either 'donothing' or 'doupdate'" , n
1311+ if q.onConflictWhereSet and not q.onConflictIsDoUpdate:
1312+ macros.error " conflict update 'where' requires 'doupdate(...)'" , n
12201313 if q.cteBase < q.ctes.len:
12211314 result .add " with "
12221315 for i in q.cteBase..< q.ctes.len:
@@ -1237,9 +1330,16 @@ proc queryAsString(q: QueryBuilder, n: NimNode): string =
12371330 result .add " \L values ("
12381331 result .add q.values
12391332 result .add " )"
1333+ if q.onConflict.len > 0 :
1334+ result .add q.onConflict
1335+ if q.onConflictWhere.len > 0 :
1336+ result .add q.onConflictWhere
12401337 if q.where.len > 0 :
1241- result .add " \L where "
1242- result .add q.where
1338+ if q.kind in {qkSelect, qkJoin, qkUpdate, qkDelete}:
1339+ result .add " \L where "
1340+ result .add q.where
1341+ else :
1342+ macros.error " 'where' is not supported for this query kind" , n
12431343 if q.groupby.len > 0 :
12441344 result .add " \L group by "
12451345 result .add q.groupby
@@ -1353,7 +1453,7 @@ proc applyQueryNode(n: NimNode; q: QueryBuilder) =
13531453 macros.error " mixed infix set operations are not supported; use nesting for precedence" , part
13541454 flushBranch (part)
13551455 else :
1356- if part.kind == nnkCommand or isSetOpCall (part):
1456+ if part.kind in { nnkCommand, nnkCall} or isSetOpCall (part):
13571457 currentBranch.add part
13581458 else :
13591459 macros.error " illformed query" , part
@@ -1368,7 +1468,7 @@ proc applyQueryNode(n: NimNode; q: QueryBuilder) =
13681468 for part in flattened:
13691469 if isSetOpCall (part):
13701470 buildSetOpQuery (part, q)
1371- elif part.kind == nnkCommand:
1471+ elif part.kind in { nnkCommand, nnkCall} :
13721472 queryh (part, q)
13731473 else :
13741474 macros.error " illformed query" , part
0 commit comments