-
Notifications
You must be signed in to change notification settings - Fork 4
/
Copy pathpg.el
3324 lines (3003 loc) · 142 KB
/
pg.el
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
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
;;; pg.el --- Socket-level interface to the PostgreSQL database -*- lexical-binding: t -*-
;; Copyright: (C) 1999-2002, 2022-2025 Eric Marsden
;; Author: Eric Marsden <[email protected]>
;; Version: 0.46
;; Keywords: data comm database postgresql
;; URL: https://github.com/emarsden/pg-el
;; Package-Requires: ((emacs "28.1") (peg "1.0"))
;; SPDX-License-Identifier: GPL-3.0-or-later
;;
;; This file is free software: you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published
;; by the Free Software Foundation, either version 3 of the License,
;; or (at your option) any later version.
;;
;; This file is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;;
;; You should have received a copy of the GNU General Public License
;; along with this file. If not, see <https://www.gnu.org/licenses/>.
;;; Commentary:
;; Overview
;; --------
;;
;; This module lets you access the PostgreSQL database from Emacs, using its socket-level
;; frontend/backend protocol (the PostgreSQL wire protocol). The module is capable of automatic type
;; coercions from a range of SQL types to the equivalent Emacs Lisp type.
;;
;; Supported features:
;;
;; - SCRAM-SHA-256 authentication (the default method since PostgreSQL version 14) and MD5
;; - authentication.
;;
;; - Encrypted (TLS) connections between Emacs and the PostgreSQL backend.
;;
;; - Parameterized queries using PostgreSQL's extended query syntax, to protect from SQL
;; injection issues.
;;
;; - The PostgreSQL COPY protocol to copy preformatted data to PostgreSQL from an Emacs
;; buffer.
;;
;; - Asynchronous handling of LISTEN/NOTIFY notification messages from PostgreSQL, allowing the
;; implementation of publish-subscribe type architectures (PostgreSQL as an "event broker" or
;; "message bus" and Emacs as event publisher and consumer).
;;
;;
;; This is a low level API, and won't be useful to end users. If you're looking for a
;; browsing/editing interface to PostgreSQL, see the PGmacs module from
;; https://github.com/emarsden/pgmacs/.
;;
;;
;; Entry points
;; ------------
;;
;; See the online documentation at <https://emarsden.github.io/pg-el/API.html>.
;; Thanks to Eric Ludlam for discovering a bug in the date parsing routines, to
;; Hartmut Pilch and Yoshio Katayama for adding multibyte support, and to Doug
;; McNaught and Pavel Janik for bug fixes.
;;; TODO
;;
;; * Implement the SASLPREP algorithm for usernames and passwords that contain
;; unprintable characters (used for SCRAM-SHA-256 authentication).
;;; Code:
(require 'cl-lib)
(require 'eieio)
(require 'hex-util)
(require 'bindat)
(require 'url)
(require 'peg)
(require 'rx)
(require 'parse-time)
;; https://www.postgresql.org/docs/current/libpq-envars.html
(defvar pg-application-name (or (getenv "PGAPPNAME") "pg.el")
"The application_name sent to the PostgreSQL backend.
This information appears in queries to the `pg_stat_activity' table
and (depending on server configuration) in the connection log.")
(defvar pg-connect-timeout
(cl-case system-type
(windows-nt 0)
(ms-dos 0)
(t 30))
"Timeout in seconds for establishing the network connection to PostgreSQL.
If set to zero (the default on Microsoft Windows platforms), do not create
a timer to signal a connection timeout.")
(defvar pg-read-timeout 10
"Timeout in seconds when reading data from PostgreSQL.")
(defvar pg-disable-type-coercion nil
"*Non-nil disables the type coercion mechanism.
The default is nil, which means that data recovered from the database
is coerced to the corresponding Emacs Lisp type before being returned;
for example numeric data is transformed to Emacs Lisp numbers, and
booleans to booleans.
The coercion mechanism requires an initialization query to the
database, in order to build a table mapping type names to OIDs. This
option is provided mainly in case you wish to avoid the overhead of
this initial query. The overhead is only incurred once per Emacs
session (not per connection to the backend).")
(defvar pg-parameter-change-functions (list 'pg-handle-parameter-client-encoding)
"List of handlers called when the backend informs us of a parameter change.
Each handler is called with three arguments: the connection to
the backend, the parameter name and the parameter value.")
(defvar pg-handle-notice-functions (list 'pg-log-notice)
"List of handlers called when the backend sends us a NOTICE message.
Each handler is called with one argument, the notice, as a pgerror
struct.")
(define-error 'pg-error "PostgreSQL error" 'error)
(define-error 'pg-user-error "pg-el user error" 'pg-error)
(define-error 'pg-protocol-error "PostgreSQL protocol error" 'pg-error)
(define-error 'pg-copy-failed "PostgreSQL COPY failed" 'pg-error)
(define-error 'pg-connect-timeout "PostgreSQL connection attempt timed out" 'pg-error)
(define-error 'pg-type-error
"Incorrect type in binding PostgreSQL prepared statement"
'pg-user-error)
(defun pg-signal-type-error (fmt &rest arguments)
(let ((msg (apply #'format fmt arguments)))
(signal 'pg-type-error (list msg))))
;; Maps from type-name to a function that converts from text representation to wire-level binary
;; representation.
(defvar pg--serializers (make-hash-table :test #'equal))
;; Contains an entry for types that serialize to a text format, rather than a binary format (e.g.
;; HSTORE). The serialization function itself is stored in pg--serializers.
(defvar pg--textual-serializers (make-hash-table :test #'equal))
;; Maps from type-name to a parsing function (from string to Emacs native type). This is built
;; dynamically at initialization of the connection with the database (once generated, the
;; information is shared between connections).
(defvar pg--parser-by-typname (make-hash-table :test #'equal))
(defclass pgcon ()
((dbname
:type string
:initarg :dbname
:accessor pgcon-dbname)
(process
:initarg :process
:accessor pgcon-process)
(pid
:type integer
:accessor pgcon-pid )
(server-version-major
:accessor pgcon-server-version-major)
;; Holds something like 'postgresql, 'ydb, 'cratedb
(server-variant
:type symbol
:initform 'postgresql
:accessor pgcon-server-variant)
(secret
:accessor pgcon-secret)
(client-encoding
:type symbol
:initform 'utf-8
:accessor pgcon-client-encoding)
;; Maps from oid (an integer) to a parsing function.
(parser-by-oid
:type hash-table
:initform (make-hash-table :test #'eql)
:accessor pgcon-parser-by-oid)
;; Maps from type-name to PostgreSQL oid, for PostgreSQL builtin types.
(oid-by-typname
:type hash-table
:initform (make-hash-table :test #'equal)
:accessor pgcon-oid-by-typname)
;; Maps from oid to type-name.
(typname-by-oid
:type hash-table
:initform (make-hash-table :test #'eql)
:accessor pgcon-typname-by-oid)
(timeout
;; This bizarre (progn ...) syntax is required by EIEIO for historical reasons.
:initform (progn pg-read-timeout)
:accessor pgcon-timeout)
(connect-timer
:initform nil
:accessor pgcon-connect-timer)
(query-log
:initform nil
:accessor pgcon-query-log)
(prepared-statement-cache
:type hash-table
:initform (make-hash-table :test #'equal)
:accessor pgcon-prepared-statement-cache)
(connect-info
:initform nil
:accessor pgcon-connect-info)))
(defun make-pgcon (&rest args)
(apply #'make-instance (cons 'pgcon args)))
(cl-defmethod cl-print-object ((this pgcon) stream)
"Printer for pgcon PostgreSQL connection objects."
(princ (format "#<PostgreSQL connection to %s, pid %s>"
(pgcon-dbname this) (pgcon-pid this))
stream))
;; Used to save the connection-specific position in our input buffer.
(defvar-local pgcon--position 1)
;; Used to check whether the connection is currently "busy", so that we can determine whether a
;; message was received asynchronously or synchronously.
(defvar-local pgcon--busy t)
(defvar-local pgcon--notification-handlers (list))
(defun pg-connection-set-busy (con busy)
(with-current-buffer (process-buffer (pgcon-process con))
(setq-local pgcon--busy busy)))
(defun pg-connection-busy-p (con)
(with-current-buffer (process-buffer (pgcon-process con))
pgcon--busy))
(defun pg-enable-query-log (con)
"Enable logging of PostgreSQL queries on connection CON.
Queries are logged to a buffer identified by `pgcon-query-log'."
(unless (pgcon-query-log con)
(setf (pgcon-query-log con) (generate-new-buffer " *PostgreSQL query log*"))))
;; The qualified name is represented in SQL queries as schema.name. The schema is often either the
;; username or "public".
(cl-defstruct pg-qualified-name
"The identifier for a table or view which optionally includes a schema."
(schema nil)
name)
;; Print as "\"schema\".\"name\"", for example "\"public\".\"mytable\"".
(defun pg-print-qualified-name (qn)
(let ((schema (pg-escape-identifier (pg-qualified-name-schema qn)))
(name (pg-escape-identifier (pg-qualified-name-name qn))))
(if schema
(format "%s.%s" schema name)
name)))
(cl-defstruct pgresult
connection status attributes tuples portal (incomplete nil))
(defsubst pg-flush (con)
(accept-process-output (pgcon-process con) 0.1))
;; this is ugly because lambda lists don't do destructuring
(defmacro with-pg-connection (con connect-args &rest body)
"Execute BODY forms in a scope with connection CON created by CONNECT-ARGS.
The database connection is bound to the variable CON. If the
connection is unsuccessful, the forms are not evaluated.
Otherwise, the BODY forms are executed, and upon termination,
normal or otherwise, the database connection is closed."
`(let ((,con (pg-connect ,@connect-args)))
(unwind-protect
(progn ,@body)
(when ,con (pg-disconnect ,con)))))
(put 'with-pg-connection 'lisp-indent-function 'defun)
(defmacro with-pg-connection-local (con connect-args &rest body)
"Execute BODY forms in a scope with local Unix connection CON
created by CONNECT-ARGS.
The database connection is bound to the variable CON. If the
connection is unsuccessful, the forms are not evaluated.
Otherwise, the BODY forms are executed, and upon termination,
normal or otherwise, the database connection is closed."
`(let ((,con (pg-connect-local ,@connect-args)))
(unwind-protect
(progn ,@body)
(when ,con (pg-disconnect ,con)))))
(put 'with-pg-connection 'lisp-indent-function 'defun)
(defmacro with-pg-transaction (con &rest body)
"Execute BODY forms in a BEGIN..END block with pre-established connection CON.
If a PostgreSQL error occurs during execution of the forms, execute
a ROLLBACK command.
Large-object manipulations _must_ occur within a transaction, since
the large object descriptors are only valid within the context of a
transaction."
(let ((exc-sym (gensym)))
`(progn
(pg-exec ,con "BEGIN")
(condition-case ,exc-sym
(prog1 (progn ,@body)
(pg-exec ,con "COMMIT"))
(error
(message "PostgreSQL error %s" ,exc-sym)
(pg-exec ,con "ROLLBACK"))))))
(put 'with-pg-transaction 'lisp-indent-function 'defun)
(defun pg-for-each (con select-form callback)
"Create a cursor for SELECT-FORM and call CALLBACK for each result.
Uses the PostgreSQL database connection CON. SELECT-FORM must be an
SQL SELECT statement. The cursor is created using an SQL DECLARE
CURSOR command, then results are fetched successively until no results
are left. The cursor is then closed.
The work is performed within a transaction. The work can be
interrupted before all tuples have been handled by THROWing to a
tag called pg-finished."
(let ((cursor (symbol-name (gensym "pgelcursor"))))
(catch 'pg-finished
(with-pg-transaction con
(pg-exec con "DECLARE " cursor " CURSOR FOR " select-form)
(unwind-protect
(cl-loop for res = (pg-result (pg-exec con "FETCH 1 FROM " cursor) :tuples)
until (zerop (length res))
do (funcall callback res))
(pg-exec con "CLOSE " cursor))))))
;; This is installed as an Emacs Lisp process filter for the PostgreSQL connection. We return nil if
;; the PostgreSQL connection is currently "busy" (meaning that we are currently processing a
;; synchronous request), or if the data received doesn't look like a complete NotificationResponse
;; message (starting with ?A, total length compatible with the length specified in the PostgreSQL
;; message format). Otherwise, we return the length of the message length that we handled.
;;
;; When we return non-nil, the original process filter is called (see `add-function' advice below),
;; which places the data in the process buffer for normal (synchronous) handling.
(defun pg-process-filter (process data)
(with-current-buffer (process-buffer process)
(unless pgcon--busy
(when (and (eql ?A (aref data 0))
(eql 0 (aref data 1)))
(let ((msglen 0))
;; read a net int in 4 octets representing the message length
(setq msglen (+ (* 256 msglen) (aref data 1)))
(setq msglen (+ (* 256 msglen) (aref data 2)))
(setq msglen (+ (* 256 msglen) (aref data 3)))
(setq msglen (+ (* 256 msglen) (aref data 4)))
;; We parse the channel and payload if we received a full NotificationResponse. msglen is one
;; less than the size of data due to the ?A message tag.
(when (eql (1+ msglen) (length data))
;; ignore a net int in 4 octets representing notifying backend PID
(let* ((channel-end-pos (cl-position 0 data :start 9 :end (length data)))
(payload-end-pos (cl-position 0 data :start (1+ channel-end-pos) :end (length data)))
(channel (cl-subseq data 9 channel-end-pos))
(payload (cl-subseq data (1+ channel-end-pos) payload-end-pos)))
(dolist (handler pgcon--notification-handlers)
(funcall handler channel payload))))
(1- msglen))))))
;; With the :before-until advice type, the original process filter (which places the incoming
;; content in the process buffer) will be called only if our process filter returns nil. Our process
;; filter returns non-nil when it has detected, parsed and handled an asynchronous notification and
;; nil otherwise. This allows us to avoid duplicate processing of asynchronous notifications, once
;; by #'pg-process-filter and once by the notification handling code in pg-exec.
(defun pg-enable-async-notification-handlers (con)
(add-function :before-until (process-filter (pgcon-process con)) #'pg-process-filter))
(defun pg-disable-async-notification-handlers (con)
(remove-function (process-filter (pgcon-process con)) #'pg-process-filter))
(defconst pg--AUTH_REQ_OK 0)
(defconst pg--AUTH_REQ_KRB4 1)
(defconst pg--AUTH_REQ_KRB5 2)
(defconst pg--AUTH_REQ_PASSWORD 3) ; AuthenticationCleartextPassword
(defconst pg--AUTH_REQ_CRYPT 4)
(defconst pg--STARTUP_MSG 7)
(defconst pg--STARTUP_KRB4_MSG 10)
(defconst pg--STARTUP_KRB5_MSG 11)
(defconst pg--STARTUP_PASSWORD_MSG 14)
(defun pg--detect-server-variant (con)
"Detect the flavour of PostgreSQL that we are connected to.
Uses connection CON. Also run variant-specific configuration actions.
The variant can be accessed by `pgcon-server-variant'."
(pcase (pgcon-server-variant con)
;; This is the default value, meaning we haven't yet identified a variant based on its backend
;; parameter values.
('postgresql
(let ((version (pg-backend-version con)))
(cond ((cl-search "CrateDB" version)
(setf (pgcon-server-variant con) 'cratedb))
((cl-search "CockroachDB" version)
(setf (pgcon-server-variant con) 'cockroachdb))
((cl-search "-YB-" version)
(setf (pgcon-server-variant con) 'yugabyte))
((cl-search "Visual C++ build 1914" version)
(setf (pgcon-server-variant con) 'questdb))
((cl-search "GreptimeDB" version)
(setf (pgcon-server-variant con) 'greptimedb))
((cl-search "RisingWave" version)
(setf (pgcon-server-variant con) 'risingwave))
((cl-search "implemented by immudb" version)
(setf (pgcon-server-variant con) 'immudb)))))
('ydb
(pg-exec con "SET search_path = public"))
;; The Timescale status was "guessed" early on in the startup sequence from the presence of the
;; "default_transaction_read_only" backend parameter key. To make sure that this is really
;; TimescaleDB, we check for the existence of a TimescaleDB-specific schema.
('timescaledb
(let* ((sql "SELECT 1 FROM information_schema.schemata WHERE schema_name=$1")
(res (pg-exec-prepared con sql '(("_timescaledb_catalog" . "text")))))
(when (null (pg-result res :tuples))
(setf (pgcon-server-variant con) 'postgresql))))))
(defun pg-handle-error-response (con &optional context)
"Handle an ErrorMessage from the backend we are connected to over CON.
Additional information CONTEXT can be optionally included in the error message
presented to the user."
(let ((e (pg-read-error-response con))
(extra (list)))
(when (pgerror-detail e)
(push (format "detail: %s" (pgerror-detail e)) extra))
(when (pgerror-hint e)
(push (format "hint: %s" (pgerror-hint e)) extra))
(when (pgerror-table e)
(push (format "table: %s" (pgerror-table e)) extra))
(when (pgerror-column e)
(push (format "column: %s" (pgerror-column e)) extra))
(when (pgerror-file e)
(push (format "file: %s" (pgerror-file e)) extra))
(when (pgerror-line e)
(push (format "line: %s" (pgerror-line e)) extra))
(when (pgerror-routine e)
(push (format "routine: %s" (pgerror-routine e)) extra))
(when (pgerror-dtype e)
(push (format "dtype: %s" (pgerror-dtype e)) extra))
(when (pgerror-where e)
(push (format "where: %s" (pgerror-where e)) extra))
;; Now read the ReadyForQuery message. We don't always receive this immediately; for example if
;; an incorrect username is sent during startup, PostgreSQL sends an ErrorMessage then an
;; AuthenticationSASL message. In that case, unread the message type octet so that it can
;; potentially be handled after the error is signaled. Some databases like Clickhouse
;; immediately close their connection on error, so we ignore any errors here.
(ignore-errors
(let ((c (pg-read-char con)))
(unless (eql c ?Z)
(message "Unexpected message type after ErrorMsg: %s" c)
(pg-unread-char con)))
;; Read message length then status, which we discard.
(pg-read-net-int con 4)
(pg-read-char con))
(let ((msg (format "%s%s: %s (%s)"
(pgerror-severity e)
(if context (concat " " context) "")
(pgerror-message e)
(string-join extra ", "))))
(signal 'pg-error (list msg)))))
;; Run the startup interaction with the PostgreSQL database. Authenticate and read the connection
;; parameters. This function allows us to share code common to TCP and Unix socket connections to
;; the backend.
(cl-defun pg-do-startup (con dbname user password)
"Handle the startup sequence to authenticate with PostgreSQL over CON.
Uses database DBNAME, user USER and password PASSWORD."
;; send the StartupMessage, as per https://www.postgresql.org/docs/current/protocol-message-formats.html
(pg-connection-set-busy con t)
(let ((packet-octets (+ 4 2 2
(1+ (length "user"))
(1+ (length user))
(1+ (length "database"))
(1+ (length dbname))
(1+ (length "application_name"))
(1+ (length pg-application-name))
1)))
(pg-send-uint con packet-octets 4)
(pg-send-uint con 3 2) ; Protocol major version = 3
(pg-send-uint con 0 2) ; Protocol minor version = 0
(pg-send-string con "user")
(pg-send-string con user)
(pg-send-string con "database")
(pg-send-string con dbname)
(pg-send-string con "application_name")
(pg-send-string con pg-application-name)
;; A zero byte is required as a terminator after the last name/value pair.
(pg-send-uint con 0 1)
(pg-flush con))
(when (pgcon-connect-timer con)
(cancel-timer (pgcon-connect-timer con)))
(cl-loop
for c = (pg-read-char con) do
(cl-case c
;; an ErrorResponse message
(?E
(pg-handle-error-response con "after StartupMessage"))
;; NegotiateProtocolVersion
(?v
(let ((_msglen (pg-read-net-int con 4))
(protocol-supported (pg-read-net-int con 4))
(unrec-options (pg-read-net-int con 4))
(unrec (list)))
;; read the list of protocol options not supported by the server
(dotimes (_i unrec-options)
(push (pg-read-string con 4096) unrec))
(let ((msg (format "Server only supports protocol minor version <= %s" protocol-supported)))
(signal 'pg-protocol-error (list msg)))))
;; BackendKeyData
(?K
(let ((_msglen (pg-read-net-int con 4)))
(setf (pgcon-pid con) (pg-read-net-int con 4))
(setf (pgcon-secret con) (pg-read-net-int con 4))))
;; NoticeResponse
(?N
;; a Notice response has the same structure and fields as an ErrorResponse
(let ((notice (pg-read-error-response con)))
(dolist (handler pg-handle-notice-functions)
(funcall handler notice))))
;; ReadyForQuery message
(?Z
(let ((_msglen (pg-read-net-int con 4))
(status (pg-read-char con)))
;; status is 'I' or 'T' or 'E', Idle or InTransaction or Error
(when (eql ?E status)
(message "PostgreSQL ReadyForQuery message with error status"))
(and (not pg-disable-type-coercion)
(zerop (hash-table-count (pgcon-parser-by-oid con)))
(pg-initialize-parsers con))
(pg--detect-server-variant con)
;; This statement fails on ClickHouse (and the database immediately closes the connection!).
(unless (eq 'clickhouse (pgcon-server-variant con))
(pg-exec con "SET datestyle = 'ISO'"))
(pg-enable-async-notification-handlers con)
(pg-connection-set-busy con nil)
(cl-return-from pg-do-startup con)))
;; an authentication request
(?R
(let ((_msglen (pg-read-net-int con 4))
(areq (pg-read-net-int con 4)))
(cond
;; AuthenticationOK message
((= areq pg--AUTH_REQ_OK)
;; Continue processing server messages and wait for the ReadyForQuery
;; message
nil)
((= areq pg--AUTH_REQ_PASSWORD)
;; send a PasswordMessage
(pg-send-char con ?p)
(pg-send-uint con (+ 5 (length password)) 4)
(pg-send-string con password)
(pg-flush con))
;; AuthenticationSASL request
((= areq 10)
(pg-do-sasl-authentication con user password))
((= areq 5)
(pg-do-md5-authentication con user password))
((= areq pg--AUTH_REQ_CRYPT)
(signal 'pg-protocol-error '("Crypt authentication not supported")))
((= areq pg--AUTH_REQ_KRB4)
(signal 'pg-protocol-error '("Kerberos4 authentication not supported")))
((= areq pg--AUTH_REQ_KRB5)
(signal 'pg-protocol-error '("Kerberos5 authentication not supported")))
(t
(let ((msg (format "Can't do that type of authentication: %s" areq)))
(signal 'pg-protocol-error (list msg)))))))
;; ParameterStatus
(?S
(let* ((msglen (pg-read-net-int con 4))
(msg (pg-read-chars con (- msglen 4)))
(items (split-string msg (string 0)))
(key (cl-first items))
(val (cl-second items)))
;; ParameterStatus items sent by the backend include application_name,
;; DateStyle, in_hot_standby, integer_datetimes
(when (> (length key) 0)
(when (string= "server_version" key)
;; We need to accept a version string of the form "17beta1" as well as "16.1"
(let* ((major (cl-first (split-string val "\\.")))
(major-numeric (apply #'string
(cl-loop
for c across major
while (<= ?0 c ?9)
collect c))))
(setf (pgcon-server-version-major con) (cl-parse-integer major-numeric)))
(when (cl-search "ydb stable" val)
(setf (pgcon-server-variant con) 'ydb))
(when (cl-search "-greptimedb-" val)
(setf (pgcon-server-variant con) 'greptimedb))
(when (cl-search "OrioleDB" val)
(setf (pgcon-server-variant con) 'orioledb)))
;; Now some somewhat ugly code to detect semi-compatible PostgreSQL variants, to allow us
;; to work around some of their behaviour that is incompatible with real PostgreSQL.
(when (string= "session_authorization" key)
;; We could also look for the existence of the "xata" schema in pg-schemas
(when (string-prefix-p "xata" val)
(setf (pgcon-server-variant con) 'xata))
(when (string= "PGAdapter" val)
(setf (pgcon-server-variant con) 'spanner)))
(when (string-prefix-p "ivorysql." key)
(setf (pgcon-server-variant con) 'ivorydb))
;; We confirm this guess later in the startup sequence by checking for the existence of
;; TimescaleDB-specific schemas in the current database.
(when (string= "default_transaction_read_only" key)
(setf (pgcon-server-variant con) 'timescaledb))
(dolist (handler pg-parameter-change-functions)
(funcall handler con key val)))))
(t
(let ((msg (format "Problem connecting: expected an authentication response, got %s" c)))
(signal 'pg-protocol-error (list msg)))))))
;; Avoid warning from the bytecode compiler
(declare-function gnutls-negotiate "gnutls.el")
(declare-function network-stream-certificate "network-stream.el")
(cl-defun pg-connect (dbname user
&optional
(password "")
(host "localhost")
(port 5432)
(tls-options nil)
(server-variant nil))
"Initiate a connection with the PostgreSQL backend over TCP.
Connect to the database DBNAME with the username USER, on PORT of
HOST, providing PASSWORD if necessary. Return a connection to the
database (as an opaque type). PORT defaults to 5432, HOST to
\"localhost\", and PASSWORD to an empty string. If TLS-OPTIONS is
non-NIL, attempt to establish an encrypted connection to PostgreSQL
passing TLS-OPTIONS to `gnutls-negotiate'.
To use client certificates to authenticate the TLS connection,
use a value of TLS-OPTIONS of the form `(:keylist ((,key
,cert)))', where `key' is the filename of the client certificate
private key and `cert' is the filename of the client certificate.
These are passed to GnuTLS."
(let* ((buf (generate-new-buffer " *PostgreSQL*"))
(process (open-network-stream "postgres" buf host port
:coding nil
;; :nowait t
:nogreeting t))
(con (make-pgcon :dbname dbname :process process)))
(when server-variant
(setf (pgcon-server-variant con) server-variant))
(when (featurep 'make-network-process :nodelay)
(set-network-process-option process :nodelay t))
(unless (zerop pg-connect-timeout)
(setf (pgcon-connect-timer con)
(run-at-time pg-connect-timeout nil
(lambda ()
(unless (memq (process-status process) '(open listen))
(delete-process process)
(kill-buffer buf)
(signal 'pg-connect-timeout
(list "PostgreSQL connection timed out")))))))
(with-current-buffer buf
(set-process-coding-system process 'binary 'binary)
(set-buffer-multibyte nil)
(setq-local pgcon--position 1
pgcon--busy t
pgcon--notification-handlers (list)))
;; Save connection info in the pgcon object, for possible later use by pg-cancel
(setf (pgcon-connect-info con) (list :tcp host port dbname user password))
;; TLS connections to PostgreSQL are based on a custom STARTTLS-like connection upgrade
;; handshake. The frontend establishes an unencrypted network connection to the backend over the
;; standard port (normally 5432). It then sends an SSLRequest message, indicating the desire to
;; establish an encrypted connection. The backend responds with ?S to indicate that it is able
;; to support an encrypted connection. The frontend then runs TLS negociation to upgrade the
;; connection to an encrypted one.
(when tls-options
(require 'gnutls)
(require 'network-stream)
(unless (gnutls-available-p)
(signal 'pg-error '("Connecting over TLS requires GnuTLS support in Emacs")))
;; send the SSLRequest message
(pg-send-uint con 8 4)
(pg-send-uint con 80877103 4)
(pg-flush con)
(let ((ch (pg-read-char con)))
(unless (eql ?S ch)
(let ((msg (format "Couldn't establish TLS connection to PostgreSQL: read char %s" ch)))
(signal 'pg-protocol-error (list msg)))))
;; FIXME could use tls-options as third arg to network-stream-certificate
(let* ((cert (network-stream-certificate host port nil))
(opts (append (list :process process)
(list :hostname host)
(when cert (list :keylist cert))
(when (listp tls-options) tls-options))))
(condition-case err
;; now do STARTTLS-like connection upgrade
(apply #'gnutls-negotiate opts)
(gnutls-error
(let ((msg (format "TLS error connecting to PostgreSQL: %s" (error-message-string err))))
(signal 'pg-protocol-error (list msg)))))))
;; the remainder of the startup sequence is common to TCP and Unix socket connections
(pg-do-startup con dbname user password)))
(cl-defun pg-connect-local (path dbname user &optional (password ""))
"Initiate a connection with the PostgreSQL backend over local Unix socket PATH.
Connect to the database DBNAME with the username USER, providing
PASSWORD if necessary. Return a connection to the database (as an
opaque type). PASSWORD defaults to an empty string."
(let* ((buf (generate-new-buffer " *PostgreSQL*"))
(process (make-network-process :name "postgres"
:buffer buf
:family 'local
:service path
:coding nil))
(connection (make-pgcon :dbname dbname :process process)))
;; Save connection info in the pgcon object, for possible later use by pg-cancel
(setf (pgcon-connect-info connection) (list :local path nil dbname user password))
(with-current-buffer buf
(set-process-coding-system process 'binary 'binary)
(set-buffer-multibyte nil)
(setq-local pgcon--position 1
pgcon--busy t
pgcon--notification-handlers (list)))
(pg-do-startup connection dbname user password)))
;; e.g. "host=localhost port=5432 dbname=mydb connect_timeout=10"
;; see https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-PARAMKEYWORDS
(defun pg-connect/string (connection-string)
"Connect to PostgreSQL with parameters specified by CONNECTION-STRING.
A connection string is of the form `host=localhost port=5432
dbname=mydb'. We do not support all the parameter keywords
supported by libpq, such as those which specify particular
aspects of the TCP connection to PostgreSQL (e.g.
keepalives_interval). The supported keywords are host, hostaddr,
port, dbname, user, password, sslmode (partial support),
connect_timeout, client_encoding and application_name."
(let* ((components (split-string connection-string "[ \t]" t))
(params (cl-loop
for c in components
for param-val = (split-string c "=" t "\s")
unless (eql 2 (length param-val))
do (error "Invalid connection string component %s" c)
collect (cons (cl-first param-val) (cl-second param-val))))
(host (or (cdr (assoc "host" params))
(cdr (assoc "hostaddr" params))
(getenv "PGHOST")
(getenv "PGHOSTADDR")
"localhost"))
(port (or (cdr (assoc "port" params))
(getenv "PGPORT")
5432))
(dbname (or (cdr (assoc "dbname" params))
(getenv "PGDATABASE")
(error "Database name not specified in connection string or PGDATABASE environment variable")))
(user (or (cdr (assoc "user" params))
(getenv "PGUSER")
(error "User not specified in connection string or PGUSER environment variable")))
(password (or (cdr (assoc "password" params))
(getenv "PGPASSWORD")))
(sslmode (or (cdr (assoc "sslmode" params))
(getenv "PGSSLMODE")))
(tls (cond ((string= sslmode "disable") nil)
((string= sslmode "allow") t)
((string= sslmode "prefer") t)
((string= sslmode "require") t)
((string= sslmode "verify-ca")
(error "verify-ca sslmode not implemented"))
((string= sslmode "verify-full")
(error "verify-full sslmode not implemented"))
((cdr (assoc "requiressl" params)) t)
(t nil)))
(connect-timeout-str (cadr (assoc "connect_timeout" params)))
(connect-timeout (and connect-timeout-str (cl-parse-integer connect-timeout-str)))
(pg-connect-timeout (or connect-timeout pg-connect-timeout))
;; This "read_timeout" is a non-standard extension that we implement
(read-timeout-str (cadr (assoc "read_timeout" params)))
(read-timeout (and read-timeout-str (cl-parse-integer read-timeout-str)))
(pg-read-timeout (or read-timeout pg-read-timeout))
(pg-application-name (or (cdr (assoc "application_name" params))
pg-application-name))
(client-encoding-str (cadr (assoc "client_encoding" params)))
(client-encoding (and client-encoding-str
(pg-normalize-encoding-name client-encoding-str))))
;; TODO: should handle sslcert, sslkey variables
;;
;; Some of the parameters are taken from our local variable bindings, but for other parameters we
;; need to set them explicitly in the pgcon object.
(let ((con (pg-connect dbname user password host port tls)))
(when client-encoding
(setf (pgcon-client-encoding con) client-encoding))
con)))
(defun pg-parse-url (url)
"Adaptation of function `url-generic-parse-url' that does not downcase
the host component of the URL."
(if (null url)
(url-parse-make-urlobj)
(with-temp-buffer
;; Don't let those temp-buffer modifications accidentally
;; deactivate the mark of the current-buffer.
(let ((deactivate-mark nil))
(set-syntax-table url-parse-syntax-table)
(erase-buffer)
(insert url)
(goto-char (point-min))
(let ((save-pos (point))
scheme user pass host port file fragment full
(inhibit-read-only t))
;; 3.1. Scheme
;; This is nil for a URI that is not fully specified.
(when (looking-at "\\([a-zA-Z][-a-zA-Z0-9+.]*\\):")
(goto-char (match-end 0))
(setq save-pos (point))
(setq scheme (downcase (match-string 1))))
;; 3.2. Authority
(when (looking-at "//")
(setq full t)
(forward-char 2)
(setq save-pos (point))
(skip-chars-forward "^/?#")
(setq host (buffer-substring save-pos (point)))
;; 3.2.1 User Information
(if (string-match "^\\([^@]+\\)@" host)
(setq user (match-string 1 host)
host (substring host (match-end 0))))
(if (and user (string-match "\\`\\([^:]*\\):\\(.*\\)" user))
(setq pass (match-string 2 user)
user (match-string 1 user)))
(cond
;; IPv6 literal address.
((string-match "^\\(\\[[^]]+\\]\\)\\(?::\\([0-9]*\\)\\)?$" host)
(setq port (match-string 2 host)
host (match-string 1 host)))
;; Registered name or IPv4 address.
((string-match ":\\([0-9]*\\)$" host)
(setq port (match-string 1 host)
host (substring host 0 (match-beginning 0)))))
(cond ((equal port "")
(setq port nil))
(port
(setq port (cl-parse-integer port)))))
;; Now point is on the / ? or # which terminates the
;; authority, or at the end of the URI, or (if there is no
;; authority) at the beginning of the absolute path.
(setq save-pos (point))
(if (string= "data" scheme)
;; For the "data" URI scheme, all the rest is the FILE.
(setq file (buffer-substring save-pos (point-max)))
;; For hysterical raisins, our data structure returns the
;; path and query components together in one slot.
;; 3.3. Path
(skip-chars-forward "^?#")
;; 3.4. Query
(when (looking-at "\\?")
(skip-chars-forward "^#"))
(setq file (buffer-substring save-pos (point)))
;; 3.5 Fragment
(when (looking-at "#")
(let ((opoint (point)))
(forward-char 1)
(setq fragment (buffer-substring (point) (point-max)))
(delete-region opoint (point-max)))))
(if (and host (string-match "%[0-9][0-9]" host))
(setq host (url-unhex-string host)))
(url-parse-make-urlobj scheme user pass host port file
fragment nil full))))))
;; postgresql://[userspec@][hostspec][/dbname][?paramspec]
;; Examples:
;; - postgresql://other@localhost/otherdb?connect_timeout=10&application_name=myapp&ssl=true
;; - postgresql://%2Fvar%2Flib%2Fpostgresql/dbname
;;
;; https://www.postgresql.org/docs/current/libpq-connect.html
(defun pg-connect/uri (uri)
"Connect to PostgreSQL with parameters specified by URI.
A connection URI is of the form
`postgresql://[userspec@][hostspec][/dbname][?paramspec]'.
`userspec' is of the form username:password. If hostspec is a
string representing a local path (e.g.
`%2Fvar%2Flib%2Fpostgresql' with percent-encoding) then it is
interpreted as a Unix pathname used for a local Unix domain
connection. We do not support all the paramspec keywords
supported by libpq, such as those which specify particular
aspects of the TCP connection to PostgreSQL (e.g.
keepalives_interval). The supported paramspec keywords are
sslmode (partial support) and application_name."
(let* ((parsed (pg-parse-url uri))
(scheme (url-type parsed)))
(unless (or (string= "postgres" scheme)
(string= "postgresql" scheme))
(let ((msg (format "Invalid protocol %s in connection URI" scheme)))
(signal 'pg-error (list msg))))
;; FIXME unfortunately the url-host is being downcased by url-generic-parse-url, which is
;; incorrect when the hostname is specifying a local path.
(let* ((host (url-unhex-string (url-host parsed)))
(user (or (url-user parsed)
(getenv "PGUSER")))
(password (or (url-password parsed)
(getenv "PGPASSWORD")))
(port (or (url-portspec parsed)
(getenv "PGPORT")
5432))
(path-query (url-path-and-query parsed))
(dbname (or (and (car path-query)
;; ignore the "/" prefix
(substring (car path-query) 1))
(getenv "PGDATABASE")
(signal 'pg-error '("Missing database name in connection URI"))))
(params (cdr path-query))
;; this is returning a list of lists, not an alist
(params (and params (url-parse-query-string params)))
(sslmode (or (cadr (assoc "sslmode" params))
(getenv "PGSSLMODE")))
(tls (cond ((string= sslmode "disable") nil)
((string= sslmode "allow") t)
((string= sslmode "prefer") t)
((string= sslmode "require") t)
((string= sslmode "verify-ca")
(signal 'pg-error '("verify-ca sslmode not implemented")))
((string= sslmode "verify-full")
(signal 'pg-error '("verify-full sslmode not implemented")))
((cdr (assoc "requiressl" params)) t)
(t nil)))
;; Should be a decimal integer designating a number of seconds
(connect-timeout-str (cadr (assoc "connect_timeout" params)))
(connect-timeout (and connect-timeout-str (cl-parse-integer connect-timeout-str)))
(pg-connect-timeout (or connect-timeout pg-connect-timeout))
;; This "read_timeout" is a non-standard extension that we implement
(read-timeout-str (cadr (assoc "read_timeout" params)))
(read-timeout (and read-timeout-str (cl-parse-integer read-timeout-str)))
(pg-read-timeout (or read-timeout pg-read-timeout))
(pg-application-name (or (cadr (assoc "application_name" params))
pg-application-name))
(client-encoding-str (cadr (assoc "client_encoding" params)))
(client-encoding (and client-encoding-str
(pg-normalize-encoding-name client-encoding-str))))
;; If the host is empty or looks like an absolute pathname, connect over Unix-domain socket.
(let ((con (if (or (zerop (length host))
(eq ?/ (aref host 0)))
(pg-connect-local host dbname user password)
(pg-connect dbname user password host port tls))))
(when client-encoding
(setf (pgcon-client-encoding con) client-encoding))
con))))
;; Called from pg-parameter-change-functions when we receive a ParameterStatus
;; message of type name=value from the backend. If the status message concerns
;; the client encoding, update the value recorded in the connection.
(defun pg-handle-parameter-client-encoding (con name value)
(when (string= "client_encoding" name)
(let ((ce (pg-normalize-encoding-name value)))
(if ce
(setf (pgcon-client-encoding con) ce)
(let ((msg (format "Don't know the Emacs equivalent for client encoding %s" value)))
(signal 'pg-error (list msg)))))))
(defun pg-add-notification-handler (con handler)
"Register HANDLER for NotificationResponse messages on CON.
A handler takes two arguments: the channel and the payload. These
correspond to SQL-level NOTIFY channel, \\='payload\\='."
(with-current-buffer (process-buffer (pgcon-process con))
(push handler pgcon--notification-handlers)))
(cl-defun pg-exec (con &rest args)
"Execute the SQL command given by concatenating ARGS on database CON.
Return a result structure which can be decoded using `pg-result'."
(pg-connection-set-busy con t)
(let* ((sql (apply #'concat args))
(tuples (list))
(attributes (list))
(result (make-pgresult :connection con))
(ce (pgcon-client-encoding con))
(encoded (if ce (encode-coding-string sql ce t) sql)))
;; (message "pg-exec: %s" sql)
(when (pgcon-query-log con)
(with-current-buffer (pgcon-query-log con)
(insert sql "\n")))
(let ((len (length encoded)))
(when (> len (- (expt 2 32) 5))
(signal 'pg-user-error (list "Query is too large")))
(pg-send-char con ?Q)
(pg-send-uint con (+ 4 len 1) 4)
(pg-send-string con encoded))
(cl-loop for c = (pg-read-char con) do
;; (message "pg-exec message-type = %c" c)
(cl-case c
;; NoData
(?n
(pg-read-net-int con 4))