Statement

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
use "pony-ffi"

class ref Statement
  """
  Non-sendable prepared statement wrapping SQLHSTMT. Reusable: bind,
  execute, fetch, close_cursor, rebind, re-execute.
  """

  var _hstmt: Pointer[None] tag
  let _param_count: U16
  let _conn_alive: _AliveFlag ref
  var _closed: Bool = false
  var _cursor_open: Bool = false
  var _last_warnings: (Warnings | None) = None
  let _bound_flags: Array[Bool] ref
  // Bound SqlValue per slot. Each value owns its own storage; Statement
  // retains the reference so the pointer captured by SQLBindParameter
  // stays valid until the next rebind on that slot or close().
  let _params: Array[(SqlValue | None)] ref
  let _param_inds: Array[I64] ref
  var _col_bindings: (_ColumnBindings | None) = None
  let _opts: OdbcOptions

  new ref _create(
    hstmt: Pointer[None] tag,
    param_count: U16,
    conn_alive: _AliveFlag ref,
    opts: OdbcOptions = OdbcOptions)
  =>
    _hstmt = hstmt
    _param_count = param_count
    _conn_alive = conn_alive
    _opts = opts

    let n = param_count.usize()
    _bound_flags = Array[Bool].init(false, n)
    _params = Array[(SqlValue | None)].init(None, n)
    _param_inds = Array[I64].init(0, n)

  fun ref _check_alive(): (None | ExecError) =>
    if _closed then
      return ExecError(
        StatementClosed, recover val Array[DiagRecord] end)
    end
    if not _conn_alive.is_alive() then
      return ExecError(
        ConnectionClosed, recover val Array[DiagRecord] end)
    end
    None

  fun ref parameter_types(): (Array[SqlTypeTag] val | MetadataError) =>
    """
    SQL type tag for each parameter placeholder, as reported by
    SQLDescribeParam. Available after prepare() succeeds; no binding or
    execution required.

    Some drivers (notably SQLite's ODBC driver) do not implement
    SQLDescribeParam and return MetadataError(
    DriverDoesNotSupportDescribeParam). psqlODBC supports it.
    """
    if _closed then
      return MetadataError(MetadataStatementClosed)
    end
    if not _conn_alive.is_alive() then
      return MetadataError(MetadataConnectionClosed)
    end

    let n = _param_count.usize()
    let tags = recover iso Array[SqlTypeTag](n) end

    var i: U16 = 1
    while i.usize() <= n do
      var data_type: I16 = 0
      var param_size: U64 = 0
      var decimal_digits: I16 = 0
      var nullable: I16 = 0

      let rc =
        @SQLDescribeParam(
        _hstmt,
        i,
        addressof data_type,
        addressof param_size,
        addressof decimal_digits,
        addressof nullable)

      if not ODBCConstants.ok(rc) then
        let diag = _DiagHelper.read(ODBCConstants.handle_stmt(), _hstmt)
        return MetadataError(
          DescribeParamErrorClassifier.classify(diag), diag)
      end

      tags.push(_SqlTypeTagMap(data_type))
      i = i + 1
    end

    consume tags

  fun ref column_types(): (Array[ColumnMeta] val | MetadataError) =>
    """
    Metadata for each result column (name, type tag, nullability) as
    reported by SQLDescribeCol. Available after prepare() succeeds.
    Returns an empty array for non-result statements (INSERT, UPDATE,
    DELETE, DDL).
    """
    if _closed then
      return MetadataError(MetadataStatementClosed)
    end
    if not _conn_alive.is_alive() then
      return MetadataError(MetadataConnectionClosed)
    end

    var num_cols_raw: I16 = 0
    @SQLNumResultCols(_hstmt, addressof num_cols_raw)
    let num_cols = num_cols_raw.usize()

    let metas = recover iso Array[ColumnMeta](num_cols) end

    var col: U16 = 1
    while col.usize() <= num_cols do
      match _describe_col(col)
      | let m: ColumnMeta => metas.push(m)
      | let e: MetadataError => return e
      end
      col = col + 1
    end

    consume metas

  fun ref _describe_col(col: U16): (ColumnMeta | MetadataError) =>
    """
    Read metadata for a single result column. Two-pass: start with a
    128-byte name buffer, retry with an exact-size buffer if the driver
    reports a longer name.
    """
    let initial_cap: USize = 128
    var name_buf = CBuffer[I16](initial_cap)
    var nbox = name_buf.written_size_ptr()
    var data_type: I16 = 0
    var col_size: U64 = 0
    var decimal_digits: I16 = 0
    var nullable: I16 = 0

    var rc =
      @SQLDescribeCol(
      _hstmt,
      col,
      name_buf.ptr(),
      initial_cap.i16(),
      addressof nbox.value,
      addressof data_type,
      addressof col_size,
      addressof decimal_digits,
      addressof nullable)

    if not ODBCConstants.ok(rc) then
      let diag = _DiagHelper.read(ODBCConstants.handle_stmt(), _hstmt)
      return MetadataError(DriverMetadataError, diag)
    end

    // col_name_max includes the null terminator, so truncation happens
    // when name_len >= (initial_cap - 1). Retry once with an exact buffer.
    if nbox.value.usize() >= (initial_cap - 1) then
      let bigger_cap = nbox.value.usize() + 1
      name_buf = CBuffer[I16](bigger_cap)
      nbox = name_buf.written_size_ptr()
      rc =
        @SQLDescribeCol(
        _hstmt,
        col,
        name_buf.ptr(),
        bigger_cap.i16(),
        addressof nbox.value,
        addressof data_type,
        addressof col_size,
        addressof decimal_digits,
        addressof nullable)
      if not ODBCConstants.ok(rc) then
        let diag = _DiagHelper.read(ODBCConstants.handle_stmt(), _hstmt)
        return MetadataError(DriverMetadataError, diag)
      end
    end

    // copy_string_truncated clamps to capacity when the driver reports a
    // longer name_len than the buffer can hold (first-pass truncation).
    // On the exact-sized retry, written_size <= allocated so no clamping.
    let name: String val =
      try name_buf.copy_string_truncated()? else "" end

    ColumnMeta(
      name,
      _SqlTypeTagMap(data_type),
      _NullabilityMap(nullable))

  fun ref parameter_types_p(): Array[SqlTypeTag] val ? =>
    """
    Partial variant of parameter_types(). Raises on error.
    """
    match \exhaustive\ parameter_types()
    | let a: Array[SqlTypeTag] val => a
    | let _: MetadataError => error
    end

  fun ref column_types_p(): Array[ColumnMeta] val ? =>
    """
    Partial variant of column_types(). Raises on error.
    """
    match \exhaustive\ column_types()
    | let a: Array[ColumnMeta] val => a
    | let _: MetadataError => error
    end

  fun ref bind(i: ParamIndex, v: SqlValue): (Bound | BindError) =>
    """
    Bind a value to a parameter slot. Each call replaces any previous
    binding on the slot — the prior SqlValue is released and the new one
    is retained for the lifetime of the binding.
    """
    if _closed then
      return BindError(BindStatementClosed, i)
    end
    if not _conn_alive.is_alive() then
      return BindError(BindConnectionClosed, i)
    end

    let idx = i.apply()
    if (idx == 0) or (idx > _param_count) then
      return BindError(ParamIndexOutOfRange, i)
    end

    let pos = (idx - 1).usize()

    try
      // Retain v before ODBC captures a pointer into its storage so the
      // backing memory outlives this method and the next SQLExecute.
      _params(pos)? = v
      _param_inds(pos)? = v.len_or_indptr()

      let rc = v.bind_to_odbc(_hstmt, idx, _param_inds.cpointer(pos))
      if not ODBCConstants.ok(rc) then
        let diag = _DiagHelper.read(ODBCConstants.handle_stmt(), _hstmt)
        _params(pos)? = None
        _bound_flags(pos)? = false
        return BindError(DriverRejected, i, diag)
      end

      _bound_flags(pos)? = true
    else
      return BindError(ParamIndexOutOfRange, i)
    end
    Bound

  fun ref bind_null(i: ParamIndex): (Bound | BindError) =>
    bind(i, SqlNull)

  fun ref bind_p(i: ParamIndex, v: SqlValue) ? =>
    """
    Partial variant of bind(). Raises error on failure.
    """
    match bind(i, v)
    | let _: BindError => error
    end

  fun ref bind_null_p(i: ParamIndex) ? =>
    """
    Partial variant of bind_null(). Raises error on failure.
    """
    match bind_null(i)
    | let _: BindError => error
    end

  fun ref execute(): (Executed | ExecError) =>
    """
    Execute a prepared SELECT, opening a cursor.
    """
    match _check_alive()
    | let e: ExecError => return e
    end
    if _cursor_open then
      return ExecError(
        CursorAlreadyOpen, recover val Array[DiagRecord] end)
    end

    match _check_all_bound()
    | let e: ExecError => return e
    end

    let rc = @SQLExecute(_hstmt)
    _last_warnings =
      if ODBCConstants.has_info(rc) then
        Warnings(_DiagHelper.read(ODBCConstants.handle_stmt(), _hstmt))
      else
        None
      end

    if not ODBCConstants.ok(rc) then
      let diag = _DiagHelper.read(ODBCConstants.handle_stmt(), _hstmt)
      return ExecError(ExecErrorClassifier.classify(diag), diag)
    end

    // Set up column bindings for fetching
    try
      _col_bindings = _ColumnBindings(_hstmt, _opts)?
    else
      // Column binding failed — close the driver-level cursor so the
      // statement can be reused via close_cursor() / re-execute.
      @SQLFreeStmt(_hstmt, ODBCConstants.sql_close_cursor())
      let diag = _DiagHelper.read(ODBCConstants.handle_stmt(), _hstmt)
      return ExecError(ExecErrorClassifier.classify(diag), diag)
    end

    _cursor_open = true
    Executed

  fun ref execute_update(): (RowCount | ExecError) =>
    """
    Execute a prepared DML. Returns affected row count.
    """
    match _check_alive()
    | let e: ExecError => return e
    end
    if _cursor_open then
      return ExecError(
        CursorAlreadyOpen, recover val Array[DiagRecord] end)
    end

    match _check_all_bound()
    | let e: ExecError => return e
    end

    let rc = @SQLExecute(_hstmt)
    _last_warnings =
      if ODBCConstants.has_info(rc) then
        Warnings(_DiagHelper.read(ODBCConstants.handle_stmt(), _hstmt))
      else
        None
      end

    if not ODBCConstants.ok(rc) then
      let diag = _DiagHelper.read(ODBCConstants.handle_stmt(), _hstmt)
      return ExecError(ExecErrorClassifier.classify(diag), diag)
    end

    var row_count: I64 = 0
    @SQLRowCount(_hstmt, addressof row_count)
    if row_count == ODBCConstants.sql_no_row_count() then
      NoRowCount
    else
      row_count.usize()
    end

  fun ref _check_all_bound(): (None | ExecError) =>
    if _param_count == 0 then return None end
    var i: USize = 0
    while i < _param_count.usize() do
      try
        if not _bound_flags(i)? then
          return ExecError(
            UnboundParams,
            recover val Array[DiagRecord] end)
        end
      end
      i = i + 1
    end
    None

  fun ref fetch(): (Row | EndOfRows | FetchError) =>
    """
    Fetch the next row. Row is a val snapshot.
    """
    if _closed then return FetchError(CursorClosed) end
    if not _conn_alive.is_alive() then
      return FetchError(FetchConnectionClosed)
    end
    if not _cursor_open then return FetchError(CursorClosed) end

    let rc = @SQLFetch(_hstmt)

    if rc == ODBCConstants.sql_no_data() then
      return EndOfRows
    end

    _last_warnings =
      if ODBCConstants.has_info(rc) then
        Warnings(_DiagHelper.read(ODBCConstants.handle_stmt(), _hstmt))
      else
        None
      end

    if not ODBCConstants.ok(rc) then
      let diag = _DiagHelper.read(ODBCConstants.handle_stmt(), _hstmt)
      return FetchError(DriverFetchError, diag)
    end

    // Build Row from bound column buffers (SQLFetch already wrote into them)
    match _col_bindings
    | let cb: _ColumnBindings => cb.build_row()
    else FetchError(DriverFetchError)
    end

  fun ref execute_p() ? =>
    """
    Partial variant of execute(). Raises error on failure.
    """
    match execute()
    | let _: ExecError => error
    end

  fun ref execute_update_p(): RowCount ? =>
    """
    Partial variant of execute_update(). Raises error on failure.
    """
    match \exhaustive\ execute_update()
    | let rc: RowCount => rc
    | let _: ExecError => error
    end

  fun ref fetch_into(row: MutableRow): (MutableRow | EndOfRows | FetchError) =>
    """
    Fetch the next row into a reusable MutableRow. Zero allocation for
    the row container (SqlText/SqlDecimal values still allocate strings).
    """
    if _closed then return FetchError(CursorClosed) end
    if not _conn_alive.is_alive() then
      return FetchError(FetchConnectionClosed)
    end
    if not _cursor_open then return FetchError(CursorClosed) end

    let rc = @SQLFetch(_hstmt)

    if rc == ODBCConstants.sql_no_data() then
      return EndOfRows
    end

    _last_warnings =
      if ODBCConstants.has_info(rc) then
        Warnings(_DiagHelper.read(ODBCConstants.handle_stmt(), _hstmt))
      else
        None
      end

    if not ODBCConstants.ok(rc) then
      let diag = _DiagHelper.read(ODBCConstants.handle_stmt(), _hstmt)
      return FetchError(DriverFetchError, diag)
    end

    match _col_bindings
    | let cb: _ColumnBindings => cb.build_row_into(row)
    else FetchError(DriverFetchError)
    end

  fun ref values(): StatementIterator =>
    """
    Return an iterator for use with Pony's `for` loop.
    Yields (Row val | FetchError) — match on each result.
    """
    StatementIterator(this)

  fun cancel_token(): CancelToken =>
    """
    Return a sendable token that can cancel this statement's
    in-progress operation from another actor.

    The token captures a raw copy of the SQLHSTMT pointer. It does not
    track whether the statement has been closed. Calling cancel() on a
    token after close() invokes SQLCancel on a freed handle — undefined
    behavior. The caller must ensure all outstanding tokens are discarded
    before calling close().
    """
    CancelToken(_hstmt)

  fun ref close_cursor() =>
    """
    Close cursor, keep statement for rebinding and re-execution.
    Unbinds columns so they can be rebound on next execute.
    """
    if _cursor_open then
      @SQLFreeStmt(_hstmt, ODBCConstants.sql_close_cursor())
      @SQLFreeStmt(_hstmt, ODBCConstants.sql_unbind())
      _cursor_open = false
      _col_bindings = None
    end

  fun ref last_warnings(): (Warnings | None) =>
    _last_warnings

  fun ref close() =>
    """
    Free the SQLHSTMT. Idempotent.

    Any CancelTokens obtained from cancel_token() become invalid after
    this call. Using a token after close() is undefined behavior — see
    cancel_token() for the lifetime contract.

    If the connection has already been closed, the driver freed this
    handle transitively via SQLFreeHandle(SQL_HANDLE_DBC); in that case
    we only mark ourselves closed without a second SQLFreeHandle call
    (which would be UB on a dangling handle).
    """
    if _closed then return end
    if _conn_alive.is_alive() then
      if _cursor_open then
        @SQLFreeStmt(_hstmt, ODBCConstants.sql_close_cursor())
      end
      @SQLFreeHandle(ODBCConstants.handle_stmt(), _hstmt)
    end
    _cursor_open = false
    _hstmt = Pointer[None]
    _closed = true
    _col_bindings = None

  fun _final() =>
    if (not _closed) and _conn_alive.is_alive() then
      @SQLFreeHandle(ODBCConstants.handle_stmt(), _hstmt)
    end

primitive Executed
  """
  Statement executed successfully (cursor opened for fetching).
  """

primitive Bound
  """
  Parameter value bound successfully.
  """

primitive EndOfRows
  """
  Returned by fetch() when no more rows are available.
  """