-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathob-sql-session.org
More file actions
559 lines (445 loc) · 15.8 KB
/
Copy pathob-sql-session.org
File metadata and controls
559 lines (445 loc) · 15.8 KB
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
#+TITLE: Ob-sql-session
[[https://github.com/flintforge/ob-sql-session/actions][file:https://github.com/flintforge/ob-sql-session/actions/workflows/CI.yml/badge.svg]]
#+author: Phil Estival pe@7d.nz
# date : [2024-05-29 Wed]
#+License: GPL3
https://github.com/flintforge/ob-sql-session
** Org Babel functions for SQL, with session
*** Overview
:PROPERTIES:
:header-args: sql-session :engine postgres :dbhost localhost :database test :session PG :results table
:END:
#+begin_example
:PROPERTIES:
:header-args: sql-session :engine postgres :dbhost localhost :database test :dbuser (getenv "pguser")
:END:
#+end_example
In the absence of session,
the scope of setting this search path is
limited to one query
#+begin_example
,#+begin_src sql
set search_path to test, public;
show search_path;
,#+end_src
#+end_example
| SET |
|--------------|
| search_path |
| test, public |
Then it gets back to default
#+begin_example
,#+begin_src sql
show search_path;
,#+end_src
#+end_example
| search_path |
|-----------------|
| "$user", public |
While of course it will be kept inside a continuing session
#+begin_example
,#+begin_src sql-session :var path="test" :session PG :results table
set search_path to $path, public;
show search_path;
,#+end_src
#+end_example
| SET |
| search_path |
|--------------|
| test, public |
#+begin_example
,#+begin_src sql-session :session PG :results table
show search_path;
,#+end_src
#+end_example
| search_path |
|--------------|
| test, public |
** Explanations
=Org/ob-sql.el= does not provide a session mode because
source blocks are passed as an input file along the
connexion arguments without any terminal, which is fast
(see for instance man:psql, option -c).
=ob-sql-mode.el= was proposed as an alternative. It
relies on =sql-session= to open a client connection, then
performs a simple =sql-redirect= as execution of the sql
source block, before cleaning the prompt.
But more interesting is this comment in comint:
file:/usr/local/share/emacs/29.3/lisp/comint.el.gz::3570
Which brings several remarks:
- Session mode is only required when keeping a state.
- =sql-redirect= can perfectly handle many batches of
commands at once, but relies on =accept-process-output=
which is not the best way to handle redirections
through comint since it can get clunky when not
managing bursts of outputs or termination or longer
execution times correctly. The problem comes from
properly handling =accept-process-output= and
termination.
- Relying on the detection of the prompt should not be
necessary as long as comint can tell where last
output began.
- What happen if we rely only on the prompt to
detect a command termination but for batches of
commands with buffered output and a prompt showing up
on each command? Then there's no way to detect when a
batch finishes, except with some sort of IPC and by
giving the job enough time to complete. Or adding a
special command in the end.
We have two situations related to the client:
1) It returns a message from a command.
2) It is silent on the output, and we don't want to
echo every input, example: a =drop= on sqlite, a
=\set= or quiet mode on psql.
We can conclude there are two solutions to run a SQL
batch:
1) Split the batch, run commands one by one, Identify
silent commands (starting with =\= for =psql=), no
semi-column at the end...), keep the execution on
hold while the next command did not output, and add
short frames for commands to complete buffered
output.
2) add a termination command to the batch: an echo
command for instance, that stays in the client,
and not given to the db.
Here we opted for solution 2).
One drawback is that some client will report an
error as related to the entire block of
commands, and not narrowed to a given line.
For such a case, where spotting precisely where
an error occurs, it's always possible to switch
into the interactive buffer and provide the
command one line at a time.
The following is reported to work on emacs 27 to 30,
org-mode 9.6 and 9.7
DB tested:
- Sqlite
- Postgres
** Comparison with the build-in ob-sql
=ob-sql-session= exists for session support, which is in the TODOs list
of =ob-sql=.
- =ob-sql= command execution relies on =org-babel-eval=
(→ process-file → call-process).
- =ob-sql-session= runs an inferior process (in which
=sqli-interactive-mode= can be activated when needed). The process
output is filtered (e.g. results and prompts). When a session is
demanded, this shell stays open for further commands and can keep a
state (typically, when given special SQL commands).
|-----------+----------------------------+----------------------------|
| | ob-sql | ob-sql-session |
|-----------+----------------------------+----------------------------|
| Feat. | - cmdline | - support for sessions |
| | - colnames as header arg | - optionnal colnames |
|-----------+----------------------------+----------------------------|
| TODO | | |
|-----------+----------------------------+----------------------------|
| | - support for sessions | - colnames as header arg |
| | - support for more engines | - support for more engines |
|-----------+----------------------------+----------------------------|
| engines | | |
| supported | | |
|-----------+----------------------------+----------------------------|
| | - mysql | - Postgresql |
| | - dbi | - sqlite |
| | - mssql | |
| | - sqsh | |
| | - postgresql | |
| | - oracle | |
| | - vertica | |
| | - saphana | |
|-----------+----------------------------+----------------------------|
- =ob-sql= defines =org-babel-sql-dbstring-[engine]=
to be provided on a shell command line.
- =ob-sql-session=, likewise, has to define
- a connection string,
- the prompt,
- and the terminal command prefix
for a every supported SQL client shell (or "engines")
- requires sql.el.
With the above defined, it should be compatible
with most database of the sql.el's zoo. maybe.
- adapts =sql-connect= of =sql.el= by declaring a function
=ob-sql-connect=, in order to prompt only
for missing connection
parameters.
** Comparison with ob-sql-mode
ob-sql-mode :
- is simple : forward the sql source through `sql-redirect'
- has test suite
- but gives clunky output
- no =:results= table
- does not handle special sql engine client commands
- prompt again for connection parameters when restarting a session
ob-sql-session :
- handle large results
- results as tables
- header variables (=:var=)
- accept special commands given to a specific sql shell
- memorize login parameters
- prompt for interactive authentication only if there is a
parameter left blank
- can provide password =with-environment-variables=
- provide some more tests
** usage
#+begin_example
,#+begin_src elisp
(load-file "./ob-sql-session.el")
,#+end_src
#+end_example
Skip confirmations
#+begin_example
,#+begin_src elisp
(defun do-org-confirm-babel-evaluations (lang body)
(not
(or
(string= lang "elisp")
(string= lang "sql-session"))))
(setq org-confirm-babel-evaluate 'do-org-confirm-babel-evaluations)
,#+end_src
#+end_example
=sql-comint-sqlite= in =sql.el= needs to accept nil
database in order to run sqlite in memory (=ob-sqlite=
has +no+ session support +either and requires a database+
(/commit 68aa43885/ merged in org 9.7: ob-sqlite: Use a transient in-memory database by default).
Test it:
#+begin_example
,#+begin_src sql-session :engine sqlite :results table :database test.db
.headers on
drop table test;
create table test(a,b);
insert into test values ("sqlite",sqlite_version());
insert into test values (date(),time());
select * from test;
,#+end_src
#+end_example
#+RESULTS:
| a | b |
| sqlite | 3.40.1 |
| 2024-06-24 | 11:57:56 |
Displaying header.
#+begin_example
,#+begin_src sql-session :engine sqlite :database test.db :results table
.headers on
--create table test(x,y);
delete from test;
insert into test values ("sqlite",sqlite_version());
insert into test values (date(),time());
select * from test;
,#+end_src
#+end_example
| one | two |
| sqlite | 3.40.1 |
| 2024-06-05 | 14:42:01 |
#+begin_example
,#+begin_src sql-session :engine sqlite :results table :database test.db :session A
--delete from test;
insert into test values ('sqlite','3.40');
insert into test values (1,2);
select * from test;
,#+end_src
#+end_example
| sqlite | 3.40 |
| 1 | 2 |
#+begin_example
,#+begin_src sql-session :engine sqlite
--drop table test;
create table test(one text, two int);
select format("sqlite %s",sqlite_version()), date(), time();
,#+end_src
#+end_example
: sqlite 3.40.1|2024-06-05|14:42:03
Returning error
#+begin_example
,#+begin_src sql-session :engine sqlite :database test.db
create table test(a, b);
drop table test;
,#+end_src
#+end_example
: Parse error: table test already exists
: create table test(a, b); drop table test;
: ^--- error here
#+begin_example
,#+begin_src sql-session :engine sqlite :database test.db :results output
drop table test;
create table test(one varchar(10), two smallint);
insert into test values('hello', 1);
insert into test values('world', 2);
select * from test;
,#+end_src
#+end_example
:
: hello|1
: world|2
** In order to run sqlite in memory (for older versions of progmodes/sql.el)
=sql-database= can be /nil/ and no option given to =sql-comint-sqlite=
#+begin_src elisp
(defun sql-comint-sqlite (product &optional options buf-name)
"Create comint buffer and connect to SQLite."
;; Put all parameters to the program (if defined) in a list and call
;; make-comint.
(let ((params
(append options
(if (and sql-database ;; allows connection to in-memory database.
(not (string-empty-p sql-database)))
`(,(expand-file-name sql-database))))))
(sql-comint product params buf-name)))
#+end_src
#+begin_src patch
modified lisp/progmodes/sql.el
@@ -5061,14 +5061,15 @@ sql-sqlite
(interactive "P")
(sql-product-interactive 'sqlite buffer))
-(defun sql-comint-sqlite (product options &optional buf-name)
+(defun sql-comint-sqlite (product &optional options buf-name)
"Create comint buffer and connect to SQLite."
;; Put all parameters to the program (if defined) in a list and call
;; make-comint.
(let ((params
(append options
- (if (not (string= "" sql-database))
- `(,(expand-file-name sql-database))))))
+ (if (and sql-database
+ (not (string= "" sql-database)))
+ `(,(expand-file-name sql-database))))))
(sql-comint product params buf-name)))
#+end_src
Test it:
#+begin_example
,#+begin_src sql-session :engine sqlite
create table test(an int, two char);
SELECT *
FROM sqlite_schema;
select format("sqlite %s",sqlite_version()), date(), time();
,#+end_src
#+end_example
:
: table|test|test|2|CREATE TABLE test(an int, two char)
: sqlite 3.40.1|2024-06-05|01:46:55
On a session
#+begin_example
,#+begin_src sql-session :engine sqlite :session A
create table test(an int, two char);
,#+end_src
#+end_example
#+begin_example
,#+begin_src sql-session :engine sqlite :session A
select format("sqlite %s",sqlite_version()), date(), time();
,#+end_src
#+end_example
*** Once a session is opened
#+begin_example
,#+begin_src sql-session :session PG :engine postgres :dbuser user :dbpassword password :dbhost host :databse db
select current_user
,#+end_src
#+end_example
The connexion parameters may be discarded when recalling an opened session
#+begin_example
,#+begin_src sql-session :session PG
select current_user
,#+end_src
#+end_example
They'll be of course needed if the commands and queries are to be run
independently and need to be able to initiate the connexion.
** Test it on postgres
:PROPERTIES:
:header-args: sql-session :engine postgres :database test :results table
:END:
#+begin_example
,#+begin_src sql-session :dbhost ""
select inet_client_addr(); -- no host=socket, empty result
select localtime(0);
select current_date, 'hello world';
,#+end_src
#+end_example
| inet_client_addr | |
| localtime | |
| 17:09:35 | |
| current_date | ?column? |
| 2024-06-05 | hello world |
Session starts
#+begin_example
,#+begin_src sql-session :session A
select inet_client_addr();
select localtime(0), current_date;
,#+end_src
#+end_example
| inet_client_addr | |
| localtime | current_date |
| 17:10:16 | 2024-06-05 |
Error handling
#+begin_example
,#+begin_src sql-session :session A
select current_date, 1;
select err;
select 'ok';
,#+end_src
#+end_example
| current_date | ?column? |
| 2024-06-05 | 1 |
| ERROR: column "err" does not exist | |
| LINE 1: select err; | |
| ^ | |
Stored procedure
#+begin_src sql-session :session A
create or replace function test(valid boolean) returns text as
$$
begin
if valid then return true;
else
RAISE EXCEPTION '%', 'woops';
end if;
end
$$ stable language plpgsql;
select test(true);
select test(false);
#+end_src
| CREATE FUNCTION |
| test |
| true |
| ERROR: woops |
| CONTEXT: PL/pgSQL function test(boolean) line 4 at RAISE |
** Variables
#+begin_example
,#+begin_src sql-session :engine sqlite :var x="3.0"
select 1/$x;
,#+end_src
#+end_example
: 0.333333333333333
Variables will also be substitued in litteral strings (eg '$var').
** Test against large output
#+begin_src sql :engine postgres :database test :var x=33
drop sequence serial2;
Create sequence serial2 start $x;
select nextval('serial2'),array(select generate_series(0, 200)) from generate_series(0, 250);
#+end_src
- [X] pass
** [1/3] TODO >
- [X] Provide password [[file:/usr/share/emacs/28.2/lisp/env.el.gz::defmacro with-environment-variables][with-environment-variables]]
+ additionnal enviro if needed
- [ ] port number please
- [ ] merge into ob-sql
** Publishing an org file on github
Turn code blocks to example
#+name: src->example
#+begin_src elisp
(save-excursion
(replace-regexp "^#\\+RESULTS:\n" "" nil nil nil t)
(goto-char (point-max))
(replace-regexp "\\(\\#\\+begin_src sql.*$\\)"
"#+begin_example\n,\\1" nil nil nil t)
(goto-char (point-max))
(replace-regexp "\\(\\#\\+end_src\s*$\\)"
",\\1\n#+end_example" nil nil nil t))
#+end_src
or vice-versa
#+name: example->src
#+begin_src elisp
(save-excursion
(replace-regexp "#\\+begin_example\n\\(,#\\+begin_src sql.*$\\)"
"\\1" nil nil nil t)
(goto-char (point-max))
(replace-regexp "\\(,#\\+end_src\s*\n\\)#\\+end_example"
"\\1" nil nil nil t))
#+end_src
#+call: src->example()
#+call: example->src()