#lang racket/base
(require racket/class
         "../generic/interfaces.rkt"
         "../generic/common.rkt"
         "../generic/sql-data.rkt"
         "../generic/sql-convert.rkt")
(provide dbsystem
         field-dvec->field-info
         field-dvec->typeid
         supported-typeid?
         classify-odbc-sql)

(define odbc-dbsystem%
  (class* dbsystem-base% (dbsystem<%>)
    (define/public (get-short-name) 'odbc)
    (define/override (get-type-list) type-list)
    (define/public (has-support? x) #f)

    (define/public (get-parameter-handlers param-typeids)
      (map get-check param-typeids))

    (define/public (field-dvecs->typeids dvecs)
      (map (lambda (dvec) (vector-ref dvec 1)) dvecs))

    (define/public (describe-params typeids)
      (map describe-typeid typeids))

    (define/public (describe-fields dvecs)
      (for/list ([dvec (in-list dvecs)])
        (describe-typeid (field-dvec->typeid dvec))))

    (super-new)))

(define dbsystem
  (new odbc-dbsystem%))

;; ----

(define (field-dvec->field-info dvec)
  `((name . ,(vector-ref dvec 0))
    (typeid . ,(vector-ref dvec 1))
    (size . ,(vector-ref dvec 2))
    (digits . ,(vector-ref dvec 3))))

(define (field-dvec->typeid dvec)
  (vector-ref dvec 1))

;; ----

;; SQL "parsing"
;; We just care about detecting commands that affect transaction status.

;; Since we have no idea what the actual database system is, just cover
;; standard commands and assume DDL is not transactional.

;; classify-odbc-sql : string [nat] -> symbol/#f
(define classify-odbc-sql
  (make-sql-classifier #:hash-comments? #t
   '(;; Explicit transaction commands
     ("ROLLBACK TRANSACTION TO"  rollback-savepoint)
     ("ROLLBACK WORK TO"  rollback-savepoint)
     ("ROLLBACK TO"       rollback-savepoint)
     ("RELEASE"           release-savepoint)
     ("SAVEPOINT"         savepoint)
     ("START"             start)
     ("BEGIN"             start)
     ("COMMIT"            commit)
     ("END"               commit)
     ("ROLLBACK"          rollback) ;; Note: after ROLLBACK TO, etc

     ;; Implicit commit
     ("ALTER"             implicit-commit)
     ("CREATE"            implicit-commit)
     ("DROP"              implicit-commit)
     ("GRANT"             implicit-commit)
     ("RENAME"            implicit-commit)
     ("TRUNCATE"          implicit-commit))))

;; ----

(define-syntax-rule
  (defchecks get-check [(typeid name pred ...) ...] [(*typeid *name *fun) ...])
  (define get-check
    (let ([name (mk-check typeid (lambda (z) (or (pred z) ...)) #:contract-parts '(pred ...))] ...
          [*name *fun] ...)
      (lambda (x)
        (case x
          ((typeid) name) ...
          ((*typeid) *name) ...
          (else
           (lambda (fsym index param)
             (error/unsupported-type fsym x))))))))

(define (mk-check typeid pred #:contract-parts [ctc-parts #f])
  (lambda (fsym param)
    (unless (pred param)
      (error/no-convert fsym "ODBC" (typeid->type typeid) param
                        #:contract (cond [(= (length ctc-parts) 1)
                                          (car ctc-parts)]
                                         [else (cons 'or/c ctc-parts)])))
    param))

(define (check-numeric fsym param)
  (define (bad note)
    (error/no-convert fsym "ODBC" "numeric" param note))
  (unless (rational? param) (bad ""))
  (let ([scaled (exact->scaled-integer (inexact->exact param))])
    (unless scaled (bad ""))
    (let ([ma (car scaled)]
          [ex (cdr scaled)])
      ;; check (abs ma) fits in 16*8 bits, ex fits in char
      (unless (<= -128 ex 127) (bad "(scale too large)"))
      (unless (< (abs ma) (expt 2 (* 16 8))) (bad "(mantissa too long)"))
      (cons ma ex))))

(defchecks get-check
  [(0  unknown        string? bytes? rational? boolean? sql-date? sql-time? sql-timestamp?)
   (1  character      string?)
   (4  integer        int32?)
   (5  smallint       int16?)
   (6  float          real?)
   (7  real           real?)
   (8  double         real?)
   (9  datetime       sql-timestamp?)
   (12 varchar        string?)
   (91 date           sql-date?)
   (92 time           sql-time?)
   (93 timestamp      sql-timestamp?)
   (-1 longvarchar    string?)
   (-2 binary         bytes?)
   (-3 varbinary      bytes?)
   (-4 longvarbinary  bytes?)
   (-5 bigint         int64?)
   (-6 tinyint        int8?)
   (-7 bit1           boolean?)
   (-8 wcharacter     string?)
   (-9 wvarchar       string?)
   (-10 wlongvarchar  string?)]
  [(2  numeric        check-numeric)
   (3  decimal        check-numeric)])

;; ----

(define-type-table (type-list
                    typeid->type
                    describe-typeid)

  (0  unknown        0)
  (1  character      0)
  (2  numeric        0)
  (3  decimal        0)
  (4  integer        0)
  (5  smallint       0)
  (6  float          0)
  (7  real           0)
  (8  double         0)
  (9  datetime       0)
  (12 varchar        0)
  (91 date           0)
  (92 time           0)
  (93 timestamp      0)
  (-1 longvarchar    0)
  (-2 binary         0)
  (-3 varbinary      0)
  (-4 longvarbinary  0)
  (-5 bigint         0)
  (-6 tinyint        0)
  (-7 bit1           0) ;; not bit(n), always single bit
  (-8 wchar          0)
  (-9 wvarchar       0)
  (-10 wlongvarchar  0)

  ;; Unsupported types

  (101 interval-year          #f)
  (102 interval-month         #f)
  (103 interval-day           #f)
  (104 interval-hour          #f)
  (105 interval-minute        #f)
  (106 interval-second        #f)
  (107 interval-year-month    #f)
  (108 interval-day-hour      #f)
  (109 interval-day-minute    #f)
  (110 interval-day-second    #f)
  (111 interval-hour-minute   #f)
  (112 interval-hour-second   #f)
  (113 interval-minute-second #f))

(define (supported-typeid? x)
  (case x
    ((0 1 2 3 4 5 6 7 8 9 12 91 92 93 -1 -2 -3 -4 -5 -6 -7 -8 -9 -10) #t)
    (else #f)))
