0 | module Oracle.Query
  1 |
  2 | import JSON
  3 | import Oracle.Statement
  4 | import Oracle.Internal.Either
  5 | import Oracle.Internal.JSONQuery
  6 | import Oracle.Internal.Pointer
  7 | import Oracle.Internal.Query
  8 | import Oracle.Types.BindParameter
  9 | import Oracle.Types.Error
 10 | import Oracle.Types.JSONQuery
 11 | import Oracle.Types.Query
 12 | import Oracle.Types.Row
 13 | import Oracle.Types.Value
 14 |
 15 | %default total
 16 |
 17 | --------------------------------------------------------------------------------
 18 | --          Decode Rows
 19 | --------------------------------------------------------------------------------
 20 |
 21 | ||| Decode a list of raw Oracle rows into a list of typed values.
 22 | |||
 23 | ||| Each raw row returned from Oracle is passed through the `FromRow` instance for the requested type.
 24 | |||
 25 | ||| Decoding proceeds from the first row to the last row.
 26 | |||
 27 | ||| If any row fails to decode, decoding stops immediately and the first `OracleError` is returned.
 28 | |||
 29 | ||| This function is used internally by `query_`, but is exported so callers can decode results obtained from `queryRaw` manually.
 30 | |||
 31 | ||| Example:
 32 | |||
 33 | ||| ```idris
 34 | ||| decodeRows
 35 | |||   [ [OracleInt 1, OracleString "Alice"]
 36 | |||   , [OracleInt 2, OracleString "Bob"]
 37 | |||   ]
 38 | ||| ```
 39 | |||
 40 | export
 41 | decodeRows : FromRow a => List (List OracleValue) -> Either OracleError (List a)
 42 | decodeRows rows =
 43 |   go rows []
 44 |   where
 45 |     go : List (List OracleValue) -> List a -> Either OracleError (List a)
 46 |     go []            acc =
 47 |       Right (reverse acc)
 48 |     go (row :: rest) acc =
 49 |       case fromRow row of
 50 |         Left err =>
 51 |           Left err
 52 |         Right value =>
 53 |           go rest (value :: acc)
 54 |
 55 | --------------------------------------------------------------------------------
 56 | --          Untyped Query
 57 | --------------------------------------------------------------------------------
 58 |
 59 | ||| Execute a SQL query while automatically managing the prepared statement lifetime.
 60 | |||
 61 | ||| This is the raw query API.
 62 | |||
 63 | ||| The statement is:
 64 | ||| 1. Prepared.
 65 | ||| 2. Bound with parameters.
 66 | ||| 3. Executed.
 67 | ||| 4. Fetched.
 68 | ||| 5. Released.
 69 | |||
 70 | ||| Returned rows contain raw Oracle values.
 71 | |||
 72 | export covering
 73 | queryRaw : Connection -> String -> List BindParameter -> IO (Either OracleError (List (List OracleValue)))
 74 | queryRaw conn sql params =
 75 |   withStatement conn sql $ \stmt => do
 76 |     bind stmt params >>== \_ =>
 77 |       execute stmt >>== \_ =>
 78 |         fetchRaw stmt
 79 |
 80 | --------------------------------------------------------------------------------
 81 | --          Typed Query
 82 | --------------------------------------------------------------------------------
 83 |
 84 | ||| Execute a query and decode every returned row.
 85 | |||
 86 | ||| The target type must provide a `FromRow` implementation describing how to convert `List OracleValue` into the target value.
 87 | |||
 88 | ||| Example:
 89 | |||
 90 | ||| ```idris
 91 | ||| record Employee where
 92 | |||   constructor MkEmployee
 93 | |||   id   : Int64
 94 | |||   name : String
 95 | |||
 96 | ||| implementation FromRow Employee where
 97 | |||   fromRow [OracleInt id, OracleString name] =
 98 | |||       Right (MkEmployee id name)
 99 | |||   fromRow _ =
100 | |||       Left invalidRow
101 | |||
102 | ||| employees <- query_ conn
103 | |||                    "select id,name from employees"
104 | |||                    []
105 | ||| ```
106 | |||
107 | export covering
108 | query_ : FromRow a => Connection -> String -> List BindParameter -> IO (Either OracleError (List a))
109 | query_ conn sql params = do
110 |   rows <- queryRaw conn sql params
111 |   case rows of
112 |     Left err     =>
113 |       pure (Left err)
114 |     Right values =>
115 |       pure (decodeRows values)
116 |
117 | --------------------------------------------------------------------------------
118 | --          Single Row Query
119 | --------------------------------------------------------------------------------
120 |
121 | ||| Execute a query and decode at most a single row.
122 | |||
123 | ||| Returns:
124 | ||| - Left OracleError if execution fails.
125 | ||| - Right Nothing if no rows were returned.
126 | ||| - Right (Just value) for the first row.
127 | |||
128 | ||| If multiple rows are returned, only the first row is used and the remainder are ignored.
129 | |||
130 | ||| Example:
131 | |||
132 | ||| ```idris
133 | ||| employee <-
134 | |||   queryOne
135 | |||     conn
136 | |||     "select id,name
137 | |||        from employees
138 | |||       where id = :id"
139 | |||     [ MkBindParameter "id"
140 | |||         (OracleInt 1)
141 | |||     ]
142 | ||| ```
143 | |||
144 | export covering
145 | queryOne : FromRow a => Connection -> String -> List BindParameter -> IO (Either OracleError (Maybe a))
146 | queryOne conn sql params = do
147 |   result <- query_ conn sql params
148 |   case result of
149 |     Left err       =>
150 |       pure (Left err)
151 |     Right []       =>
152 |       pure (Right Nothing)
153 |     Right (x :: _) =>
154 |       pure (Right (Just x))
155 |
156 | ||| Execute a query and require exactly one row.
157 | |||
158 | ||| Returns:
159 | ||| - Left OracleError if query execution fails.
160 | ||| - Left OracleError if no rows are returned.
161 | ||| - Left OracleError if more than one row is returned.
162 | ||| - Right value if exactly one row is returned.
163 | |||
164 | ||| This function is useful when querying by a primary key or other unique identifier.
165 | |||
166 | ||| Example:
167 | |||
168 | ||| ```idris
169 | ||| employee <-
170 | |||   queryExactlyOne
171 | |||     conn
172 | |||     "select id,name
173 | |||        from employees
174 | |||       where id = :id"
175 | |||     [ MkBindParameter "id"
176 | |||         (OracleInt 1)
177 | |||     ]
178 | ||| ```
179 | |||
180 | export covering
181 | queryExactlyOne : FromRow a => Connection -> String -> List BindParameter -> IO (Either OracleError a)
182 | queryExactlyOne conn sql params = do
183 |   result <- query_ conn sql params
184 |   case result of
185 |     Left err            =>
186 |       pure (Left err)
187 |     Right []            =>
188 |       pure $
189 |         Left $
190 |           MkOracleError
191 |             (-1)
192 |             "Expected exactly one row but query returned no rows"
193 |             "Oracle.Query.queryExactlyOne"
194 |             False
195 |     Right [value]       =>
196 |       pure (Right value)
197 |     Right (_ :: _ :: _) =>
198 |       pure $
199 |         Left $
200 |           MkOracleError
201 |             (-1)
202 |             "Expected exactly one row but query returned multiple rows"
203 |             "Oracle.Query.queryExactlyOne"
204 |             False
205 |
206 | --------------------------------------------------------------------------------
207 | --          Structured Query
208 | --------------------------------------------------------------------------------
209 |
210 | ||| Execute a structured Query and decode every returned row.
211 | |||
212 | ||| The SQL statement is constructed using `buildQuerySQL`, which renders each `QueryColumn` in the SELECT projection.
213 | |||
214 | ||| Ordinary columns are rendered unchanged, while `JSONColumn` expressions are wrapped with `JSON_SERIALIZE(... RETURNING CLOB)`.
215 | |||
216 | ||| Each returned row is decoded using the `FromRow` implementation for the requested result type.
217 | |||
218 | ||| If the query returns no rows, this function succeeds with an empty list.
219 | |||
220 | ||| Any Oracle error encountered while preparing, binding, executing, or fetching the query is returned as `Left OracleError`.
221 | |||
222 | export covering
223 | queryAs : FromRow a => Connection -> Query -> IO (Either OracleError (List a))
224 | queryAs conn query =
225 |   query_ conn (buildQuerySQL query) (binds query)
226 |
227 | ||| Execute a structured Query and decode exactly one returned row.
228 | |||
229 | ||| The SQL statement is constructed using `buildQuerySQL`, which renders each `QueryColumn` in the SELECT projection.
230 | |||
231 | ||| Ordinary columns are rendered unchanged, while `JSONColumn` expressions are wrapped with `JSON_SERIALIZE(... RETURNING CLOB)`.
232 | |||
233 | ||| The returned row is decoded using the `FromRow` implementation for the requested result type.
234 | |||
235 | ||| This function succeeds only when the query returns exactly one row.
236 | |||
237 | ||| It returns an `OracleError` if the query returns no rows or more than one row.
238 | |||
239 | ||| Any Oracle error encountered while preparing, binding, executing, or fetching the query is returned as `Left OracleError`.
240 | |||
241 | export covering
242 | queryOneAs : FromRow a => Connection -> Query -> IO (Either OracleError a)
243 | queryOneAs conn query =
244 |   queryExactlyOne conn (buildQuerySQL query) (binds query)
245 |
246 | --------------------------------------------------------------------------------
247 | --          JSON Query
248 | --------------------------------------------------------------------------------
249 |
250 | ||| Execute a JSON query and return the serialized JSON document.
251 | |||
252 | ||| The JSONQuery expression is wrapped internally as:
253 | |||
254 | |||   JSON_SERIALIZE(expression RETURNING CLOB)
255 | |||
256 | ||| The query must return exactly one row containing one non-null JSON value.
257 | |||
258 | ||| The returned JSON is represented as a String so that it can be decoded using the Idris2 JSON library.
259 | |||
260 | ||| Example:
261 | |||
262 | |||   queryJSON conn
263 | |||     (MkJSONQuery
264 | |||       "payload"
265 | |||       "documents WHERE id = :id"
266 | |||       [MkBindParameter "id" (OracleNumber 42)])
267 | |||
268 | export covering
269 | queryJSON : Connection -> JSONQuery -> IO (Either OracleError String)
270 | queryJSON conn jsonquery = do
271 |   result <- query conn (buildJSONQuerySQL jsonquery) jsonquery.binds
272 |   case result of
273 |     Left err   =>
274 |       pure (Left err)
275 |     Right rows => do
276 |       case rows of
277 |         []    =>
278 |           pure $
279 |             Left $
280 |               MkOracleError
281 |                 (-1)
282 |                 "JSON query returned no rows"
283 |                 "Oracle.Query.queryJSON"
284 |                 False
285 |         [row] =>
286 |           case row of
287 |             []                   =>
288 |               pure $
289 |                 Left $
290 |                   MkOracleError
291 |                     (-1)
292 |                     "JSON query returned no columns"
293 |                     "Oracle.Query.queryJSON"
294 |                     False
295 |             [OracleNull]         =>
296 |               pure $
297 |                 Left $
298 |                   MkOracleError
299 |                     (-1)
300 |                     "JSON query returned NULL"
301 |                     "Oracle.Query.queryJSON"
302 |                     False
303 |             [OracleClob value]   =>
304 |               pure (Right value)
305 |             [OracleString value] =>
306 |               pure $
307 |                 Left $
308 |                   MkOracleError
309 |                     (-1)
310 |                     "JSON query returned an OracleString"
311 |                     "Oracle.Query.queryJSON"
312 |                     False
313 |             _                    =>
314 |               pure $
315 |                 Left $
316 |                   MkOracleError
317 |                     (-1)
318 |                     "JSON query returned an unexpected value type"
319 |                     "queryJSON"
320 |                     False
321 |         _     =>
322 |           pure $
323 |             Left $
324 |               MkOracleError
325 |                 (-1)
326 |                 "JSON query returned more than one row; use queryJSONList"
327 |                 "queryJSON"
328 |                 False
329 |
330 | ||| Execute a JSON query and return all serialized JSON documents.
331 | |||
332 | ||| Each row must contain exactly one non-null JSON value.
333 | |||
334 | ||| This is the multi-row counterpart to queryJSON.
335 | |||
336 | export covering
337 | queryJSONList : Connection -> JSONQuery -> IO (Either OracleError (List String))
338 | queryJSONList conn jsonquery = do
339 |   result <- query conn (buildJSONQuerySQL jsonquery) jsonquery.binds
340 |   case result of
341 |     Left err   =>
342 |       pure (Left err)
343 |     Right rows =>
344 |       decodeRows rows
345 |   where
346 |     decodeRows : List (List OracleValue) -> IO (Either OracleError (List String))
347 |     decodeRows []            =
348 |       pure (Right [])
349 |     decodeRows (row :: rest) =
350 |       case row of
351 |         [OracleClob value]   => do
352 |           tailresult <- decodeRows rest
353 |           case tailresult of
354 |             Left err     =>
355 |               pure (Left err)
356 |             Right values =>
357 |               pure (Right (value :: values))
358 |         [OracleString value] =>
359 |           pure $
360 |             Left $
361 |               MkOracleError
362 |                 (-1)
363 |                 "JSON query returned an OracleString"
364 |                 "Oracle.Query.queryJSONList"
365 |                 False
366 |         [OracleNull]         =>
367 |           pure $
368 |             Left $
369 |               MkOracleError
370 |                 (-1)
371 |                 "JSON query returned NULL"
372 |                 "Oracle.Query.queryJSONList"
373 |                 False
374 |         []                   =>
375 |           pure $
376 |             Left $
377 |               MkOracleError
378 |                 (-1)
379 |                 "JSON query returned no columns"
380 |                 "Oracle.Query.queryJSONList"
381 |                 False
382 |         _                    =>
383 |           pure $
384 |             Left $
385 |               MkOracleError
386 |                 (-1)
387 |                 "JSON query returned an unexpected number or type of columns"
388 |                 "Oracle.Query.queryJSONList"
389 |                 False
390 |
391 | ||| Execute a JSON query and decode the resulting JSON document.
392 | |||
393 | ||| The result is decoded using the FromJSON implementation for `a`.
394 | |||
395 | ||| This allows callers to query Oracle JSON directly into an Idris data type with a derived FromJSON implementation.
396 | |||
397 | export covering
398 | queryJSONAs : FromJSON a => Connection -> JSONQuery -> IO (Either OracleError a)
399 | queryJSONAs conn query = do
400 |   result <- queryJSON conn query
401 |   case result of
402 |     Left err   =>
403 |       pure (Left err)
404 |     Right json =>
405 |       case decode json of
406 |         Left jsonerr =>
407 |           pure $
408 |             Left $
409 |               MkOracleError
410 |                 (-1)
411 |                 ("Failed to decode JSON: " ++ show jsonerr)
412 |                 "Oracle.Query.queryJSONAs"
413 |                 False
414 |         Right value  =>
415 |           pure (Right value)
416 |
417 | ||| Execute a JSON query and decode all resulting JSON documents.
418 | |||
419 | ||| Each row is decoded using the FromJSON implementation for `a`.
420 | |||
421 | export covering
422 | queryJSONListAs : FromJSON a => Connection -> JSONQuery -> IO (Either OracleError (List a))
423 | queryJSONListAs conn query = do
424 |   result <- queryJSONList conn query
425 |   case result of
426 |     Left err         =>
427 |       pure (Left err)
428 |     Right jsonvalues =>
429 |       decodeValues jsonvalues
430 |   where
431 |     decodeValues : List String -> IO (Either OracleError (List a))
432 |     decodeValues []             =
433 |       pure (Right [])
434 |     decodeValues (json :: rest) =
435 |       case decode json of
436 |         Left jsonerr =>
437 |           pure $
438 |             Left $
439 |               MkOracleError
440 |                 (-1)
441 |                 ("Failed to decode JSON: " ++ show jsonerr)
442 |                 "Oracle.Query.queryJSONListAs"
443 |                 False
444 |         Right value  => do
445 |           tailresult <- decodeValues rest
446 |           case tailresult of
447 |             Left err     =>
448 |               pure (Left err)
449 |             Right values =>
450 |               pure (Right (value :: values))
451 |
452 | --------------------------------------------------------------------------------
453 | --          Execute Statement That Returns No Rows
454 | --------------------------------------------------------------------------------
455 |
456 | ||| Execute a statement that does not return rows.
457 | |||
458 | ||| This function is intended for:
459 | ||| - INSERT
460 | ||| - UPDATE
461 | ||| - DELETE
462 | ||| - MERGE
463 | ||| - DDL statements
464 | |||
465 | ||| The statement is automatically:
466 | ||| 1. Prepared.
467 | ||| 2. Bound.
468 | ||| 3. Executed.
469 | ||| 4. Released.
470 | |||
471 | ||| Example:
472 | |||
473 | ||| ```idris
474 | ||| execute_
475 | |||   conn
476 | |||   "insert into employees(id,name)
477 | |||    values (:id,:name)"
478 | |||   [ MkBindParameter "id"
479 | |||       (OracleInt 1)
480 | |||   , MkBindParameter "name"
481 | |||       (OracleString "Alice")
482 | |||   ]
483 | ||| ```
484 | |||
485 | export
486 | execute_ : Connection -> String -> List BindParameter -> IO (Either OracleError ())
487 | execute_ conn sql params =
488 |   withStatement conn sql $ \stmt =>
489 |     bind stmt params >>== \_ =>
490 |       execute stmt
491 |