| 1 | (require '#:asdf) |
|---|
| 2 | (require '#:cl-ppcre) |
|---|
| 3 | (require '#:dso-binary-class) |
|---|
| 4 | |
|---|
| 5 | (use-package '#:cl-ppcre) |
|---|
| 6 | (use-package '#:binary-class) |
|---|
| 7 | |
|---|
| 8 | |
|---|
| 9 | |
|---|
| 10 | ;;; xBase stuff. |
|---|
| 11 | |
|---|
| 12 | (define-file-struct dbf-header |
|---|
| 13 | (version :unsigned 1) |
|---|
| 14 | (last-update :date "YYMMDD") |
|---|
| 15 | (record-count :unsigned 4) |
|---|
| 16 | (header-length :unsigned 2) |
|---|
| 17 | (record-length :unsigned 2) |
|---|
| 18 | (nil nil 2) |
|---|
| 19 | (incomplete-transaction :unsigned 1) |
|---|
| 20 | (encrypted :unsigned 1) |
|---|
| 21 | (nil nil 12) |
|---|
| 22 | (mdx-flag :unsigned 1) |
|---|
| 23 | (language :unsigned 1) |
|---|
| 24 | (nil nil 2)) |
|---|
| 25 | |
|---|
| 26 | (define-file-struct field-desc |
|---|
| 27 | (name :string 11) |
|---|
| 28 | (type :string 1) |
|---|
| 29 | (nil nil 4) |
|---|
| 30 | (size :unsigned 1) |
|---|
| 31 | (precision :unsigned 1) |
|---|
| 32 | (nil nil 5) |
|---|
| 33 | (set-fields-flag :unsigned 1) |
|---|
| 34 | (nil nil 7) |
|---|
| 35 | (is-indexed :unsigned 1)) |
|---|
| 36 | |
|---|
| 37 | (defun peek-byte (in) |
|---|
| 38 | (let ((b (read-byte in))) |
|---|
| 39 | (file-position in (1- (file-position in))) |
|---|
| 40 | b)) |
|---|
| 41 | |
|---|
| 42 | (defun read-field-descs (in &optional descs) |
|---|
| 43 | (let ((b (peek-byte in))) |
|---|
| 44 | (if (= b 13) |
|---|
| 45 | (nreverse descs) |
|---|
| 46 | (read-field-descs in (cons (read-binary 'field-desc in) descs))))) |
|---|
| 47 | |
|---|
| 48 | (defun read-dbf-structure (file) |
|---|
| 49 | (with-open-file (in file :element-type '(unsigned-byte 8)) |
|---|
| 50 | (let* ((dbf-header (read-binary 'dbf-header in)) |
|---|
| 51 | (field-descs (read-field-descs in))) |
|---|
| 52 | (values dbf-header field-descs)))) |
|---|
| 53 | |
|---|
| 54 | |
|---|
| 55 | |
|---|
| 56 | (defun slot-from-field (desc) |
|---|
| 57 | (with-slots (name type size) desc |
|---|
| 58 | (let ((type (cond |
|---|
| 59 | ((string= type "C") :string) |
|---|
| 60 | ((string= type "N") :string) |
|---|
| 61 | ((string= type "D") :string) |
|---|
| 62 | ((string= type "M") :string) |
|---|
| 63 | ((string= type "L") :string) |
|---|
| 64 | (t (abort))))) |
|---|
| 65 | `(,(intern name) ,type ,size)))) |
|---|
| 66 | |
|---|
| 67 | (defun struct-from-field-descs (name descs) |
|---|
| 68 | `(define-file-struct ,name |
|---|
| 69 | (%deleted :unsigned 1) |
|---|
| 70 | ,@(mapcar #'slot-from-field descs))) |
|---|
| 71 | |
|---|
| 72 | (defstruct dbf-desc |
|---|
| 73 | name |
|---|
| 74 | filename |
|---|
| 75 | sql-create) |
|---|
| 76 | |
|---|
| 77 | (defvar *dbfs* (make-hash-table :test 'eq)) |
|---|
| 78 | |
|---|
| 79 | (defun sql-escape-fieldname (name) |
|---|
| 80 | (let ((name (string-downcase name))) |
|---|
| 81 | (if (member name '("group" "user") :test 'equal) |
|---|
| 82 | (concatenate 'string "\"" name "\"") |
|---|
| 83 | name))) |
|---|
| 84 | |
|---|
| 85 | (defun recode (str) |
|---|
| 86 | (let (r |
|---|
| 87 | e) |
|---|
| 88 | (dotimes (i (length str)) |
|---|
| 89 | (let* ((ch (aref str i)) |
|---|
| 90 | (code (char-code ch))) |
|---|
| 91 | (cond |
|---|
| 92 | ((or (<= (char-code #\Space) code (char-code #\[)) |
|---|
| 93 | (<= (char-code #\]) code (char-code #\~))) |
|---|
| 94 | (push (string ch) r)) |
|---|
| 95 | ((<= 0 code 255) |
|---|
| 96 | (push (format nil "\\x~2,'0X" code) r) |
|---|
| 97 | (setf e t)) |
|---|
| 98 | (t |
|---|
| 99 | (error "Bad character!"))))) |
|---|
| 100 | (values |
|---|
| 101 | (apply #'concatenate 'string (nreverse r)) |
|---|
| 102 | e))) |
|---|
| 103 | |
|---|
| 104 | (defun sql-escape-data (value) |
|---|
| 105 | (if (string= value "") |
|---|
| 106 | "null" |
|---|
| 107 | (multiple-value-bind (s recoded) (recode value) |
|---|
| 108 | (concatenate 'string |
|---|
| 109 | (if recoded "E'" "'") |
|---|
| 110 | (regex-replace-all "'" s "''") |
|---|
| 111 | "'")))) |
|---|
| 112 | |
|---|
| 113 | (defun sql-column (desc) |
|---|
| 114 | (with-slots (name type size precision) desc |
|---|
| 115 | (format nil "~A ~A" |
|---|
| 116 | (sql-escape-fieldname name) |
|---|
| 117 | (cond |
|---|
| 118 | ((string= type "C") |
|---|
| 119 | (format nil "VARCHAR(~A)" size)) |
|---|
| 120 | ((string= type "N") |
|---|
| 121 | (if (= precision 0) |
|---|
| 122 | (cond |
|---|
| 123 | ((< size 10) "INTEGER") |
|---|
| 124 | ((< size 19) "BIGINT") |
|---|
| 125 | (t "NUMERIC(~A)" size)) |
|---|
| 126 | (format nil "NUMERIC(~A,~A)" (1- size) precision))) |
|---|
| 127 | ((string= type "D") |
|---|
| 128 | "DATE") |
|---|
| 129 | ((string= type "M") |
|---|
| 130 | (warn "MEMOs aren't implemented yet.") |
|---|
| 131 | "NUMERIC(10)") |
|---|
| 132 | ((string= type "L") |
|---|
| 133 | "BOOLEAN") |
|---|
| 134 | (t (error "Bad type ~S." type)))))) |
|---|
| 135 | |
|---|
| 136 | (defvar *temporary-tables* nil) |
|---|
| 137 | |
|---|
| 138 | (defun sql-create (name descs) |
|---|
| 139 | (format nil "CREATE ~:[~;TEMP ~]TABLE ~A~% (~{~A~@{,~% ~A~}~})~:[~;~%ON COMMIT DROP~]" |
|---|
| 140 | *temporary-tables* |
|---|
| 141 | name |
|---|
| 142 | (mapcar 'sql-column descs) |
|---|
| 143 | (eql *temporary-tables* 1))) |
|---|
| 144 | |
|---|
| 145 | (defgeneric sql-insert (obj)) |
|---|
| 146 | |
|---|
| 147 | (defun make-sql-function (type name descs) |
|---|
| 148 | `(defmethod sql-insert ((obj ,type)) |
|---|
| 149 | (format |
|---|
| 150 | nil |
|---|
| 151 | ,(format nil "INSERT INTO ~A(~{~A~@{,~A~}~}) VALUES(~{~A~@{,~A~}~})" |
|---|
| 152 | name |
|---|
| 153 | (mapcar (lambda (desc) |
|---|
| 154 | (sql-escape-fieldname (field-desc-name desc))) |
|---|
| 155 | descs) |
|---|
| 156 | (mapcar (lambda (d) |
|---|
| 157 | (declare (ignore d)) |
|---|
| 158 | "~A") |
|---|
| 159 | descs)) |
|---|
| 160 | ,@(mapcar (lambda (desc) |
|---|
| 161 | `(sql-escape-data |
|---|
| 162 | (slot-value obj ',(intern (field-desc-name desc))))) |
|---|
| 163 | descs)))) |
|---|
| 164 | |
|---|
| 165 | (defmacro define-dbf-struct (name filename) |
|---|
| 166 | (let ((descs (nth-value 1 (read-dbf-structure filename)))) |
|---|
| 167 | `(progn |
|---|
| 168 | (setf (gethash ',name *dbfs*) |
|---|
| 169 | (make-dbf-desc :name ',name |
|---|
| 170 | :filename ,filename |
|---|
| 171 | :sql-create ,(sql-create name descs))) |
|---|
| 172 | ,(struct-from-field-descs name descs) |
|---|
| 173 | ,(make-sql-function name name descs)))) |
|---|
| 174 | |
|---|
| 175 | |
|---|
| 176 | |
|---|
| 177 | ;;; Now file stuff. |
|---|
| 178 | |
|---|
| 179 | (defun read-records (in type &optional records) |
|---|
| 180 | (if (or (not (listen in)) (= (peek-byte in) 26)) |
|---|
| 181 | (nreverse records) |
|---|
| 182 | (read-records in type (cons (read-binary type in) records)))) |
|---|
| 183 | |
|---|
| 184 | (defun read-dbf (type) |
|---|
| 185 | (let ((dbf-desc (gethash type *dbfs*))) |
|---|
| 186 | (with-slots (filename) dbf-desc |
|---|
| 187 | (let ((header (read-dbf-structure filename))) |
|---|
| 188 | (with-open-file (in filename :element-type '(unsigned-byte 8)) |
|---|
| 189 | (file-position in (slot-value header 'header-length)) |
|---|
| 190 | (read-records in type)))))) |
|---|
| 191 | |
|---|
| 192 | |
|---|
| 193 | |
|---|
| 194 | (defun import-dbf (filename table-name) |
|---|
| 195 | "Reads the DBF file, analyzing meta-data to create Lisp structures, |
|---|
| 196 | populating those structures, and constructing descriptive SQL." |
|---|
| 197 | (let ((field-descs (nth-value 1 (read-dbf-structure filename)))) |
|---|
| 198 | (setf (gethash 'import-struct *dbfs*) |
|---|
| 199 | (make-dbf-desc :name 'import-struct |
|---|
| 200 | :filename filename |
|---|
| 201 | :sql-create (sql-create table-name field-descs))) |
|---|
| 202 | (eval (struct-from-field-descs 'import-struct field-descs)) |
|---|
| 203 | (eval (make-sql-function 'import-struct table-name field-descs)) |
|---|
| 204 | (read-dbf 'import-struct))) |
|---|
| 205 | |
|---|
| 206 | (defun split-name (filename) |
|---|
| 207 | (unless (pathnamep filename) |
|---|
| 208 | (setf filename (pathname filename))) |
|---|
| 209 | (let ((name (pathname-name filename))) |
|---|
| 210 | (values (subseq name 0 4) |
|---|
| 211 | (subseq name 4)))) |
|---|
| 212 | |
|---|
| 213 | (defun do-dbf (filename) |
|---|
| 214 | (multiple-value-bind (db-name table-name) (split-name filename) |
|---|
| 215 | (let ((recs (import-dbf filename table-name))) |
|---|
| 216 | (format t "~A;~4%" |
|---|
| 217 | (slot-value (gethash 'import-struct *dbfs*) 'sql-create)) |
|---|
| 218 | (dolist (rec recs) |
|---|
| 219 | (unless (/= (slot-value rec '%deleted) 32) |
|---|
| 220 | (format t "~2&~A;~%" (sql-insert rec))))))) |
|---|
| 221 | |
|---|
| 222 | (defun main () |
|---|
| 223 | (unless (member (length *posix-argv*) '(2 3)) |
|---|
| 224 | (format *error-output* "Usage: xbase [-t | -tt] dbf-file~%") |
|---|
| 225 | (quit)) |
|---|
| 226 | (let* ((*temporary-tables* (position (second *posix-argv*) '("-t" "-tt") :test 'string=)) |
|---|
| 227 | (file (if *temporary-tables* |
|---|
| 228 | (third *posix-argv*) |
|---|
| 229 | (second *posix-argv*)))) |
|---|
| 230 | (do-dbf file)) |
|---|
| 231 | (quit)) |
|---|