02241130创建于 2021年6月9日历史提交
;;;
;;; Manage PostgreSQL specific quoting of identifiers.
;;;
;;; We use this facility as early as possible (in schema-structs), so we
;;; need those bits of code in utils/ rather than pgsql/.
;;;

(in-package :pgloader.quoting)

(defun apply-identifier-case (identifier)
  "Return given IDENTIFIER with CASE handled to be PostgreSQL compatible."
  (let* ((lowercase-identifier (cl-ppcre:regex-replace-all
                                "[^a-zA-Z0-9.]" (string-downcase identifier) "_"))
         (*identifier-case*
          ;; we might need to force to :quote in some cases
          ;;
          ;; http://www.postgresql.org/docs/9.1/static/sql-syntax-lexical.html
          ;;
          ;; SQL identifiers and key words must begin with a letter (a-z, but
          ;; also letters with diacritical marks and non-Latin letters) or an
          ;; underscore (_).
          (cond ((quoted-p identifier)
                 :none)

                ((not (cl-ppcre:scan "^[A-Za-z_][A-Za-z0-9_$]*$" identifier))
                 :quote)

                ((member lowercase-identifier *pgsql-reserved-keywords*
                         :test #'string=)
                 (progn
                   ;; we need to both downcase and quote here
                   (when (eq :downcase *identifier-case*)
                     (setf identifier lowercase-identifier))
                   :quote))

                ;; in other cases follow user directive
                (t *identifier-case*))))
    (ecase *identifier-case*
      (:snake_case (camelCase-to-colname identifier))
      (:downcase   lowercase-identifier)
      (:quote      (format nil "~s"
                           (cl-ppcre:regex-replace-all "\"" identifier "\"\"")))
      (:none       identifier))))

(defun quoted-p (s &optional (quote-char #\"))
  "Return true if s is a double-quoted string"
  (or (null s)
      (when (< 1 (length s))
        (and (eq (char s 0) quote-char)
             (eq (char s (- (length s) 1)) quote-char)))))

(defun ensure-unquoted (identifier &optional (quote-char #\"))
  (cond ((null identifier) nil)
        ((< (length identifier) 2) identifier)
        ((quoted-p identifier quote-char)
         ;; when the table name comes from the user (e.g. in the
         ;; load file) then we might have to unquote it: the
         ;; PostgreSQL catalogs does not store object names in
         ;; their quoted form.
         (subseq identifier 1 (1- (length identifier))))

        (t identifier)))

(defun ensure-quoted (value &optional (quote-char #\"))
  (if (quoted-p value quote-char)
      value
      (format nil "~c~a~c" quote-char value quote-char)))

(defun build-identifier (sep &rest parts)
  "Concatenante PARTS into a PostgreSQL identifier, with SEP in between
   parts. That's useful for creating an index name from a table's oid and name."
  (apply-identifier-case
   (apply #'concatenate
          'string
          (loop :for (part . more?) :on parts
             :collect (ensure-unquoted (typecase part
                                         (string part)
                                         (t      (princ-to-string part))))
             :when more? :collect sep))))

;;;
;;; Camel Case converter
;;;
(defun camelCase-to-colname (string)
  "Transform input STRING into a suitable column name.
    lahmanID        lahman_id
    playerID        player_id
    birthYear       birth_year"
  (coerce
   (loop
      :for first := t :then nil
      :for char :across string
      :for previous-upper-p := nil :then char-upper-p
      :for char-upper-p := (and (alpha-char-p char)
                                (eq char (char-upcase char)))
      :for new-word := (and (not first) char-upper-p (not previous-upper-p))
      :when (and new-word (not (char= char #\_))) :collect #\_
      :collect (char-downcase char))
   'string))