sqlite-bro 0.13.1__tar.gz → 0.14.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {sqlite_bro-0.13.1 → sqlite_bro-0.14.0}/PKG-INFO +12 -9
- {sqlite_bro-0.13.1 → sqlite_bro-0.14.0}/README.rst +7 -5
- {sqlite_bro-0.13.1 → sqlite_bro-0.14.0}/pyproject.toml +2 -2
- sqlite_bro-0.14.0/sqlite_bro/__init__.py +1 -0
- {sqlite_bro-0.13.1 → sqlite_bro-0.14.0}/sqlite_bro/sqlite_bro.py +465 -67
- sqlite_bro-0.14.0/sqlite_bro/tests/test_general_duckdb.py +150 -0
- sqlite_bro-0.14.0/sqlite_bro/tests/test_general_no_gui.py +68 -0
- sqlite_bro-0.13.1/sqlite_bro/__init__.py +0 -1
- {sqlite_bro-0.13.1 → sqlite_bro-0.14.0}/LICENSE +0 -0
- {sqlite_bro-0.13.1 → sqlite_bro-0.14.0}/sqlite_bro/tests/__init__.py +0 -0
- {sqlite_bro-0.13.1 → sqlite_bro-0.14.0}/sqlite_bro/tests/test_general.py +0 -0
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: sqlite_bro
|
|
3
|
-
Version: 0.
|
|
4
|
-
Summary: a graphic SQLite Client in 1 Python file
|
|
5
|
-
Keywords: sqlite,gui,ttk,sql
|
|
3
|
+
Version: 0.14.0
|
|
4
|
+
Summary: a graphic SQLite and DuckDB Client in 1 Python file
|
|
5
|
+
Keywords: sqlite,duckdb,gui,ttk,sql
|
|
6
6
|
Author: stonebig
|
|
7
7
|
Requires-Python: >=3.3
|
|
8
8
|
Description-Content-Type: text/x-rst
|
|
@@ -17,20 +17,23 @@ Classifier: Programming Language :: Python :: 3
|
|
|
17
17
|
Classifier: Development Status :: 5 - Production/Stable
|
|
18
18
|
Classifier: Topic :: Scientific/Engineering
|
|
19
19
|
Classifier: Topic :: Software Development :: Widget Sets
|
|
20
|
+
License-File: LICENSE
|
|
20
21
|
Project-URL: Documentation, https://github.com/stonebig/sqlite_bro/README.rst
|
|
21
22
|
Project-URL: Source, https://github.com/stonebig/sqlite_bro
|
|
22
23
|
|
|
23
|
-
sqlite_bro : a graphic SQLite browser in 1 Python file
|
|
24
|
-
|
|
24
|
+
sqlite_bro : a graphic SQLite and DuckDB browser in 1 Python file
|
|
25
|
+
=================================================================
|
|
25
26
|
|
|
26
|
-
sqlite_bro is a tool to browse SQLite
|
|
27
|
-
any basic python installation.
|
|
27
|
+
sqlite_bro is a tool to browse SQLite (and optionally DuckDB)
|
|
28
|
+
databases with any basic python installation.
|
|
28
29
|
|
|
29
30
|
|
|
30
31
|
Features
|
|
31
32
|
--------
|
|
32
33
|
|
|
33
|
-
* Tabular browsing of a SQLite database
|
|
34
|
+
* Tabular browsing of a SQLite or DuckDB database
|
|
35
|
+
|
|
36
|
+
* Optional DuckDB engine : open or create a '.duckdb' file (needs 'pip install duckdb')
|
|
34
37
|
|
|
35
38
|
* Import/Export of .csv files with auto-detection
|
|
36
39
|
|
|
@@ -1,14 +1,16 @@
|
|
|
1
|
-
sqlite_bro : a graphic SQLite browser in 1 Python file
|
|
2
|
-
|
|
1
|
+
sqlite_bro : a graphic SQLite and DuckDB browser in 1 Python file
|
|
2
|
+
=================================================================
|
|
3
3
|
|
|
4
|
-
sqlite_bro is a tool to browse SQLite
|
|
5
|
-
any basic python installation.
|
|
4
|
+
sqlite_bro is a tool to browse SQLite (and optionally DuckDB)
|
|
5
|
+
databases with any basic python installation.
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
Features
|
|
9
9
|
--------
|
|
10
10
|
|
|
11
|
-
* Tabular browsing of a SQLite database
|
|
11
|
+
* Tabular browsing of a SQLite or DuckDB database
|
|
12
|
+
|
|
13
|
+
* Optional DuckDB engine : open or create a '.duckdb' file (needs 'pip install duckdb')
|
|
12
14
|
|
|
13
15
|
* Import/Export of .csv files with auto-detection
|
|
14
16
|
|
|
@@ -25,8 +25,8 @@ classifiers=[
|
|
|
25
25
|
'Topic :: Software Development :: Widget Sets',
|
|
26
26
|
]
|
|
27
27
|
dynamic = ["version",]
|
|
28
|
-
description="a graphic SQLite Client in 1 Python file"
|
|
29
|
-
keywords = ["sqlite", "gui", "ttk", "sql"]
|
|
28
|
+
description="a graphic SQLite and DuckDB Client in 1 Python file"
|
|
29
|
+
keywords = ["sqlite", "duckdb", "gui", "ttk", "sql"]
|
|
30
30
|
|
|
31
31
|
[project.urls]
|
|
32
32
|
Documentation = "https://github.com/stonebig/sqlite_bro/README.rst"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = '0.14.0'
|
|
@@ -8,6 +8,15 @@ except ImportError:
|
|
|
8
8
|
pass # Python<3.2
|
|
9
9
|
|
|
10
10
|
import sqlite3 as sqlite
|
|
11
|
+
|
|
12
|
+
try: # optional second engine : DuckDB (pip install duckdb)
|
|
13
|
+
import duckdb
|
|
14
|
+
|
|
15
|
+
db_errors = (sqlite.Error, duckdb.Error)
|
|
16
|
+
except ImportError:
|
|
17
|
+
duckdb = None
|
|
18
|
+
db_errors = (sqlite.Error,)
|
|
19
|
+
|
|
11
20
|
import sys
|
|
12
21
|
import os
|
|
13
22
|
import locale
|
|
@@ -35,15 +44,74 @@ except ImportError: # or we are still Python2.7
|
|
|
35
44
|
tipwindow = None
|
|
36
45
|
|
|
37
46
|
# starting Python-3.13, PEP 667 forces us to us a specific dictionnary pydef_locals instead of locals()
|
|
38
|
-
pydef_locals ={}
|
|
47
|
+
pydef_locals ={}
|
|
48
|
+
|
|
49
|
+
WELCOME_DUCKDB = """-- DuckDB Memo (Demo = click on green "->" icon)
|
|
50
|
+
-- (this database uses the DuckDB engine : SQL is stricter than SQLite)
|
|
51
|
+
\n-- to CREATE tables : column types are mandatory
|
|
52
|
+
DROP TABLE IF EXISTS item; DROP TABLE IF EXISTS part;
|
|
53
|
+
CREATE TABLE item (ItemNo TEXT, Description TEXT, Kg REAL, PRIMARY KEY (ItemNo));
|
|
54
|
+
CREATE TABLE part(ParentNo TEXT, ChildNo TEXT, Description TEXT, Qty_per REAL);
|
|
55
|
+
\n-- to CREATE an index :
|
|
56
|
+
DROP INDEX IF EXISTS parts_id1;
|
|
57
|
+
CREATE INDEX parts_id1 ON part(ParentNo, ChildNo);
|
|
58
|
+
\n-- to CREATE a view 'v1':
|
|
59
|
+
DROP VIEW IF EXISTS v1;
|
|
60
|
+
CREATE VIEW v1 as select * from item inner join part as p ON ItemNo=p.ParentNo;
|
|
61
|
+
\n-- to INSERT datas : string literals use single quotes
|
|
62
|
+
INSERT INTO item values('T','Ford',1000);
|
|
63
|
+
INSERT INTO item select 'A','Merced',1250 union all select 'W','Wheel',9 ;
|
|
64
|
+
INSERT INTO part select ItemNo,'W','needed',Kg/250 from item where Kg>250;
|
|
65
|
+
\n-- to CREATE a Python embedded function (needs numpy installed),
|
|
66
|
+
-- enclose it by "pydef" and ";" :
|
|
67
|
+
pydef py_hello():
|
|
68
|
+
"hello world"
|
|
69
|
+
return ("Hello, World !");
|
|
70
|
+
pydef py_fib(n):
|
|
71
|
+
"fibonacci : example with function call (may only be internal) "
|
|
72
|
+
fib = lambda n: n if n < 2 else fib(n-1) + fib(n-2)
|
|
73
|
+
return("%s" % fib(n*1));
|
|
74
|
+
pydef py_fib_typed(n: int) -> int:
|
|
75
|
+
"type-annotated pydef : registered natively (faster, typed) "
|
|
76
|
+
fib = lambda n: n if n < 2 else fib(n-1) + fib(n-2)
|
|
77
|
+
return fib(n);
|
|
78
|
+
\n-- to USE a python embedded function :
|
|
79
|
+
select py_hello(), py_fib(6) as fibonacci, py_fib_typed(7) as fib_typed,
|
|
80
|
+
version() as duckdb_version;
|
|
81
|
+
\n-- some DuckDB goodies :
|
|
82
|
+
DESCRIBE item;
|
|
83
|
+
SUMMARIZE item;
|
|
84
|
+
select * from duckdb_tables();
|
|
85
|
+
\n-- to use COMMIT and ROLLBACK (DuckDB does not support SAVEPOINT) :
|
|
86
|
+
BEGIN TRANSACTION;
|
|
87
|
+
UPDATE item SET Kg = Kg + 1;
|
|
88
|
+
COMMIT;
|
|
89
|
+
BEGIN TRANSACTION;
|
|
90
|
+
UPDATE item SET Kg = 0;
|
|
91
|
+
select Kg, Description from Item;
|
|
92
|
+
ROLLBACK;
|
|
93
|
+
select Kg, Description from Item;
|
|
94
|
+
\n-- '.' commands understood (same as in SQLite mode, see sqlite demo) :
|
|
95
|
+
.headers on
|
|
96
|
+
.separator ;
|
|
97
|
+
.once --bom '~this_file_of result.txt'
|
|
98
|
+
select ItemNo, Description from item order by ItemNo desc;
|
|
99
|
+
.import '~this_file_of result.txt' in_this_table
|
|
100
|
+
.cd ~
|
|
101
|
+
ATTACH 'test.duckdb' as toto;
|
|
102
|
+
DROP TABLE IF EXISTS toto.new_item;
|
|
103
|
+
CREATE TABLE toto.new_item as select * from "main"."item";
|
|
104
|
+
.dump
|
|
105
|
+
"""
|
|
106
|
+
|
|
39
107
|
|
|
40
108
|
class App:
|
|
41
109
|
"""the GUI graphic application"""
|
|
42
110
|
|
|
43
111
|
def __init__(self, use_gui=True):
|
|
44
112
|
"""create a tkk graphic interface with a main window tk_win"""
|
|
45
|
-
self.__version__ = "0.
|
|
46
|
-
self._title = "of
|
|
113
|
+
self.__version__ = "0.14.0"
|
|
114
|
+
self._title = "of 2026-07-12 : 'Quack me up !'"
|
|
47
115
|
self.conn = None # Baresql database object
|
|
48
116
|
self.database_file = ""
|
|
49
117
|
self.initialdir = "."
|
|
@@ -57,7 +125,9 @@ class App:
|
|
|
57
125
|
|
|
58
126
|
if self.use_gui:
|
|
59
127
|
self.tk_win.title(
|
|
60
|
-
"A graphic SQLite Client in 1 Python file ("
|
|
128
|
+
"A graphic SQLite and DuckDB Client in 1 Python file ("
|
|
129
|
+
+ self.__version__
|
|
130
|
+
+ ")"
|
|
61
131
|
)
|
|
62
132
|
self.tk_win.option_add("*tearOff", FALSE) # hint of tk documentation
|
|
63
133
|
self.tk_win.minsize(600, 200) # minimal size
|
|
@@ -107,6 +177,9 @@ class App:
|
|
|
107
177
|
self.default_separator = ","
|
|
108
178
|
self.current_directory = os.getcwd()
|
|
109
179
|
|
|
180
|
+
# show the DuckDB welcome demo only once per session
|
|
181
|
+
self.duckdb_demo_shown = False
|
|
182
|
+
|
|
110
183
|
# Initiate Output State
|
|
111
184
|
self.once_mode = False
|
|
112
185
|
self.encode_in = "utf-8"
|
|
@@ -131,6 +204,11 @@ class App:
|
|
|
131
204
|
self.menu.add_command(
|
|
132
205
|
label="New In-Memory Database", command=lambda: self.new_db(":memory:")
|
|
133
206
|
)
|
|
207
|
+
if duckdb is not None:
|
|
208
|
+
self.menu.add_command(
|
|
209
|
+
label="New In-Memory DuckDB Database",
|
|
210
|
+
command=lambda: self.new_db(":memory:", engine="duckdb"),
|
|
211
|
+
)
|
|
134
212
|
self.menu.add_command(label="Open Database ...", command=self.open_db)
|
|
135
213
|
self.menu.add_command(
|
|
136
214
|
label="Open Database ...(legacy auto-commit)",
|
|
@@ -152,7 +230,7 @@ class App:
|
|
|
152
230
|
label="about",
|
|
153
231
|
command=lambda: messagebox.showinfo(
|
|
154
232
|
message="""
|
|
155
|
-
\nSQLite_bro : a graphic SQLite Client in 1 Python file
|
|
233
|
+
\nSQLite_bro : a graphic SQLite and DuckDB Client in 1 Python file
|
|
156
234
|
\nVersion """
|
|
157
235
|
+ self.__version__
|
|
158
236
|
+ " "
|
|
@@ -212,14 +290,19 @@ class App:
|
|
|
212
290
|
if os.path.isfile(proposal):
|
|
213
291
|
self.initialdir = os.path.dirname(proposal)
|
|
214
292
|
|
|
215
|
-
def new_db(self, filename=""):
|
|
216
|
-
"""create a new database"""
|
|
293
|
+
def new_db(self, filename="", engine=None):
|
|
294
|
+
"""create a new database (engine guessed from extension if not given)"""
|
|
217
295
|
if filename == "":
|
|
218
296
|
filename = filedialog.asksaveasfilename(
|
|
219
297
|
initialdir=self.initialdir,
|
|
220
298
|
defaultextension=".db",
|
|
221
299
|
title="Define a new database name and location",
|
|
222
|
-
filetypes=[
|
|
300
|
+
filetypes=[
|
|
301
|
+
("default", "*.db"),
|
|
302
|
+
("duckdb", "*.duckdb"),
|
|
303
|
+
("other", "*.db*"),
|
|
304
|
+
("all", "*.*"),
|
|
305
|
+
],
|
|
223
306
|
)
|
|
224
307
|
if filename != "":
|
|
225
308
|
self.database_file = filename
|
|
@@ -231,23 +314,40 @@ class App:
|
|
|
231
314
|
title="Destroying",
|
|
232
315
|
):
|
|
233
316
|
os.remove(filename)
|
|
234
|
-
self.conn = Baresql(self.database_file)
|
|
317
|
+
self.conn = Baresql(self.database_file, engine=engine)
|
|
318
|
+
self.show_duckdb_demo()
|
|
235
319
|
self.actualize_db()
|
|
236
320
|
|
|
237
|
-
def open_db(self, filename="", isolation_level=None):
|
|
238
|
-
"""open an existing database"""
|
|
321
|
+
def open_db(self, filename="", isolation_level=None, engine=None):
|
|
322
|
+
"""open an existing database (engine guessed from extension if not given)"""
|
|
239
323
|
if filename == "":
|
|
240
324
|
filename = filedialog.askopenfilename(
|
|
241
325
|
initialdir=self.initialdir,
|
|
242
326
|
defaultextension=".db",
|
|
243
|
-
filetypes=[
|
|
327
|
+
filetypes=[
|
|
328
|
+
("default", "*.db"),
|
|
329
|
+
("duckdb", "*.duckdb"),
|
|
330
|
+
("other", "*.db*"),
|
|
331
|
+
("all", "*.*"),
|
|
332
|
+
],
|
|
244
333
|
)
|
|
245
334
|
if filename != "":
|
|
246
335
|
self.set_initialdir(filename)
|
|
247
336
|
self.database_file = filename
|
|
248
|
-
self.conn = Baresql(self.database_file)
|
|
337
|
+
self.conn = Baresql(self.database_file, engine=engine)
|
|
338
|
+
self.show_duckdb_demo()
|
|
249
339
|
self.actualize_db()
|
|
250
340
|
|
|
341
|
+
def show_duckdb_demo(self):
|
|
342
|
+
"""open the DuckDB welcome demo tab, once, when DuckDB is first used"""
|
|
343
|
+
if (
|
|
344
|
+
self.use_gui
|
|
345
|
+
and self.conn.engine == "duckdb"
|
|
346
|
+
and not self.duckdb_demo_shown
|
|
347
|
+
):
|
|
348
|
+
self.duckdb_demo_shown = True
|
|
349
|
+
self.n.new_query_tab("DuckDB Memo", WELCOME_DUCKDB)
|
|
350
|
+
|
|
251
351
|
def backup_db(self, filename="", isolation_level=None):
|
|
252
352
|
"""Backup the current database"""
|
|
253
353
|
if filename == "":
|
|
@@ -266,9 +366,7 @@ class App:
|
|
|
266
366
|
title="Destroying",
|
|
267
367
|
):
|
|
268
368
|
os.remove(filename)
|
|
269
|
-
|
|
270
|
-
self.conn.conn.backup(db_to)
|
|
271
|
-
db_to.close()
|
|
369
|
+
self.backup_main_to(filename)
|
|
272
370
|
self.actualize_db()
|
|
273
371
|
|
|
274
372
|
def restore_db(self, filename="", isolation_level=None):
|
|
@@ -280,10 +378,40 @@ class App:
|
|
|
280
378
|
filetypes=[("default", "*.db"), ("other", "*.db*"), ("all", "*.*")],
|
|
281
379
|
)
|
|
282
380
|
if filename != "":
|
|
381
|
+
self.restore_main_from(filename)
|
|
382
|
+
self.actualize_db()
|
|
383
|
+
|
|
384
|
+
def backup_main_to(self, filename):
|
|
385
|
+
"""backup the current 'main' database to filename, per engine"""
|
|
386
|
+
if self.conn.engine == "duckdb":
|
|
387
|
+
current = self.conn.execute("select current_database()").fetchall()[0][0]
|
|
388
|
+
self.conn.execute(
|
|
389
|
+
"ATTACH '%s' AS bro_backup" % filename.replace("'", "''")
|
|
390
|
+
)
|
|
391
|
+
self.conn.execute(
|
|
392
|
+
'COPY FROM DATABASE "%s" TO bro_backup' % current.replace('"', '""')
|
|
393
|
+
)
|
|
394
|
+
self.conn.execute("DETACH bro_backup")
|
|
395
|
+
else:
|
|
396
|
+
db_to = sqlite.connect(filename)
|
|
397
|
+
self.conn.conn.backup(db_to)
|
|
398
|
+
db_to.close()
|
|
399
|
+
|
|
400
|
+
def restore_main_from(self, filename):
|
|
401
|
+
"""restore the database in filename into current 'main', per engine"""
|
|
402
|
+
if self.conn.engine == "duckdb":
|
|
403
|
+
current = self.conn.execute("select current_database()").fetchall()[0][0]
|
|
404
|
+
self.conn.execute(
|
|
405
|
+
"ATTACH '%s' AS bro_restore (READ_ONLY)" % filename.replace("'", "''")
|
|
406
|
+
)
|
|
407
|
+
self.conn.execute(
|
|
408
|
+
'COPY FROM DATABASE bro_restore TO "%s"' % current.replace('"', '""')
|
|
409
|
+
)
|
|
410
|
+
self.conn.execute("DETACH bro_restore")
|
|
411
|
+
else:
|
|
283
412
|
db_from = sqlite.connect(filename)
|
|
284
413
|
db_from.backup(self.conn.conn)
|
|
285
414
|
db_from.close
|
|
286
|
-
self.actualize_db()
|
|
287
415
|
|
|
288
416
|
def load_script(self):
|
|
289
417
|
"""load a script file, ask validation of detected Python code"""
|
|
@@ -367,7 +495,12 @@ class App:
|
|
|
367
495
|
initialdir=self.initialdir,
|
|
368
496
|
defaultextension=".db",
|
|
369
497
|
title="Choose a database to attach ",
|
|
370
|
-
filetypes=[
|
|
498
|
+
filetypes=[
|
|
499
|
+
("default", "*.db"),
|
|
500
|
+
("duckdb", "*.duckdb"),
|
|
501
|
+
("other", "*.db*"),
|
|
502
|
+
("all", "*.*"),
|
|
503
|
+
],
|
|
371
504
|
)
|
|
372
505
|
if attach_as == "":
|
|
373
506
|
attach = os.path.basename(filename).split(".")[0]
|
|
@@ -379,7 +512,8 @@ class App:
|
|
|
379
512
|
attach, indice = att + "_" + str(indice), indice + 1
|
|
380
513
|
if filename != "":
|
|
381
514
|
self.set_initialdir(filename)
|
|
382
|
-
|
|
515
|
+
# double-quoted alias : accepted by both SQLite and DuckDB
|
|
516
|
+
attach_order = 'ATTACH DATABASE \'%s\' as "%s" ' % (filename, attach)
|
|
383
517
|
self.conn.execute(attach_order)
|
|
384
518
|
self.actualize_db()
|
|
385
519
|
|
|
@@ -401,10 +535,22 @@ class App:
|
|
|
401
535
|
# delete existing tree entries before re-creating them
|
|
402
536
|
for node in self.db_tree.get_children():
|
|
403
537
|
self.db_tree.delete(node)
|
|
404
|
-
# create top node
|
|
538
|
+
# create top node, showing the engine driving the main database
|
|
539
|
+
engine_label = (
|
|
540
|
+
"DuckDB" if getattr(self.conn, "engine", "sqlite") == "duckdb"
|
|
541
|
+
else "SQLite"
|
|
542
|
+
)
|
|
405
543
|
dbtext = os.path.basename(self.database_file)
|
|
544
|
+
self.tk_win.title(
|
|
545
|
+
"A graphic SQLite and DuckDB Client in 1 Python file (%s) - %s %s"
|
|
546
|
+
% (self.__version__, engine_label, self.database_file)
|
|
547
|
+
)
|
|
406
548
|
id0 = self.db_tree.insert(
|
|
407
|
-
"",
|
|
549
|
+
"",
|
|
550
|
+
0,
|
|
551
|
+
"Database",
|
|
552
|
+
text="main (%s %s)" % (engine_label, dbtext),
|
|
553
|
+
values=(dbtext, ""),
|
|
408
554
|
)
|
|
409
555
|
# add Database Objects, by Category
|
|
410
556
|
for categ in ["master_table", "table", "view", "trigger", "index", "pydef"]:
|
|
@@ -948,11 +1094,12 @@ e/BqhsRJM2fHnD1puuQJ9GdQewIBKN23tOnSfTR5FgSQlKlVqlQXZs169anCrQOxrhyLMCAAOw==
|
|
|
948
1094
|
values=(definition, ""),
|
|
949
1095
|
)
|
|
950
1096
|
# level 3 : Insert a line per column of the Table/View
|
|
1097
|
+
# (id by column position : DuckDB views may repeat names)
|
|
951
1098
|
for c in range(len(sub_c)):
|
|
952
1099
|
self.db_tree.insert(
|
|
953
1100
|
idc,
|
|
954
1101
|
"end",
|
|
955
|
-
"%s%s
|
|
1102
|
+
"%s%s.%s" % (root_txt, t_id, c),
|
|
956
1103
|
text=columns[c],
|
|
957
1104
|
tags=("run_up",),
|
|
958
1105
|
values=("", ""),
|
|
@@ -964,13 +1111,23 @@ e/BqhsRJM2fHnD1puuQJ9GdQewIBKN23tOnSfTR5FgSQlKlVqlQXZs169anCrQOxrhyLMCAAOw==
|
|
|
964
1111
|
a_jouer = self.conn.get_sqlsplit(instructions, remove_comments=False)
|
|
965
1112
|
# must read :https://www.youtube.com/watch?v=09tM18_st4I#t=1751
|
|
966
1113
|
# stackoverflow.com/questions/15856976/transactions-with-python-sqlite3
|
|
967
|
-
|
|
1114
|
+
# DuckDB connections have no isolation_level attribute
|
|
1115
|
+
isolation = getattr(self.conn.conn, "isolation_level", None)
|
|
968
1116
|
counter = 0
|
|
969
1117
|
shell_list = ["", ""]
|
|
970
1118
|
if isolation == "": # Sqlite3 and dump.py default don't match
|
|
971
1119
|
self.conn.conn.isolation_level = None # right behavior
|
|
972
1120
|
cu = self.conn.conn.cursor()
|
|
973
1121
|
sql_error = False
|
|
1122
|
+
log_rows = [] # one line per instruction : the "Logs" tab of the run
|
|
1123
|
+
grid_count = 0 # number of data grids ("Qry N°x" tabs) displayed
|
|
1124
|
+
|
|
1125
|
+
def add_log(result, status, first_line):
|
|
1126
|
+
"""record one instruction outcome for the "Logs" tab"""
|
|
1127
|
+
timing = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
1128
|
+
log_rows.append(
|
|
1129
|
+
(len(log_rows) + 1, timing, result, status, first_line)
|
|
1130
|
+
)
|
|
974
1131
|
|
|
975
1132
|
def beurk(r):
|
|
976
1133
|
"""format data line log"""
|
|
@@ -998,16 +1155,16 @@ e/BqhsRJM2fHnD1puuQJ9GdQewIBKN23tOnSfTR5FgSQlKlVqlQXZs169anCrQOxrhyLMCAAOw==
|
|
|
998
1155
|
first_line = (instru + "\n").splitlines()[0]
|
|
999
1156
|
if instru[:5] == "pydef":
|
|
1000
1157
|
pydef = self.conn.createpydef(instru)
|
|
1001
|
-
titles = ("Creating embedded python function",)
|
|
1002
1158
|
rows = self.conn.conn_def[pydef]["pydef"].splitlines()
|
|
1003
1159
|
rows.append(self.conn.conn_def[pydef]["inst"])
|
|
1004
|
-
|
|
1160
|
+
add_log("python function %s created" % pydef, "Ok", first_line)
|
|
1005
1161
|
if log is not None: # write to logFile
|
|
1006
1162
|
log.write("\n".join(['("%s")' % r for r in rows]) + "\n")
|
|
1007
1163
|
elif instru[:1] == ".": # a shell command !
|
|
1008
1164
|
# handle a ".function" here !
|
|
1009
1165
|
# import FILE TABLE
|
|
1010
1166
|
shell_list = shlex.split(instru, posix=False) # magic standard library
|
|
1167
|
+
dot_result = ""
|
|
1011
1168
|
try:
|
|
1012
1169
|
if shell_list[0] == ".cd" and len(shell_list) >= 2:
|
|
1013
1170
|
db_file = shell_list[1]
|
|
@@ -1109,12 +1266,9 @@ e/BqhsRJM2fHnD1puuQJ9GdQewIBKN23tOnSfTR5FgSQlKlVqlQXZs169anCrQOxrhyLMCAAOw==
|
|
|
1109
1266
|
create_table=False,
|
|
1110
1267
|
replace=False,
|
|
1111
1268
|
)
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
((guess.table_name, csv_file),),
|
|
1116
|
-
"Info",
|
|
1117
|
-
first_line,
|
|
1269
|
+
dot_result = 'file %s imported in "%s"' % (
|
|
1270
|
+
csv_file,
|
|
1271
|
+
guess.table_name,
|
|
1118
1272
|
)
|
|
1119
1273
|
if log is not None: # write to logFile
|
|
1120
1274
|
log.write(
|
|
@@ -1138,6 +1292,7 @@ e/BqhsRJM2fHnD1puuQJ9GdQewIBKN23tOnSfTR5FgSQlKlVqlQXZs169anCrQOxrhyLMCAAOw==
|
|
|
1138
1292
|
([("%s" % line) for line in self.conn.iterdump()]),
|
|
1139
1293
|
"Dump",
|
|
1140
1294
|
".dump",
|
|
1295
|
+
position=0, # keep the "Dump" tab on the left
|
|
1141
1296
|
)
|
|
1142
1297
|
if shell_list[0] == ".read" and len(shell_list) >= 2:
|
|
1143
1298
|
filename = shell_list[1]
|
|
@@ -1166,25 +1321,23 @@ e/BqhsRJM2fHnD1puuQJ9GdQewIBKN23tOnSfTR5FgSQlKlVqlQXZs169anCrQOxrhyLMCAAOw==
|
|
|
1166
1321
|
filename = shell_list[1]
|
|
1167
1322
|
if (filename + "z")[0] == "~":
|
|
1168
1323
|
filename = os.path.join(self.home, filename[1:])
|
|
1169
|
-
|
|
1170
|
-
db_from.backup(self.conn.conn)
|
|
1171
|
-
db_from.close
|
|
1324
|
+
self.restore_main_from(filename)
|
|
1172
1325
|
self.actualize_db()
|
|
1173
1326
|
if shell_list[0] == ".backup" and len(shell_list) >= 2:
|
|
1174
1327
|
filename = shell_list[1]
|
|
1175
1328
|
if (filename + "z")[0] == "~":
|
|
1176
1329
|
filename = os.path.join(self.home, filename[1:])
|
|
1177
|
-
|
|
1178
|
-
self.conn.conn.backup(db_to)
|
|
1179
|
-
db_to.close()
|
|
1330
|
+
self.backup_main_to(filename)
|
|
1180
1331
|
if shell_list[0] == ".shell" and len(shell_list) >= 2:
|
|
1181
1332
|
os.system(instru[len(".print") + 1 :] + "\n")
|
|
1333
|
+
add_log(dot_result, "Ok", first_line)
|
|
1182
1334
|
|
|
1183
1335
|
except IOError as err:
|
|
1184
1336
|
msg = "I/O error: {0}".format(err)
|
|
1185
1337
|
self.n.add_treeview(
|
|
1186
1338
|
tab_tk_id, ("Error !",), [(msg,)], "Error !", instru
|
|
1187
1339
|
)
|
|
1340
|
+
add_log(msg, "Error", first_line)
|
|
1188
1341
|
if not self.use_gui:
|
|
1189
1342
|
print("Error !", [msg])
|
|
1190
1343
|
if log is not None: # write to logFile
|
|
@@ -1205,13 +1358,9 @@ e/BqhsRJM2fHnD1puuQJ9GdQewIBKN23tOnSfTR5FgSQlKlVqlQXZs169anCrQOxrhyLMCAAOw==
|
|
|
1205
1358
|
)
|
|
1206
1359
|
if nb_columns > 0:
|
|
1207
1360
|
self.once_mode, self.init_output = False, False
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
("qry_to_csv", "file"),
|
|
1212
|
-
((instruction, self.output_file),),
|
|
1213
|
-
"Qry",
|
|
1214
|
-
# ".once %s" % self.output_file,
|
|
1361
|
+
add_log(
|
|
1362
|
+
"query exported to %s" % self.output_file,
|
|
1363
|
+
"Ok",
|
|
1215
1364
|
first_line,
|
|
1216
1365
|
)
|
|
1217
1366
|
if self.x_mode and nb_columns > 0:
|
|
@@ -1231,9 +1380,49 @@ e/BqhsRJM2fHnD1puuQJ9GdQewIBKN23tOnSfTR5FgSQlKlVqlQXZs169anCrQOxrhyLMCAAOw==
|
|
|
1231
1380
|
cur.description is not None and len(cur.description) > 0
|
|
1232
1381
|
): # pypy needs this test
|
|
1233
1382
|
titles = [row_info[0] for row_info in cur.description]
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
)
|
|
1383
|
+
# DuckDB DDL/DML return a synthetic 'Count' or
|
|
1384
|
+
# 'Success' resultset : log it, don't grid it
|
|
1385
|
+
# (unless the instruction is a genuine query)
|
|
1386
|
+
if (
|
|
1387
|
+
titles in (["Count"], ["Success"])
|
|
1388
|
+
and len(rows) <= 1
|
|
1389
|
+
and not instru.lstrip("( \t\n\r")[:9]
|
|
1390
|
+
.lower()
|
|
1391
|
+
.startswith(
|
|
1392
|
+
(
|
|
1393
|
+
"select",
|
|
1394
|
+
"with",
|
|
1395
|
+
"from",
|
|
1396
|
+
"values",
|
|
1397
|
+
"table",
|
|
1398
|
+
"show",
|
|
1399
|
+
"describe",
|
|
1400
|
+
"summarize",
|
|
1401
|
+
"pragma",
|
|
1402
|
+
"call",
|
|
1403
|
+
"explain",
|
|
1404
|
+
)
|
|
1405
|
+
)
|
|
1406
|
+
):
|
|
1407
|
+
affected = rows[0][0] if rows else 0
|
|
1408
|
+
add_log(
|
|
1409
|
+
"%s rows affected" % affected
|
|
1410
|
+
if titles == ["Count"]
|
|
1411
|
+
else "",
|
|
1412
|
+
"Ok",
|
|
1413
|
+
first_line,
|
|
1414
|
+
)
|
|
1415
|
+
else:
|
|
1416
|
+
add_log("%s rows" % len(rows), "Ok", first_line)
|
|
1417
|
+
# grid tab title carries the "Logs" record N°
|
|
1418
|
+
self.n.add_treeview(
|
|
1419
|
+
tab_tk_id,
|
|
1420
|
+
titles,
|
|
1421
|
+
rows,
|
|
1422
|
+
"Qry_%s" % len(log_rows),
|
|
1423
|
+
first_line,
|
|
1424
|
+
)
|
|
1425
|
+
grid_count += 1
|
|
1237
1426
|
if log is not None: # write to logFile
|
|
1238
1427
|
log.write(beurk(titles) + "\n")
|
|
1239
1428
|
log.write(
|
|
@@ -1241,16 +1430,38 @@ e/BqhsRJM2fHnD1puuQJ9GdQewIBKN23tOnSfTR5FgSQlKlVqlQXZs169anCrQOxrhyLMCAAOw==
|
|
|
1241
1430
|
)
|
|
1242
1431
|
if len(rows) > limit:
|
|
1243
1432
|
log.write("%s more..." % len((rows) - limit))
|
|
1244
|
-
|
|
1433
|
+
else:
|
|
1434
|
+
rowcount = getattr(cur, "rowcount", -1)
|
|
1435
|
+
add_log(
|
|
1436
|
+
"%s rows affected" % rowcount
|
|
1437
|
+
if rowcount >= 0
|
|
1438
|
+
else "",
|
|
1439
|
+
"Ok",
|
|
1440
|
+
first_line,
|
|
1441
|
+
)
|
|
1442
|
+
except db_errors as msg: # OperationalError
|
|
1245
1443
|
self.n.add_treeview(
|
|
1246
1444
|
tab_tk_id, ("Error !",), [(msg,)], "Error !", first_line
|
|
1247
1445
|
)
|
|
1446
|
+
add_log("%s" % msg, "Error", first_line)
|
|
1248
1447
|
if log is not None: # write to logFile
|
|
1249
1448
|
log.write("Error ! %s" % msg)
|
|
1250
1449
|
sql_error = True
|
|
1251
1450
|
break
|
|
1252
1451
|
|
|
1253
|
-
|
|
1452
|
+
# display the "Logs" tab, unless the run was a single query
|
|
1453
|
+
# already fully rendered by its data grid (classic behavior)
|
|
1454
|
+
if len(log_rows) > 1 or (log_rows and grid_count == 0):
|
|
1455
|
+
self.n.add_treeview(
|
|
1456
|
+
tab_tk_id,
|
|
1457
|
+
("N°", "Time", "Result", "Status", "Instruction"),
|
|
1458
|
+
log_rows,
|
|
1459
|
+
"Logs",
|
|
1460
|
+
"execution log",
|
|
1461
|
+
position=0, # keep the permanent "Logs" tab leftmost
|
|
1462
|
+
)
|
|
1463
|
+
|
|
1464
|
+
if getattr(self.conn.conn, "isolation_level", None) != isolation:
|
|
1254
1465
|
# if we're in 'backward' compatible mode (automatic commit)
|
|
1255
1466
|
try:
|
|
1256
1467
|
if self.conn.conn.in_transaction: # python 3.2
|
|
@@ -1468,7 +1679,9 @@ class NotebookForQueries:
|
|
|
1468
1679
|
xx.grid_forget()
|
|
1469
1680
|
xx.destroy()
|
|
1470
1681
|
|
|
1471
|
-
def add_treeview(
|
|
1682
|
+
def add_treeview(
|
|
1683
|
+
self, given_tk_id, columns, data, title="__", subt="", position="end"
|
|
1684
|
+
):
|
|
1472
1685
|
"""add a dataset result to the given tab tk_id"""
|
|
1473
1686
|
if not self.use_gui:
|
|
1474
1687
|
return
|
|
@@ -1491,7 +1704,9 @@ class NotebookForQueries:
|
|
|
1491
1704
|
height=100,
|
|
1492
1705
|
)
|
|
1493
1706
|
f2.pack(fill="both", expand=True)
|
|
1494
|
-
fw_result_nb.
|
|
1707
|
+
if position != "end" and not fw_result_nb.tabs():
|
|
1708
|
+
position = "end" # Tk refuses insert(0) in an empty notebook
|
|
1709
|
+
fw_result_nb.insert(position, f2, text=title)
|
|
1495
1710
|
|
|
1496
1711
|
# ttk.Style().configure('TLabelframe.label', font=("Arial",14, "bold"))
|
|
1497
1712
|
# lines=queries
|
|
@@ -1514,7 +1729,8 @@ class NotebookForQueries:
|
|
|
1514
1729
|
fw_Box.heading(
|
|
1515
1730
|
col, text=col.title(), command=lambda c=col: self.sortby(fw_Box, c, 0)
|
|
1516
1731
|
)
|
|
1517
|
-
|
|
1732
|
+
# keep a little breathing room around the header text
|
|
1733
|
+
fw_Box.column(col, width=font.Font().measure(col.title()) + 12)
|
|
1518
1734
|
|
|
1519
1735
|
def flat(x):
|
|
1520
1736
|
"""replace line_return by space, if given a string"""
|
|
@@ -1534,7 +1750,7 @@ class NotebookForQueries:
|
|
|
1534
1750
|
# adjust columns length if necessary and possible
|
|
1535
1751
|
for indx, val in enumerate(line_cells):
|
|
1536
1752
|
try:
|
|
1537
|
-
ilen = font.Font().measure(val)
|
|
1753
|
+
ilen = font.Font().measure(val) + 12
|
|
1538
1754
|
if (
|
|
1539
1755
|
fw_Box.column(tree_columns[indx], width=None) < ilen
|
|
1540
1756
|
and ilen < 400
|
|
@@ -1633,7 +1849,10 @@ def guess_sql_creation(table_name, separ, decim, header, data, quoter='"'):
|
|
|
1633
1849
|
head = ",\n".join([('"%s" %s' % (r[i], typ[i])) for i in range(len(r))])
|
|
1634
1850
|
sql_crea = 'CREATE TABLE "%s" (%s);' % (table_name, head)
|
|
1635
1851
|
else:
|
|
1636
|
-
|
|
1852
|
+
# typed columns : mandatory for DuckDB, harmless for SQLite
|
|
1853
|
+
head = ",".join(
|
|
1854
|
+
["c_" + ("000" + str(i))[-3:] + " " + typ[i] for i in range(len(r))]
|
|
1855
|
+
)
|
|
1637
1856
|
sql_crea = 'CREATE TABLE "%s" (%s);' % (table_name, head)
|
|
1638
1857
|
return sql_crea, typ, head
|
|
1639
1858
|
|
|
@@ -1862,6 +2081,49 @@ def get_leaves(conn, category, attached_db="", tbl=""):
|
|
|
1862
2081
|
|
|
1863
2082
|
if category == "pydef": # pydef request is not sql, answer is direct
|
|
1864
2083
|
Tables = [[k, k, v["pydef"], ""] for k, v in conn.conn_def.items()]
|
|
2084
|
+
elif getattr(conn, "engine", "sqlite") == "duckdb":
|
|
2085
|
+
# DuckDB : sqlite_master only exists for the current database,
|
|
2086
|
+
# so rely on the duckdb_xxx() catalog functions instead
|
|
2087
|
+
dbf = (
|
|
2088
|
+
"current_database()"
|
|
2089
|
+
if attached_db in ("", "main", "temp")
|
|
2090
|
+
else "'%s'" % attached_db.replace("'", "''")
|
|
2091
|
+
)
|
|
2092
|
+
if category == "attached_databases":
|
|
2093
|
+
sql = """select database_oid, database_name, coalesce(path, '')
|
|
2094
|
+
from duckdb_databases()
|
|
2095
|
+
where not internal and database_name != current_database()"""
|
|
2096
|
+
for c in conn.execute(sql).fetchall():
|
|
2097
|
+
instruct = "ATTACH DATABASE %s as %s" % (f(c[2]), f(c[1]))
|
|
2098
|
+
Tables.append([c[0], c[1], instruct, ""])
|
|
2099
|
+
elif category == "fields":
|
|
2100
|
+
qualified = (
|
|
2101
|
+
tbl
|
|
2102
|
+
if attached_db in ("", "main")
|
|
2103
|
+
else "%s.%s" % (attached_db, tbl)
|
|
2104
|
+
)
|
|
2105
|
+
resu = conn.execute(
|
|
2106
|
+
"PRAGMA table_info('%s')" % qualified.replace("'", "''")
|
|
2107
|
+
).fetchall()
|
|
2108
|
+
Tables = [[c[1], c[1], c[2], ""] for c in resu]
|
|
2109
|
+
elif category in ("table", "view", "index"):
|
|
2110
|
+
if category == "table":
|
|
2111
|
+
sql = """select 'table:' || table_name, table_name,
|
|
2112
|
+
coalesce(sql, '--auto'), 'fields'
|
|
2113
|
+
from duckdb_tables() where not internal
|
|
2114
|
+
and database_name = {0} order by table_name"""
|
|
2115
|
+
elif category == "view":
|
|
2116
|
+
sql = """select 'view:' || view_name, view_name,
|
|
2117
|
+
coalesce(sql, '--auto'), 'fields'
|
|
2118
|
+
from duckdb_views() where not internal
|
|
2119
|
+
and database_name = {0} order by view_name"""
|
|
2120
|
+
else:
|
|
2121
|
+
sql = """select 'index:' || index_name, index_name,
|
|
2122
|
+
coalesce(sql, '--auto'), ''
|
|
2123
|
+
from duckdb_indexes()
|
|
2124
|
+
where database_name = {0} order by index_name"""
|
|
2125
|
+
Tables = list(conn.execute(sql.format(dbf)).fetchall())
|
|
2126
|
+
# 'trigger' and 'master_table' : nothing to show for DuckDB
|
|
1865
2127
|
elif category == "attached_databases":
|
|
1866
2128
|
# get all attached database, but not the first one ('main')
|
|
1867
2129
|
resu = list((conn.execute("PRAGMA database_list").fetchall()))[1:]
|
|
@@ -1889,23 +2151,70 @@ def get_leaves(conn, category, attached_db="", tbl=""):
|
|
|
1889
2151
|
return Tables
|
|
1890
2152
|
|
|
1891
2153
|
|
|
2154
|
+
def duck_type_adapt(fn, nargs):
|
|
2155
|
+
"""wrap an annotation-less pydef for DuckDB : mimic SQLite dynamic typing
|
|
2156
|
+
(arguments arrive as VARCHAR, numeric-looking ones are converted back)"""
|
|
2157
|
+
|
|
2158
|
+
def convert_call(args):
|
|
2159
|
+
converted = []
|
|
2160
|
+
for arg in args:
|
|
2161
|
+
if isinstance(arg, str):
|
|
2162
|
+
try:
|
|
2163
|
+
arg = int(arg)
|
|
2164
|
+
except ValueError:
|
|
2165
|
+
try:
|
|
2166
|
+
arg = float(arg)
|
|
2167
|
+
except ValueError:
|
|
2168
|
+
pass
|
|
2169
|
+
converted.append(arg)
|
|
2170
|
+
result = fn(*converted)
|
|
2171
|
+
return result if result is None else str(result)
|
|
2172
|
+
|
|
2173
|
+
if nargs == 0:
|
|
2174
|
+
return lambda: convert_call(())
|
|
2175
|
+
# build a wrapper of the exact arity DuckDB expects (*args confuses it)
|
|
2176
|
+
argnames = ",".join("a%s" % i for i in range(nargs))
|
|
2177
|
+
return eval("lambda %s: _c((%s,))" % (argnames, argnames), {"_c": convert_call})
|
|
2178
|
+
|
|
2179
|
+
|
|
1892
2180
|
class Baresql:
|
|
1893
2181
|
"""a small wrapper around sqlite3 module"""
|
|
1894
2182
|
|
|
1895
2183
|
def __init__(
|
|
1896
|
-
self,
|
|
2184
|
+
self,
|
|
2185
|
+
connection="",
|
|
2186
|
+
keep_log=False,
|
|
2187
|
+
cte_inline=True,
|
|
2188
|
+
isolation_level=None,
|
|
2189
|
+
engine=None,
|
|
1897
2190
|
):
|
|
1898
2191
|
self.dbname = connection.replace(":///", "://").replace("sqlite://", "")
|
|
1899
|
-
|
|
2192
|
+
if engine is None: # guess the engine from the file extension
|
|
2193
|
+
engine = (
|
|
2194
|
+
"duckdb"
|
|
2195
|
+
if self.dbname.lower().endswith((".duckdb", ".ddb"))
|
|
2196
|
+
else "sqlite"
|
|
2197
|
+
)
|
|
2198
|
+
self.engine = engine
|
|
2199
|
+
if self.engine == "duckdb":
|
|
2200
|
+
if duckdb is None:
|
|
2201
|
+
raise ImportError(
|
|
2202
|
+
"DuckDB engine requested, but 'duckdb' module is not installed"
|
|
2203
|
+
)
|
|
2204
|
+
self.conn = duckdb.connect(self.dbname)
|
|
2205
|
+
else:
|
|
2206
|
+
self.conn = sqlite.connect(
|
|
2207
|
+
self.dbname, detect_types=sqlite.PARSE_DECLTYPES
|
|
2208
|
+
)
|
|
2209
|
+
self.conn.isolation_level = isolation_level # commit experience
|
|
1900
2210
|
# pydef and logging infrastructure
|
|
1901
2211
|
self.conn_def = {}
|
|
1902
2212
|
self.do_log = keep_log
|
|
1903
2213
|
self.log = []
|
|
1904
|
-
self.conn.isolation_level = isolation_level # commit experience
|
|
1905
2214
|
|
|
1906
2215
|
def close(self):
|
|
1907
2216
|
"""close database and clear dictionnary of registered 'pydef'"""
|
|
1908
|
-
self.conn.close
|
|
2217
|
+
self.conn.close()
|
|
1909
2218
|
self.conn_def = {}
|
|
1910
2219
|
|
|
1911
2220
|
def iterdump(self):
|
|
@@ -1915,6 +2224,42 @@ class Baresql:
|
|
|
1915
2224
|
# add the Python functions pydef
|
|
1916
2225
|
for k in self.conn_def.values():
|
|
1917
2226
|
yield (k["pydef"] + ";\n")
|
|
2227
|
+
if self.engine == "duckdb":
|
|
2228
|
+
# DuckDB has no iterdump : rebuild one from catalog functions
|
|
2229
|
+
yield ("BEGIN TRANSACTION;")
|
|
2230
|
+
tables = self.conn.execute(
|
|
2231
|
+
"""select table_name, sql from duckdb_tables()
|
|
2232
|
+
where not internal and database_name = current_database()
|
|
2233
|
+
order by table_name"""
|
|
2234
|
+
).fetchall()
|
|
2235
|
+
for table_name, sql in tables:
|
|
2236
|
+
yield (sql if sql.rstrip().endswith(";") else sql + ";")
|
|
2237
|
+
quoted = table_name.replace('"', '""')
|
|
2238
|
+
for row in self.conn.execute(
|
|
2239
|
+
'SELECT * FROM "%s"' % quoted
|
|
2240
|
+
).fetchall():
|
|
2241
|
+
vals = ",".join(
|
|
2242
|
+
"NULL"
|
|
2243
|
+
if v is None
|
|
2244
|
+
else str(v)
|
|
2245
|
+
if isinstance(v, (int, float))
|
|
2246
|
+
else "'%s'" % str(v).replace("'", "''")
|
|
2247
|
+
for v in row
|
|
2248
|
+
)
|
|
2249
|
+
yield ('INSERT INTO "%s" VALUES(%s);' % (quoted, vals))
|
|
2250
|
+
for (sql,) in self.conn.execute(
|
|
2251
|
+
"""select sql from duckdb_views()
|
|
2252
|
+
where not internal and database_name = current_database()"""
|
|
2253
|
+
).fetchall():
|
|
2254
|
+
yield (sql if sql.rstrip().endswith(";") else sql + ";")
|
|
2255
|
+
for (sql,) in self.conn.execute(
|
|
2256
|
+
"""select sql from duckdb_indexes()
|
|
2257
|
+
where database_name = current_database()"""
|
|
2258
|
+
).fetchall():
|
|
2259
|
+
if sql:
|
|
2260
|
+
yield (sql if sql.rstrip().endswith(";") else sql + ";")
|
|
2261
|
+
yield ("COMMIT;")
|
|
2262
|
+
return
|
|
1918
2263
|
# disable Foreign Constraints at Load
|
|
1919
2264
|
yield ("PRAGMA foreign_keys = OFF; /*if SQlite */;")
|
|
1920
2265
|
yield ("\n/* SET foreign_key_checks = 0;/*if Mysql*/;")
|
|
@@ -1950,7 +2295,38 @@ class Baresql:
|
|
|
1950
2295
|
instr_name = instr_header[1]
|
|
1951
2296
|
instr_parms = len(instr_header) - 2
|
|
1952
2297
|
instr_pointer=eval(instr_name, globals(), pydef_locals)
|
|
1953
|
-
self.
|
|
2298
|
+
if self.engine == "duckdb":
|
|
2299
|
+
# DuckDB needs typed functions (and numpy installed) :
|
|
2300
|
+
# 1- clean up previous definitions of the same name
|
|
2301
|
+
for cleanup in (
|
|
2302
|
+
lambda: self.conn.remove_function(instr_name),
|
|
2303
|
+
lambda: self.conn.remove_function("_bro_" + instr_name),
|
|
2304
|
+
lambda: self.conn.execute('DROP MACRO IF EXISTS "%s"' % instr_name),
|
|
2305
|
+
):
|
|
2306
|
+
try:
|
|
2307
|
+
cleanup()
|
|
2308
|
+
except Exception:
|
|
2309
|
+
pass
|
|
2310
|
+
# 2- a fully type-annotated pydef registers natively ...
|
|
2311
|
+
try:
|
|
2312
|
+
self.conn.create_function(instr_name, instr_pointer)
|
|
2313
|
+
except Exception:
|
|
2314
|
+
# ... otherwise register a VARCHAR implementation behind a
|
|
2315
|
+
# macro that casts any argument, to mimic SQLite duck-typing
|
|
2316
|
+
self.conn.create_function(
|
|
2317
|
+
"_bro_" + instr_name,
|
|
2318
|
+
duck_type_adapt(instr_pointer, instr_parms),
|
|
2319
|
+
["VARCHAR"] * instr_parms,
|
|
2320
|
+
"VARCHAR",
|
|
2321
|
+
)
|
|
2322
|
+
parms = ["p%s" % i for i in range(instr_parms)]
|
|
2323
|
+
casts = ["CAST(p%s AS VARCHAR)" % i for i in range(instr_parms)]
|
|
2324
|
+
self.conn.execute(
|
|
2325
|
+
'CREATE OR REPLACE MACRO "%s"(%s) AS "_bro_%s"(%s)'
|
|
2326
|
+
% (instr_name, ",".join(parms), instr_name, ",".join(casts))
|
|
2327
|
+
)
|
|
2328
|
+
else:
|
|
2329
|
+
self.conn.create_function(instr_name, instr_parms, instr_pointer)
|
|
1954
2330
|
instr_add = "self.conn.create_function('%s', %s, %s)" % (
|
|
1955
2331
|
instr_name,
|
|
1956
2332
|
instr_parms,
|
|
@@ -2102,8 +2478,11 @@ class Baresql:
|
|
|
2102
2478
|
curs.execute("begin transaction")
|
|
2103
2479
|
except:
|
|
2104
2480
|
pass
|
|
2105
|
-
# check if table exists
|
|
2106
|
-
|
|
2481
|
+
# check if table exists (DuckDB raises where SQLite returns empty)
|
|
2482
|
+
try:
|
|
2483
|
+
here = curs.execute('PRAGMA table_info("%s")' % table_name).fetchall()
|
|
2484
|
+
except db_errors:
|
|
2485
|
+
here = []
|
|
2107
2486
|
if create_sql and (create_table or len(here) == 0):
|
|
2108
2487
|
curs.execute('drop TABLE if exists "%s";' % table_name)
|
|
2109
2488
|
curs.execute(create_sql)
|
|
@@ -2117,7 +2496,10 @@ class Baresql:
|
|
|
2117
2496
|
next(reader)
|
|
2118
2497
|
# 2-push records
|
|
2119
2498
|
curs.executemany(sql, reader)
|
|
2120
|
-
self.
|
|
2499
|
+
if self.engine == "duckdb":
|
|
2500
|
+
curs.commit() # a DuckDB cursor is a duplicated connection
|
|
2501
|
+
else:
|
|
2502
|
+
self.conn.commit()
|
|
2121
2503
|
|
|
2122
2504
|
def export_writer(
|
|
2123
2505
|
self,
|
|
@@ -2135,8 +2517,21 @@ class Baresql:
|
|
|
2135
2517
|
# do nothing if nothing
|
|
2136
2518
|
if cursor.description is None or len(cursor.description) == 0:
|
|
2137
2519
|
return -1
|
|
2138
|
-
|
|
2139
|
-
|
|
2520
|
+
if self.engine == "duckdb":
|
|
2521
|
+
# DuckDB gives a description ('Count') even to non-SELECT
|
|
2522
|
+
# statements : export only genuine queries, like SQLite does
|
|
2523
|
+
first_word = ""
|
|
2524
|
+
for line in sql.splitlines():
|
|
2525
|
+
line = line.strip()
|
|
2526
|
+
if line and not line.startswith("--"):
|
|
2527
|
+
first_word = line.split()[0].lower()
|
|
2528
|
+
break
|
|
2529
|
+
if first_word not in (
|
|
2530
|
+
"select", "with", "from", "pragma", "show",
|
|
2531
|
+
"describe", "values", "call", "summarize", "table",
|
|
2532
|
+
):
|
|
2533
|
+
return -1
|
|
2534
|
+
nb_columns = len(cursor.description)
|
|
2140
2535
|
# with PyPy, the "with io.open" for is more than necessary
|
|
2141
2536
|
if sys.version_info[0] != 2: # python3
|
|
2142
2537
|
write_mode = "w" if initialize else "a" # Write or Append
|
|
@@ -2170,6 +2565,7 @@ class Baresql:
|
|
|
2170
2565
|
|
|
2171
2566
|
def _main():
|
|
2172
2567
|
welcome_text = """-- SQLite Memo (Demo = click on green "->" and "@" icons)
|
|
2568
|
+
-- (tip : open or create a '.duckdb' file to use the DuckDB engine instead)
|
|
2173
2569
|
\n-- to CREATE a table 'items' and a table 'parts' :
|
|
2174
2570
|
DROP TABLE IF EXISTS item; DROP TABLE IF EXISTS part;
|
|
2175
2571
|
CREATE TABLE item (ItemNo, Description,Kg , PRIMARY KEY (ItemNo));
|
|
@@ -2247,7 +2643,8 @@ CREATE TABLE toto.new_item as select * from "main"."item";
|
|
|
2247
2643
|
|
|
2248
2644
|
if "argparse" in globals(): # not before Python-3.2
|
|
2249
2645
|
parser = argparse.ArgumentParser(
|
|
2250
|
-
description="sqlite_bro : a graphic SQLite
|
|
2646
|
+
description="sqlite_bro : a graphic SQLite and DuckDB browser"
|
|
2647
|
+
" in 1 Python file"
|
|
2251
2648
|
)
|
|
2252
2649
|
parser.add_argument(
|
|
2253
2650
|
"-q", "--quiet", action="store_true", help="do not launch the gui"
|
|
@@ -2263,7 +2660,8 @@ CREATE TABLE toto.new_item as select * from "main"."item";
|
|
|
2263
2660
|
"--database",
|
|
2264
2661
|
default=":memory:",
|
|
2265
2662
|
type=str,
|
|
2266
|
-
help="specify initial Database if not ':memory:'"
|
|
2663
|
+
help="specify initial Database if not ':memory:'"
|
|
2664
|
+
" (a .duckdb/.ddb extension selects the DuckDB engine)",
|
|
2267
2665
|
)
|
|
2268
2666
|
parser.add_argument(
|
|
2269
2667
|
"-sc", "--scripts", type=str, help="qive a list of initial scripts"
|
|
@@ -2290,7 +2688,7 @@ CREATE TABLE toto.new_item as select * from "main"."item";
|
|
|
2290
2688
|
app.n.new_query_tab("Welcome", welcome_text)
|
|
2291
2689
|
if not args.wait:
|
|
2292
2690
|
app.run_tab()
|
|
2293
|
-
|
|
2691
|
+
elif app.conn.engine != "duckdb": # duckdb welcome tab auto-shows
|
|
2294
2692
|
app.n.new_query_tab("Welcome", welcome_text)
|
|
2295
2693
|
if args.quiet:
|
|
2296
2694
|
app.close_db
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# same scenarios as test_general_no_gui.py, but with the DuckDB engine
|
|
2
|
+
# (DuckDB needs typed CREATE TABLE and single-quoted string literals)
|
|
3
|
+
import pytest
|
|
4
|
+
import pathlib
|
|
5
|
+
import tempfile
|
|
6
|
+
|
|
7
|
+
import io
|
|
8
|
+
|
|
9
|
+
duckdb = pytest.importorskip("duckdb")
|
|
10
|
+
from sqlite_bro import sqlite_bro
|
|
11
|
+
app = sqlite_bro.App(use_gui=False)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def test_EngineDetection():
|
|
15
|
+
"a .duckdb file extension must select the duckdb engine"
|
|
16
|
+
with tempfile.TemporaryDirectory(prefix='.tmp') as tmp_dir:
|
|
17
|
+
db_file = str(pathlib.PurePath(tmp_dir, 'detect.duckdb'))
|
|
18
|
+
app.open_db(db_file)
|
|
19
|
+
assert app.conn.engine == "duckdb"
|
|
20
|
+
app.conn.close()
|
|
21
|
+
app.new_db(":memory:")
|
|
22
|
+
assert app.conn.engine == "sqlite"
|
|
23
|
+
app.new_db(":memory:", engine="duckdb")
|
|
24
|
+
assert app.conn.engine == "duckdb"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def test_Basics():
|
|
28
|
+
"create script, run script, output result, check result"
|
|
29
|
+
app.new_db(":memory:", engine="duckdb")
|
|
30
|
+
with tempfile.TemporaryDirectory(prefix='.tmp') as tmp_dir:
|
|
31
|
+
print(tmp_dir)
|
|
32
|
+
tmp_file = str(pathlib.PurePath(tmp_dir, 'sqlite_bro_test_Basics.tmp'))
|
|
33
|
+
welcome_text = """
|
|
34
|
+
create table item (ItemNo TEXT, Description TEXT, Kg INTEGER, PRIMARY KEY (ItemNo));
|
|
35
|
+
INSERT INTO item values('T','Ford',1000);
|
|
36
|
+
INSERT INTO item select 'A','Merced',1250 union all select 'W','Wheel',9 ;
|
|
37
|
+
.once %s
|
|
38
|
+
select ItemNo, Description, 1000*Kg Gramm from item order by ItemNo desc;
|
|
39
|
+
.import %s in_this_table""" % (tmp_file, tmp_file)
|
|
40
|
+
app.n.new_query_tab("Welcome", welcome_text)
|
|
41
|
+
app.run_tab()
|
|
42
|
+
app.close_db
|
|
43
|
+
|
|
44
|
+
file_encoding = sqlite_bro.guess_encoding(tmp_file)[0]
|
|
45
|
+
with io.open(tmp_file, mode='rt', encoding=file_encoding) as f:
|
|
46
|
+
result = f.readlines()
|
|
47
|
+
assert len(result) == 4
|
|
48
|
+
assert result[-1] == "A,Merced,1250000\n"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def test_Outputs():
|
|
52
|
+
"testing .output, .print, .header, .separator"
|
|
53
|
+
|
|
54
|
+
app.new_db(":memory:", engine="duckdb")
|
|
55
|
+
with tempfile.TemporaryDirectory(prefix='.tmp') as tmp_dir:
|
|
56
|
+
print(tmp_dir)
|
|
57
|
+
tmp_file = str(pathlib.PurePath(tmp_dir, 'sqlite_bro_test_Output.tmp'))
|
|
58
|
+
welcome_text = """
|
|
59
|
+
create table item (ItemNo TEXT, Description TEXT, PRIMARY KEY (ItemNo));
|
|
60
|
+
INSERT INTO item values('DS','Citroën');
|
|
61
|
+
.output %s
|
|
62
|
+
.separator ;
|
|
63
|
+
.headers off
|
|
64
|
+
.print a;b
|
|
65
|
+
select * from item;
|
|
66
|
+
.headers on
|
|
67
|
+
.separator !
|
|
68
|
+
select * from item;
|
|
69
|
+
.import %s in_this_table""" % (tmp_file, tmp_file)
|
|
70
|
+
app.n.new_query_tab("Welcome", welcome_text)
|
|
71
|
+
app.run_tab()
|
|
72
|
+
app.close_db
|
|
73
|
+
|
|
74
|
+
file_encoding = sqlite_bro.guess_encoding(tmp_file)[0]
|
|
75
|
+
with io.open(tmp_file, mode='rt', encoding=file_encoding) as f:
|
|
76
|
+
result = f.readlines()
|
|
77
|
+
print(result)
|
|
78
|
+
assert len(result) == 4
|
|
79
|
+
assert result[0] == "a;b\n"
|
|
80
|
+
assert result[1] == "DS;Citroën\n"
|
|
81
|
+
assert result[2] == "ItemNo!Description\n"
|
|
82
|
+
assert result[3] == "DS!Citroën\n"
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def test_Pydef():
|
|
86
|
+
"python function embedded in sql, on the duckdb engine (needs numpy)"
|
|
87
|
+
pytest.importorskip("numpy")
|
|
88
|
+
app.new_db(":memory:", engine="duckdb")
|
|
89
|
+
app.default_separator, app.default_header = ",", True # reset prior state
|
|
90
|
+
with tempfile.TemporaryDirectory(prefix='.tmp') as tmp_dir:
|
|
91
|
+
tmp_file = str(pathlib.PurePath(tmp_dir, 'sqlite_bro_test_Pydef.tmp'))
|
|
92
|
+
welcome_text = """
|
|
93
|
+
pydef py_dup(a):
|
|
94
|
+
"duplicate a string"
|
|
95
|
+
return a + a;
|
|
96
|
+
pydef py_fib(n):
|
|
97
|
+
"fibonacci, annotation-less, called with an int literal"
|
|
98
|
+
fib = lambda n: n if n < 2 else fib(n-1) + fib(n-2)
|
|
99
|
+
return("%%s" %% fib(n*1));
|
|
100
|
+
pydef py_fib_t(n: int) -> int:
|
|
101
|
+
"fibonacci, type-annotated : registered natively"
|
|
102
|
+
fib = lambda n: n if n < 2 else fib(n-1) + fib(n-2)
|
|
103
|
+
return fib(n);
|
|
104
|
+
.once %s
|
|
105
|
+
select py_dup('ab') as dup, py_fib(6) as fib, py_fib_t(7) as fib_t;""" % tmp_file
|
|
106
|
+
app.n.new_query_tab("Welcome", welcome_text)
|
|
107
|
+
app.run_tab()
|
|
108
|
+
app.run_tab() # re-run : pydef re-registration must not fail
|
|
109
|
+
|
|
110
|
+
file_encoding = sqlite_bro.guess_encoding(tmp_file)[0]
|
|
111
|
+
with io.open(tmp_file, mode='rt', encoding=file_encoding) as f:
|
|
112
|
+
result = f.readlines()
|
|
113
|
+
assert result[0] == "dup,fib,fib_t\n"
|
|
114
|
+
assert result[1] == "abab,8,13\n"
|
|
115
|
+
app.close_db
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def test_WelcomeDemo():
|
|
119
|
+
"the DuckDB welcome demo must run error-free (sql + pydef part)"
|
|
120
|
+
pytest.importorskip("numpy")
|
|
121
|
+
app.new_db(":memory:", engine="duckdb")
|
|
122
|
+
app.output_mode, app.once_mode = False, False
|
|
123
|
+
# keep only the sql + pydef part : the '.' commands part writes to ~
|
|
124
|
+
demo = sqlite_bro.WELCOME_DUCKDB.split(".headers on")[0]
|
|
125
|
+
# a sentinel : it is only reached if no previous instruction errored
|
|
126
|
+
demo += "\ncreate table demo_ok as select 1 as ok;"
|
|
127
|
+
app.n.new_query_tab("DuckDB Memo", demo)
|
|
128
|
+
app.run_tab()
|
|
129
|
+
assert app.conn.execute("select ok from demo_ok").fetchall() == [(1,)]
|
|
130
|
+
assert app.conn.execute("select py_fib(6)").fetchall() == [('8',)]
|
|
131
|
+
assert app.conn.execute("select py_fib_typed(7)").fetchall() == [(13,)]
|
|
132
|
+
app.close_db
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def test_Dump():
|
|
136
|
+
"iterdump must produce a replayable sql script"
|
|
137
|
+
app.new_db(":memory:", engine="duckdb")
|
|
138
|
+
app.output_mode, app.once_mode = False, False # reset state of prior tests
|
|
139
|
+
app.n.new_query_tab(
|
|
140
|
+
"Welcome",
|
|
141
|
+
"""create table item (ItemNo TEXT, Kg INTEGER);
|
|
142
|
+
INSERT INTO item values('T',1000);
|
|
143
|
+
create view v1 as select * from item;""",
|
|
144
|
+
)
|
|
145
|
+
app.run_tab()
|
|
146
|
+
dump = "\n".join(app.conn.iterdump())
|
|
147
|
+
assert "CREATE TABLE item" in dump
|
|
148
|
+
assert "INSERT INTO \"item\" VALUES('T',1000);" in dump
|
|
149
|
+
assert "CREATE VIEW v1" in dump
|
|
150
|
+
app.close_db
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# from pyappveyordemo.extension import some_function
|
|
2
|
+
import pytest
|
|
3
|
+
import pathlib
|
|
4
|
+
import tempfile
|
|
5
|
+
|
|
6
|
+
import io
|
|
7
|
+
from sqlite_bro import sqlite_bro
|
|
8
|
+
app = sqlite_bro.App(use_gui=False)
|
|
9
|
+
def test_DeBase():
|
|
10
|
+
"learning the ropes"
|
|
11
|
+
assert 1 == 1
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def test_Basics():
|
|
15
|
+
"create script, run script, output result, check result"
|
|
16
|
+
app.new_db(":memory:")
|
|
17
|
+
with tempfile.TemporaryDirectory(prefix='.tmp') as tmp_dir:
|
|
18
|
+
print(tmp_dir)
|
|
19
|
+
tmp_file = str(pathlib.PurePath(tmp_dir, 'sqlite_bro_test_Basics.tmp'))
|
|
20
|
+
welcome_text = """
|
|
21
|
+
create table item (ItemNo, Description,Kg , PRIMARY KEY (ItemNo));
|
|
22
|
+
INSERT INTO item values("T","Ford",1000);
|
|
23
|
+
INSERT INTO item select "A","Merced",1250 union all select "W","Wheel",9 ;
|
|
24
|
+
.once %s
|
|
25
|
+
select ItemNo, Description, 1000*Kg Gramm from item order by ItemNo desc;
|
|
26
|
+
.import %s in_this_table""" % (tmp_file, tmp_file)
|
|
27
|
+
app.n.new_query_tab("Welcome", welcome_text)
|
|
28
|
+
app.run_tab()
|
|
29
|
+
app.close_db
|
|
30
|
+
|
|
31
|
+
file_encoding = sqlite_bro.guess_encoding(tmp_file)[0]
|
|
32
|
+
with io.open(tmp_file, mode='rt', encoding=file_encoding) as f:
|
|
33
|
+
result = f.readlines()
|
|
34
|
+
assert len(result) == 4
|
|
35
|
+
assert result[-1] == "A,Merced,1250000\n"
|
|
36
|
+
|
|
37
|
+
def test_Outputs():
|
|
38
|
+
"testing .output, .print, .header, .separator"
|
|
39
|
+
|
|
40
|
+
app.new_db(":memory:")
|
|
41
|
+
with tempfile.TemporaryDirectory(prefix='.tmp') as tmp_dir:
|
|
42
|
+
print(tmp_dir)
|
|
43
|
+
tmp_file = str(pathlib.PurePath(tmp_dir, 'sqlite_bro_test_Output.tmp'))
|
|
44
|
+
welcome_text = """
|
|
45
|
+
create table item (ItemNo, Description , PRIMARY KEY (ItemNo));
|
|
46
|
+
INSERT INTO item values("DS","Citroën");
|
|
47
|
+
.output %s
|
|
48
|
+
.separator ;
|
|
49
|
+
.headers off
|
|
50
|
+
.print a;b
|
|
51
|
+
select * from item;
|
|
52
|
+
.headers on
|
|
53
|
+
.separator !
|
|
54
|
+
select * from item;
|
|
55
|
+
.import %s in_this_table""" % (tmp_file, tmp_file)
|
|
56
|
+
app.n.new_query_tab("Welcome", welcome_text)
|
|
57
|
+
app.run_tab()
|
|
58
|
+
app.close_db
|
|
59
|
+
|
|
60
|
+
file_encoding = sqlite_bro.guess_encoding(tmp_file)[0]
|
|
61
|
+
with io.open(tmp_file, mode='rt', encoding=file_encoding) as f:
|
|
62
|
+
result = f.readlines()
|
|
63
|
+
print(result)
|
|
64
|
+
assert len(result) == 4
|
|
65
|
+
assert result[0] == "a;b\n"
|
|
66
|
+
assert result[1] == "DS;Citroën\n"
|
|
67
|
+
assert result[2] == "ItemNo!Description\n"
|
|
68
|
+
assert result[3] == "DS!Citroën\n"
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = '0.13.1'
|
|
File without changes
|
|
File without changes
|
|
File without changes
|