Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
sagemath
GitHub Repository: sagemath/sage
Path: blob/develop/src/sage/databases/sql_db.py
4081 views
1
r"""
2
Relational (sqlite) Databases Module
3
4
INFO:
5
6
This module implements classes (SQLDatabase and SQLQuery (pythonic
7
implementation for the user with little or no knowledge of sqlite))
8
that wrap the basic functionality of sqlite.
9
10
Databases are constructed via a triple indexed dictionary called a
11
skeleton. A skeleton should be constructed to fit the following format::
12
13
| - skeleton -- a triple-indexed dictionary
14
| - outer key -- table name
15
| - inner key -- column name
16
| - inner inner key -- one of the following:
17
| - ``primary_key`` -- boolean, whether column has been set as
18
primary key
19
| - ``index`` -- boolean, whether column has been set as index
20
| - ``unique`` -- boolean, whether column has been set as unique
21
| - ``sql`` -- one of ``'TEXT'``, ``'BOOLEAN'``, ``'INTEGER'``,
22
``'REAL'``, or other user defined type
23
24
An example skeleton of a database with one table, that table with one
25
column::
26
27
{'table1':{'col1':{'primary_key':False, 'index':True, 'sql':'REAL'}}}
28
29
SQLDatabases can also be constructed via the add, drop, and commit
30
functions. The vacuum function is also useful for restoring hard disk space
31
after a database has shrunk in size.
32
33
A SQLQuery can be constructed by providing a query_dict, which is a
34
dictionary with the following sample format::
35
36
{'table_name': 'tblname', 'display_cols': ['col1', 'col2', 'col3'], 'expression':[col, operator, value]}
37
38
Finally a SQLQuery also allows the user to directly input the query
39
string for a database, and also supports the '?' syntax by allowing an
40
argument for a tuple of parameters to query.
41
42
For full details, please see the tutorial. sage.graphs.graph_database.py
43
is an example of implementing a database class in Sage using this
44
interface.
45
46
AUTHORS:
47
48
- R. Andrew Ohana (2011-07-16): refactored and rewrote most of the code;
49
merged the Generic classes into the non-Generic versions; changed the
50
skeleton format to include a boolean indicating whether the column stores
51
unique keys; changed the index names so as to avoid potential ambiguity
52
53
- Emily A. Kirkman (2008-09-20): added functionality to generate plots and
54
reformat output in show
55
56
- Emily A. Kirkman and Robert L. Miller (2007-06-17): initial version
57
"""
58
# FUTURE TODOs (Ignore for now):
59
# - order by clause in query strings
60
# - delete from query containing joins
61
# - add data by column
62
# - wrap sqlalchemy
63
# - create query interface (with interact)
64
# - allow kwds arguments to SQLQuery (like GraphQuery)
65
66
# ****************************************************************************
67
# Copyright (C) 2011 R. Andrew Ohana <[email protected]>
68
# Copyright (C) 2007 Emily A. Kirkman
69
# Robert L. Miller
70
#
71
# This program is free software: you can redistribute it and/or modify
72
# it under the terms of the GNU General Public License as published by
73
# the Free Software Foundation, either version 2 of the License, or
74
# (at your option) any later version.
75
# https://www.gnu.org/licenses/
76
# ****************************************************************************
77
78
import sqlite3 as sqlite
79
import os
80
import re
81
from sage.misc.temporary_file import tmp_filename
82
from sage.structure.sage_object import SageObject
83
84
sqlite_keywords = ['ABORT','ACTION','ADD','AFTER','ALL','ALTER','ANALYZE',
85
'AND','AS','ASC','ATTACH','AUTOINCREMENT','BEFORE','BEGIN','BETWEEN','BY',
86
'CASCADE','CASE','CAST','CHECK','COLLATE','COLUMN','COMMIT','CONFLICT',
87
'CONSTRAINT','CREATE','CROSS','CURRENT_DATE','CURRENT_TIME',
88
'CURRENT_TIMESTAMP','DATABASE','DEFAULT','DEFERRABLE','DEFERRED','DELETE',
89
'DESC','DETACH','DISTINCT','DROP','EACH','ELSE','END','ESCAPE','EXCEPT',
90
'EXCLUSIVE','EXISTS','EXPLAIN','FAIL','FOR','FOREIGN','FROM','FULL',
91
'GLOB','GROUP','HAVING','IF','IGNORE','IMMEDIATE','IN','INDEX','INDEXED',
92
'INITIALLY','INNER','INSERT','INSTEAD','INTERSECT','INTO','IS','ISNULL',
93
'JOIN','KEY','LEFT','LIKE','LIMIT','MATCH','NATURAL','NO','NOT','NOTNULL',
94
'NULL','OF','OFFSET','ON','OR','ORDER','OUTER','PLAN','PRAGMA','PRIMARY',
95
'QUERY','RAISE','REFERENCES','REGEXP','REINDEX','RELEASE','RENAME',
96
'REPLACE','RESTRICT','RIGHT','ROLLBACK','ROW','SAVEPOINT','SELECT','SET',
97
'TABLE','TEMP','TEMPORARY','THEN','TO','TRANSACTION','TRIGGER','UNION',
98
'UNIQUE','UPDATE','USING','VACUUM','VALUES','VIEW','VIRTUAL','WHEN',
99
'WHERE']
100
101
102
def regexp(expr, item):
103
"""
104
Function to define regular expressions in pysqlite.
105
106
OUTPUT:
107
108
- ``True`` if parameter ``item`` matches the regular expression
109
parameter ``expr``
110
- ``False`` otherwise (i.e.: no match)
111
112
REFERENCES:
113
114
- [Ha2005]_
115
116
EXAMPLES::
117
118
sage: from sage.databases.sql_db import regexp
119
sage: regexp('.s.*','cs')
120
True
121
sage: regexp('.s.*','ccs')
122
False
123
sage: regexp('.s.*','cscccc')
124
True
125
"""
126
r = re.compile(expr)
127
return r.match(item) is not None
128
129
130
def verify_type(type):
131
"""
132
Verify that the specified ``type`` is one of the allowed strings.
133
134
EXAMPLES::
135
136
sage: from sage.databases.sql_db import verify_type
137
sage: verify_type('INT')
138
True
139
sage: verify_type('int')
140
True
141
sage: verify_type('float')
142
Traceback (most recent call last):
143
...
144
TypeError: float is not a legal type.
145
"""
146
types = ['INTEGER','INT','BOOLEAN','REAL','TEXT','BOOL','BLOB','NOTYPE']
147
if type.upper() not in types:
148
raise TypeError('%s is not a legal type.' % type)
149
return True
150
151
152
def verify_column(col_dict):
153
"""
154
Verify that ``col_dict`` is in proper format, and return a dict with
155
default values filled in. Proper format::
156
157
{'primary_key':False, 'index':False, 'unique': False, 'sql':'REAL'}
158
159
EXAMPLES::
160
161
sage: from sage.databases.sql_db import verify_column
162
sage: col = {'sql':'BOOLEAN'}
163
sage: verify_column(col)
164
{'index': False, 'primary_key': False, 'sql': 'BOOLEAN', 'unique': False}
165
sage: col = {'primary_key':True, 'sql':'INTEGER'}
166
sage: verify_column(col)
167
{'index': True, 'primary_key': True, 'sql': 'INTEGER', 'unique': True}
168
sage: verify_column({})
169
Traceback (most recent call last):
170
...
171
ValueError: SQL type must be declared, e.g. {'sql':'REAL'}.
172
"""
173
d = {}
174
d['primary_key'] = col_dict.get('primary_key', False)
175
d['index'] = col_dict.get('index', False) or d['primary_key']
176
d['unique'] = col_dict.get('unique', False) or d['primary_key']
177
if 'sql' not in col_dict:
178
raise ValueError("SQL type must be declared, e.g. {'sql':'REAL'}.")
179
if verify_type(col_dict['sql']):
180
d['sql'] = col_dict['sql']
181
return d
182
183
184
def verify_operator(operator):
185
"""
186
Check that ``operator`` is one of the allowed strings.
187
Legal operators include the following strings:
188
189
- '='
190
- '<='
191
- '>='
192
- '<'
193
- '>'
194
- '<>'
195
- 'like'
196
- 'regexp'
197
- 'is null'
198
- 'is not null'
199
200
EXAMPLES::
201
202
sage: from sage.databases.sql_db import verify_operator
203
sage: verify_operator('<=')
204
True
205
sage: verify_operator('regexp')
206
True
207
sage: verify_operator('is null')
208
True
209
sage: verify_operator('not_an_operator')
210
Traceback (most recent call last):
211
...
212
TypeError: not_an_operator is not a legal operator.
213
"""
214
binaries = ['=','<=','>=','like','<','>','<>','regexp']
215
unaries = ['is null','is not null']
216
if operator not in binaries and operator not in unaries:
217
raise TypeError('%s is not a legal operator.' % operator)
218
return True
219
220
221
def construct_skeleton(database):
222
"""
223
Construct a database skeleton from the sql data. The skeleton data
224
structure is a triple indexed dictionary of the following format::
225
226
| - skeleton -- a triple-indexed dictionary
227
| - outer key -- table name
228
| - inner key -- column name
229
| - inner inner key -- one of the following:
230
| - ``primary_key`` -- boolean, whether column has been set as
231
primary key
232
| - ``index`` -- boolean, whether column has been set as index
233
| - ``unique`` -- boolean, whether column has been set as unique
234
| - ``sql`` -- one of ``'TEXT'``, ``'BOOLEAN'``, ``'INTEGER'``,
235
``'REAL'``, or other user defined type
236
237
An example skeleton of a database with one table, that table with one
238
column::
239
240
{'table1':{'col1':{'primary_key':False, 'unique': True, 'index':True, 'sql':'REAL'}}}
241
242
EXAMPLES::
243
244
sage: G = SQLDatabase(GraphDatabase().__dblocation__, False)
245
sage: from sage.databases.sql_db import construct_skeleton
246
sage: sorted(construct_skeleton(G))
247
['aut_grp', 'degrees', 'graph_data', 'misc', 'spectrum']
248
"""
249
skeleton = {}
250
cur = database.__connection__.cursor()
251
exe = cur.execute("SELECT name FROM sqlite_master WHERE TYPE='table'")
252
for table in exe.fetchall():
253
skeleton[table[0]] = {}
254
exe1 = cur.execute("PRAGMA table_info(%s)" % table[0])
255
for col in exe1.fetchall():
256
if not col[2]:
257
typ = 'NOTYPE'
258
else:
259
typ = col[2]
260
skeleton[table[0]][col[1]] = {'sql':typ,
261
'primary_key':(col[5] != 0), 'index':(col[5] != 0), 'unique': False}
262
exe2 = cur.execute("PRAGMA index_list(%s)" % table[0])
263
for col in exe2.fetchall():
264
if col[1].find('sqlite') == -1:
265
if os.path.basename(database.__dblocation__) == 'graphs.db':
266
name = col[1]
267
else:
268
name = col[1][len(table[0])+3:]
269
skeleton[table[0]][name]['index'] = True
270
skeleton[table[0]][name]['unique'] = bool(col[2])
271
else:
272
name = cur.execute("PRAGMA index_info(%s)" % col[1])
273
name = name.fetchone()[2]
274
skeleton[table[0]][name]['unique'] = bool(col[2])
275
return skeleton
276
277
278
p = 0
279
280
281
def _create_print_table(cur, col_titles, **kwds):
282
r"""
283
Create a nice printable table from the cursor given with the given
284
column titles.
285
286
KEYWORDS:
287
288
- ``max_field_size`` -- how wide each field can be
289
- ``format_cols`` -- dictionary that allows the user to specify the
290
format of a column's output by supplying a function. The format of
291
the dictionary is::
292
293
{'column_name':(lambda x: format_function(x))}
294
295
- ``plot_cols`` -- dictionary that allows the user to specify that a
296
plot should be drawn by the object generated by a data slice. Note
297
that plot kwds are permitted. The dictionary format is::
298
299
{'column_name':((lambda x: plot_function(x)), **kwds)}
300
301
- ``relabel_cols`` -- dictionary to specify a relabeling of column
302
headers. The dictionary format is::
303
304
{'table_name':{'old_col_name':'new_col_name'}}
305
306
- ``id_col`` -- reference to a column that can be used as an object
307
identifier for each row
308
309
- ``html_table`` -- boolean that if ``True`` creates an html table instead of
310
a print table. Always set to ``True`` in the notebook
311
312
EXAMPLES::
313
314
sage: DB = SQLDatabase()
315
sage: DB.create_table('simon',{'a1':{'sql':'bool', 'primary_key':False}, 'b2':{'sql':'int'}})
316
sage: DB.add_data('simon',[(0,0),(1,1),(1,2)])
317
sage: r = SQLQuery(DB, {'table_name':'simon', 'display_cols':['a1'], 'expression':['b2','<=', 6]})
318
sage: from sage.databases.sql_db import _create_print_table
319
sage: cur = r.__database__.__connection__.cursor()
320
sage: exe = cur.execute(r.__query_string__, r.__param_tuple__)
321
sage: _create_print_table(cur, [des[0] for des in cur.description])
322
'a1 \n--------------------\n0 \n1 \n1 '
323
"""
324
fcol_index = []
325
pcol_index = []
326
327
if 'format_cols' in kwds:
328
fcol_map = []
329
for col in kwds['format_cols']:
330
fcol_map.append(kwds['format_cols'][col])
331
fcol_index.append(col_titles.index(col))
332
if 'plot_cols' in kwds:
333
pcol_map = []
334
for col in kwds['plot_cols']:
335
pcol_map.append(kwds['plot_cols'][col])
336
pcol_index.append(col_titles.index(col))
337
338
max_field_size = kwds['max_field_size'] if 'max_field_size' in kwds \
339
else 20
340
id_col_index = col_titles.index(kwds['id_col']) if 'id_col' in kwds \
341
else None
342
343
if 'relabel_cols' in kwds:
344
relabel_cols = kwds['relabel_cols']
345
for i in range(len(col_titles)):
346
try:
347
col_titles[i] = relabel_cols[col_titles[i]]
348
except KeyError:
349
continue
350
global p
351
p = 0
352
353
def row_str(row, html):
354
f = 0
355
global p
356
cur_str = []
357
for index in range(len(col_titles)):
358
if index in pcol_index:
359
if html:
360
plot = pcol_map[p % len(pcol_map)](row[index])
361
plot.save('%d.png' % p, figsize=[1,1])
362
field_val = ' <td bgcolor=white align=center> ' \
363
+ '%s <br> <img src="cell://%d.png"> ' % (row[index],p) \
364
+ '</td>\n'
365
p += 1
366
else:
367
raise NotImplementedError('Cannot display plot on '
368
'command line.')
369
else:
370
if index in fcol_index:
371
if id_col_index is None:
372
field_val = fcol_map[f](row[index])
373
else:
374
field_val = fcol_map[f](row[index], row[id_col_index])
375
f += 1
376
else:
377
field_val = row[index]
378
if html:
379
field_val = ' <td bgcolor=white align=center> ' \
380
+ str(field_val) + ' </td>\n'
381
else:
382
field_val = str(field_val).ljust(max_field_size)
383
cur_str.append(field_val)
384
return ' '.join(cur_str)
385
386
if 'html_table' in kwds and kwds['html_table']:
387
# Notebook Version
388
ret = '<html><!--notruncate-->\n'
389
ret += ' <table bgcolor=lightgrey cellpadding=0>\n'
390
ret += ' <tr>\n <td bgcolor=white align=center> '
391
ret += (' </td>\n <td bgcolor=white '
392
+ 'align=center> ').join(col_titles)
393
ret += ' </td>\n </tr>\n'
394
ret += '\n'.join([' <tr>\n ' + row_str(row, True) + ' </tr>'
395
for row in cur])
396
ret += '\n </table>\n</html>'
397
else:
398
# Command Prompt Version
399
ret = ' '.join([col.ljust(max_field_size) for col in col_titles])
400
ret += '\n' + '-' * max_field_size * len(col_titles) + '\n'
401
ret += '\n'.join([row_str(row, False) for row in cur])
402
return ret
403
404
405
class SQLQuery(SageObject):
406
def __init__(self, database, *args, **kwds):
407
"""
408
A query for a SQLite database.
409
410
INPUT:
411
412
- ``database`` -- a SQLDatabase object
413
- ``query_dict`` -- dictionary specifying the query itself. The
414
format is::
415
416
{'table_name':'tblname', 'display_cols':['col1', 'col2','col3'], 'expression': [col, operator, value]}
417
418
NOTE:
419
Every SQL type we are using is ultimately represented as a string,
420
so if you wish to save actual strings to a database, you actually
421
need to do something like: '"value"'.
422
423
See the documentation of ``SQLDatabase`` for an introduction to using
424
SQLite in Sage.
425
426
EXAMPLES::
427
428
sage: D = SQLDatabase()
429
sage: D.create_table('simon',{'a1':{'sql':'bool', 'primary_key':False}, 'b2':{'sql':'int'}})
430
sage: D.add_data('simon',[(0,0),(1,2),(2,4)])
431
sage: r = SQLQuery(D, {'table_name':'simon', 'display_cols':['a1'], 'expression':['b2','<=', 3]})
432
sage: r.show()
433
a1
434
--------------------
435
0
436
1
437
438
Test that :issue:`27562` is fixed::
439
440
sage: D = SQLDatabase()
441
sage: r = SQLQuery(D, {'table_name':'simon', 'display_cols':['a1'], 'expression':['b2','<=', 3]})
442
Traceback (most recent call last):
443
...
444
ValueError: Database has no table simon
445
sage: D.create_table('simon',{'a1':{'sql':'bool', 'primary_key':False}, 'b2':{'sql':'int'}})
446
sage: D.create_table('simon',{'a1':{'sql':'bool', 'primary_key':False}, 'b2':{'sql':'int'}})
447
Traceback (most recent call last):
448
...
449
ValueError: Database already has a table named simon
450
sage: SQLQuery(D, {'table_name':'simon', 'display_cols':['a1'], 'expression':['c1','>',2]})
451
Traceback (most recent call last):
452
...
453
ValueError: Table has no column c1
454
"""
455
if not isinstance(database, SQLDatabase):
456
raise TypeError('%s is not a valid SQLDatabase' % database)
457
self.__database__ = database
458
total_args = len(args) + len(kwds)
459
if total_args == 0:
460
self.__query_string__ = ''
461
self.__param_tuple__ = tuple()
462
self.__query_dict__ = {}
463
return
464
for x in args:
465
if isinstance(x,dict):
466
if 'query_dict' not in kwds:
467
kwds['query_dict'] = x
468
elif isinstance(x, str):
469
if 'query_string' not in kwds:
470
kwds['query_string'] = x
471
elif isinstance(x, tuple):
472
if 'param_tuple' not in kwds:
473
kwds['param_tuple'] = x
474
if total_args > 2 or not ('query_dict' in kwds or
475
'query_string' in kwds) or ('query_dict' in kwds and
476
('param_tuple' in kwds or 'query_string' in kwds)):
477
raise ValueError('Query must be constructed with either a '
478
+ 'dictionary or a string and tuple')
479
480
if 'query_dict' in kwds:
481
query_dict = kwds['query_dict']
482
else:
483
self.__query_string__ = kwds['query_string']
484
if 'param_tuple' in kwds:
485
self.__param_tuple__ = tuple(str(x) for x in kwds['param_tuple'])
486
else:
487
self.__param_tuple__ = tuple()
488
return
489
if query_dict:
490
skel = database.__skeleton__
491
if query_dict['table_name'] not in skel:
492
raise ValueError("Database has no table %s"
493
% query_dict['table_name'])
494
table_name = query_dict['table_name']
495
if query_dict['display_cols'] is not None:
496
for column in query_dict['display_cols']:
497
if column not in skel[table_name]:
498
raise ValueError("Table has no column %s" % column)
499
if query_dict['expression'][0] not in skel[table_name]:
500
raise ValueError("Table has no column %s"
501
% query_dict['expression'][0])
502
503
self.__query_dict__ = query_dict
504
self.__param_tuple__ = (str(query_dict['expression'][2]),)
505
verify_operator(query_dict['expression'][1])
506
if query_dict['display_cols'] is None:
507
self.__query_string__ = 'SELECT , FROM %s WHERE ' % table_name \
508
+ '%s.%s ' % (table_name, query_dict['expression'][0]) \
509
+ '%s ?' % query_dict['expression'][1]
510
else:
511
query_dict['display_cols'] = ['%s.%s' % (table_name, x)
512
for x in query_dict['display_cols']]
513
self.__query_string__ = 'SELECT ' \
514
+ ', '.join(query_dict['display_cols']) + ' FROM ' \
515
+ '%s WHERE %s.' % (table_name, table_name) \
516
+ '%s ' % query_dict['expression'][0] \
517
+ '%s ?' % query_dict['expression'][1]
518
else:
519
self.__query_dict__ = {}
520
self.__param_tuple__ = tuple()
521
self.__query_string__ = ''
522
523
def __repr__(self):
524
"""
525
Override the print output to display useful info regarding the
526
query.
527
528
EXAMPLES::
529
530
sage: G = GraphDatabase()
531
sage: q = 'SELECT graph_id,graph6,num_vertices,num_edges FROM graph_data WHERE graph_id<=(?) AND num_vertices=(?)'
532
sage: param = (22,5)
533
sage: SQLQuery(G,q,param)
534
Query for sql database: ...graphs.db
535
Query string: SELECT graph_id,graph6,num_vertices,num_edges FROM
536
graph_data WHERE graph_id<=(?) AND num_vertices=(?)
537
Parameter tuple: ('22', '5')
538
sage: r = 'SELECT graph6 FROM graph_data WHERE num_vertices<=3'
539
sage: SQLQuery(G,r)
540
Query for sql database: ...graphs.db
541
Query string: SELECT graph6 FROM graph_data WHERE num_vertices<=3
542
"""
543
if not self.__query_string__:
544
return 'Empty query on %s.' % self.__database__.__dblocation__
545
return "Query for sql database: %s" % self.__database__.__dblocation__ \
546
+ "\nQuery string: %s" % self.__query_string__ \
547
+ ("\nParameter tuple: %s" % str(self.__param_tuple__) if
548
self.__param_tuple__ else "")
549
550
def get_query_string(self):
551
"""
552
Return a copy of the query string.
553
554
EXAMPLES::
555
556
sage: G = GraphDatabase()
557
sage: q = 'SELECT graph_id,graph6,num_vertices,num_edges FROM graph_data WHERE graph_id<=(?) AND num_vertices=(?)'
558
sage: param = (22,5)
559
sage: SQLQuery(G,q,param).get_query_string()
560
'SELECT graph_id,graph6,num_vertices,num_edges FROM graph_data
561
WHERE graph_id<=(?) AND num_vertices=(?)'
562
sage: r = 'SELECT graph6 FROM graph_data WHERE num_vertices<=3'
563
sage: SQLQuery(G,r).get_query_string()
564
'SELECT graph6 FROM graph_data WHERE num_vertices<=3'
565
"""
566
from copy import copy
567
return copy(self.__query_string__)
568
569
def __iter__(self):
570
"""
571
Return an iterator over the results of the query.
572
573
EXAMPLES::
574
575
sage: G = GraphDatabase()
576
sage: q = 'SELECT graph_id,graph6 FROM graph_data WHERE num_vertices=(?)'
577
sage: param = (5,)
578
sage: Q = SQLQuery(G,q,param)
579
sage: it = Q.__iter__()
580
sage: next(it)
581
(18, 'D??')
582
sage: next(it)
583
(19, 'D?C')
584
sage: skip = [next(it) for _ in range(15)]
585
sage: next(it)
586
(35, 'DBk')
587
"""
588
try:
589
cur = self.__database__.__connection__.cursor()
590
return cur.execute(self.__query_string__, self.__param_tuple__)
591
except sqlite.OperationalError:
592
raise RuntimeError('Failure to fetch query.')
593
594
def query_results(self):
595
"""
596
Run the query by executing the ``__query_string__``. Return the
597
results of the query in a list.
598
599
EXAMPLES::
600
601
sage: G = GraphDatabase()
602
sage: q = 'SELECT graph_id,graph6,num_vertices,num_edges FROM graph_data WHERE graph_id<=(?) AND num_vertices=(?)'
603
sage: param = (22,5)
604
sage: Q = SQLQuery(G,q,param)
605
sage: Q.query_results()
606
[(18, 'D??', 5, 0), (19, 'D?C', 5, 1), (20, 'D?K', 5, 2),
607
(21, 'D@O', 5, 2), (22, 'D?[', 5, 3)]
608
sage: R = SQLQuery(G,{'table_name':'graph_data', 'display_cols':['graph6'], 'expression':['num_vertices','=',4]})
609
sage: R.query_results()
610
[('C?',), ('C@',), ('CB',), ('CK',), ('CF',), ('CJ',),
611
('CL',), ('CN',), ('C]',), ('C^',), ('C~',)]
612
"""
613
return list(self)
614
615
def show(self, **kwds):
616
"""
617
Display the result of the query in table format.
618
619
KEYWORDS:
620
621
- ``max_field_size`` -- how wide each field can be
622
- ``format_cols`` -- dictionary that allows the user to specify the
623
format of a column's output by supplying a function. The format of
624
the dictionary is::
625
626
{'column_name':(lambda x: format_function(x))}
627
628
- ``plot_cols`` -- dictionary that allows the user to specify that a
629
plot should be drawn by the object generated by a data slice. Note
630
that plot kwds are permitted. The dictionary format is::
631
632
{'column_name':((lambda x: plot_function(x)), **kwds)}
633
634
- ``relabel_cols`` -- dictionary to specify a relabeling of column
635
headers. The dictionary format is::
636
637
{'table_name':{'old_col_name':'new_col_name'}}
638
639
- ``id_col`` -- reference to a column that can be used as an object
640
identifier for each row
641
642
EXAMPLES::
643
644
sage: DB = SQLDatabase()
645
sage: DB.create_table('simon',{'a1':{'sql':'bool', 'primary_key':False}, 'b2':{'sql':'int'}})
646
sage: DB.add_data('simon',[(0,0),(1,1),(1,2)])
647
sage: r = SQLQuery(DB, {'table_name':'simon', 'display_cols':['a1'], 'expression':['b2','<=', 6]})
648
sage: r.show()
649
a1
650
--------------------
651
0
652
1
653
1
654
sage: D = GraphDatabase()
655
sage: from sage.graphs.graph_database import valid_kwds, data_to_degseq
656
sage: relabel = {}
657
sage: for col in valid_kwds:
658
....: relabel[col] = ' '.join([word.capitalize() for word in col.split('_')])
659
sage: q = GraphQuery(display_cols=['graph6','degree_sequence'], num_vertices=4)
660
sage: SQLQuery.show(q, format_cols={'degree_sequence':(lambda x,y: data_to_degseq(x,y))}, relabel_cols=relabel, id_col='graph6')
661
Graph6 Degree Sequence
662
----------------------------------------
663
C? [0, 0, 0, 0]
664
C@ [0, 0, 1, 1]
665
CB [0, 1, 1, 2]
666
CF [1, 1, 1, 3]
667
CJ [0, 2, 2, 2]
668
CK [1, 1, 1, 1]
669
CL [1, 1, 2, 2]
670
CN [1, 2, 2, 3]
671
C] [2, 2, 2, 2]
672
C^ [2, 2, 3, 3]
673
C~ [3, 3, 3, 3]
674
"""
675
if not self.__query_string__:
676
return self.__database__.show()
677
678
try:
679
cur = self.__database__.__connection__.cursor()
680
cur.execute(self.__query_string__, self.__param_tuple__)
681
except Exception:
682
raise RuntimeError('Failure to fetch query.')
683
684
print(_create_print_table(cur, [des[0] for des in cur.description],
685
**kwds))
686
687
def __copy__(self):
688
"""
689
Return a copy of itself.
690
691
EXAMPLES::
692
693
sage: G = GraphDatabase()
694
sage: Q = SQLQuery(G, {'table_name':'graph_data', 'display_cols':['graph_id','graph6','num_vertices'], 'expression':['num_edges','<',3]})
695
sage: R = copy(Q)
696
sage: R.__query_string__ = ''
697
sage: Q.__query_string__ == ''
698
False
699
"""
700
d = SQLQuery(self.__database__)
701
d.__query_dict__ = self.__query_dict__
702
d.__query_string__ = self.__query_string__
703
d.__param_tuple__ = self.__param_tuple__
704
return d
705
706
def intersect(self, other, join_table=None, join_dict=None,
707
in_place=False):
708
"""
709
Return a new ``SQLQuery`` that is the intersection of ``self`` and
710
``other``. ``join_table`` and ``join_dict`` can be ``None`` iff the
711
two queries only search one table in the database. All display columns
712
will be concatenated in order: ``self`` display cols + other display cols.
713
714
INPUT:
715
716
- ``other`` -- the ``SQLQuery`` to intersect with
717
- ``join_table`` -- base table to join on (This table should have at
718
least one column in each table to join on).
719
- ``join_dict`` -- dictionary that represents the join structure for
720
the new query. (Must include a mapping for all tables, including
721
those previously joined in either query). Structure is given by::
722
723
{'join_table1':('corr_base_col1', 'col1'), 'join_table2':('corr_base_col2', 'col2')}
724
725
where ``join_table1`` is to be joined with ``join_table`` on
726
``join_table.corr_base_col1 = join_table1.col1``
727
728
EXAMPLES::
729
730
sage: DB = SQLDatabase()
731
sage: DB.create_table('simon',{'a1':{'sql':'bool'}, 'b2':{'sql':'int'}})
732
sage: DB.create_table('lucy',{'a1':{'sql':'bool'}, 'b2':{'sql':'int'}})
733
sage: DB.add_data('simon', [(0,5),(1,4)])
734
sage: DB.add_data('lucy', [(1,1),(1,4)])
735
sage: q = SQLQuery(DB, {'table_name':'lucy', 'display_cols':['b2'], 'expression':['a1','=',1]})
736
sage: r = SQLQuery(DB, {'table_name':'simon', 'display_cols':['a1'], 'expression':['b2','<=', 6]})
737
sage: s = q.intersect(r, 'simon', {'lucy':('a1','a1')})
738
sage: s.get_query_string()
739
'SELECT lucy.b2,simon.a1 FROM simon INNER JOIN lucy ON
740
simon.a1=lucy.a1 WHERE ( lucy.a1 = ? ) AND ( simon.b2 <= ? )'
741
sage: s.query_results()
742
[(1, 1), (4, 1)]
743
sage: s = q.intersect(r)
744
Traceback (most recent call last):
745
...
746
ValueError: Input queries query different tables but join
747
parameters are NoneType
748
sage: s.__query_string__ == q.__query_string__
749
False
750
sage: q.intersect(r, 'simon', {'lucy':('a1','a1')}, True)
751
sage: q.__query_string__ == s.__query_string__
752
True
753
"""
754
if self.__query_dict__ is None or other.__query_dict__ is None:
755
raise RuntimeError('Queries must be constructed using a '
756
+ 'dictionary in order to be intersected.')
757
if self.__database__ != other.__database__:
758
raise TypeError('Queries %s and %s must be ' % (self, other)
759
+ 'attached to the same database.')
760
761
if in_place:
762
if not self.__query_string__:
763
self.__query_string__ = other.__query_string__
764
self.__param_tuple__ = other.__param_tuple__
765
elif not other.__query_string__:
766
return
767
else:
768
self._merge_queries(other, self, join_table, join_dict, 'AND')
769
else:
770
from copy import copy
771
if not self.__query_string__:
772
return copy(other)
773
if not other.__query_string__:
774
return copy(self)
775
return self._merge_queries(other, copy(self), join_table,
776
join_dict, 'AND')
777
778
def _merge_queries(self, other, ret, join_table, join_dict, operator):
779
"""
780
The query ``ret`` is set to a new ``SQLQuery`` that is combines self
781
and other through ``operator``. The other arguments are the same as
782
``intersect`` and ``union``.
783
784
EXAMPLES::
785
786
sage: DB = SQLDatabase()
787
sage: DB.create_table('simon',{'a1':{'sql':'bool'}, 'b2':{'sql':'int'}})
788
sage: DB.create_table('lucy',{'a1':{'sql':'bool'}, 'b2':{'sql':'int'}})
789
sage: DB.add_data('simon', [(0,5),(1,4)])
790
sage: DB.add_data('lucy', [(1,1),(1,4)])
791
sage: q = SQLQuery(DB, {'table_name':'lucy', 'display_cols':['b2'], 'expression':['a1','=',1]})
792
sage: r = SQLQuery(DB, {'table_name':'simon', 'display_cols':['a1'], 'expression':['b2','<=', 6]})
793
sage: s = q._merge_queries(r, copy(q), 'simon', {'lucy':('a1','a1')}, 'OR')
794
sage: s.get_query_string()
795
'SELECT lucy.b2,simon.a1 FROM simon INNER JOIN lucy ON
796
simon.a1=lucy.a1 WHERE ( lucy.a1 = ? ) OR ( simon.b2 <= ? )'
797
sage: s.query_results()
798
[(1, 1), (4, 1)]
799
"""
800
if join_table is None or join_dict is None:
801
pattern = ' JOIN '
802
if re.search(pattern, self.__query_string__) \
803
or re.search(pattern, other.__query_string__):
804
raise TypeError('Input queries have joins but join '
805
+ 'parameters are NoneType')
806
s = ((self.__query_string__).upper()).split('FROM ')
807
o = ((other.__query_string__).upper()).split('FROM ')
808
s = s[1].split(' WHERE ')
809
o = o[1].split(' WHERE ')
810
if s[0] != o[0]:
811
raise ValueError('Input queries query different tables but '
812
+ 'join parameters are NoneType')
813
814
# inner join clause
815
if join_dict is not None:
816
joins = join_table
817
for table in join_dict:
818
joins += ' INNER JOIN %s ON %s.' % (table, join_table) \
819
+ '%s=%s.' % (join_dict[table][0], table) \
820
+ '%s ' % join_dict[table][1]
821
ret.__query_string__ = re.sub(' FROM .* WHERE ', ' FROM ' + joins
822
+ 'WHERE ', self.__query_string__)
823
824
# concatenate display cols
825
disp1 = ret.__query_string__.split(' FROM')
826
disp2 = other.__query_string__.split(' FROM')
827
disp1.insert(1, ',%s FROM' % disp2[0].split('SELECT ')[1])
828
new_query = ''.join(disp1)
829
830
# concatenate where clause
831
new_query = re.sub(' WHERE ', ' WHERE ( ',
832
new_query)
833
new_query += re.sub('^.* WHERE ', f' ) {operator} ( ',
834
other.__query_string__)
835
ret.__query_string__ = new_query + ' )'
836
837
ret.__param_tuple__ = self.__param_tuple__ + other.__param_tuple__
838
839
return ret
840
841
def union(self, other, join_table=None, join_dict=None, in_place=False):
842
"""
843
Return a new ``SQLQuery`` that is the union of ``self`` and ``other``.
844
``join_table`` and ``join_dict`` can be ``None`` iff the two queries
845
only search one table in the database. All display columns will be
846
concatenated in order: ``self`` display cols + other display cols.
847
848
INPUT:
849
850
- ``other`` -- the ``SQLQuery`` to union with
851
- ``join_table`` -- base table to join on (This table should have at
852
least one column in each table to join on).
853
- ``join_dict`` -- dictionary that represents the join structure for
854
the new query. (Must include a mapping for all tables, including
855
those previously joined in either query). Structure is given by::
856
857
{'join_table1':('corr_base_col1', 'col1'), 'join_table2':('corr_base_col2', 'col2')}
858
859
where ``join_table1`` is to be joined with ``join_table`` on
860
``join_table.corr_base_col1=join_table1.col1``
861
862
EXAMPLES::
863
864
sage: DB = SQLDatabase()
865
sage: DB.create_table('simon',{'a1':{'sql':'bool'}, 'b2':{'sql':'int'}})
866
sage: DB.create_table('lucy',{'a1':{'sql':'bool'}, 'b2':{'sql':'int'}})
867
sage: DB.add_data('simon', [(0,5),(1,4)])
868
sage: DB.add_data('lucy', [(1,1),(1,4)])
869
sage: q = SQLQuery(DB, {'table_name':'lucy', 'display_cols':['b2'], 'expression':['a1','=',1]})
870
sage: r = SQLQuery(DB, {'table_name':'simon', 'display_cols':['a1'], 'expression':['b2','<=', 6]})
871
sage: s = q.union(r, 'simon', {'lucy':('a1','a1')})
872
sage: s.get_query_string()
873
'SELECT lucy.b2,simon.a1 FROM simon INNER JOIN lucy ON
874
simon.a1=lucy.a1 WHERE ( lucy.a1 = ? ) OR ( simon.b2 <= ? )'
875
sage: s.query_results()
876
[(1, 1), (4, 1)]
877
"""
878
if self.__query_dict__ is None or other.__query_dict__ is None:
879
raise RuntimeError('Queries must be constructed using a '
880
+ 'dictionary in order to be unioned.')
881
if self.__database__ != other.__database__:
882
raise TypeError('Queries %s and %s must be ' % (self, other)
883
+ 'attached to the same database.')
884
885
if in_place:
886
if self.__query_string__ and other.__query_string__:
887
self._merge_queries(other, self, join_table, join_dict, 'OR')
888
else:
889
from copy import copy
890
if not self.__query_string__:
891
return copy(self)
892
if not other.__query_string__:
893
return copy(other)
894
return self._merge_queries(other, copy(self), join_table,
895
join_dict, 'OR')
896
897
898
class SQLDatabase(SageObject):
899
def __init__(self, filename=None, read_only=None, skeleton=None):
900
r"""
901
A SQL Database object corresponding to a database file.
902
903
INPUT:
904
905
- ``filename`` -- string
906
- ``skeleton`` -- a triple-indexed dictionary::
907
908
| - outer key -- table name
909
| - inner key -- column name
910
| - inner inner key -- one of the following:
911
| - ``primary_key`` -- boolean, whether column has been set
912
as primary key
913
| - ``index`` -- boolean, whether column has been set as
914
index
915
| - ``unique`` -- boolean, whether column has been set as
916
unique
917
| - ``sql`` -- one of ``'TEXT'``, ``'BOOLEAN'``,
918
``'INTEGER'``, ``'REAL'``, or other user defined type
919
920
TUTORIAL:
921
922
The ``SQLDatabase`` class is for interactively building databases
923
intended for queries. This may sound redundant, but it is important. If
924
you want a database intended for quick lookup of entries in very large
925
tables, much like a hash table (such as a Python dictionary), a
926
``SQLDatabase`` may not be what you are looking for. The strength of
927
``SQLDatabases`` is in queries, searches through the database with
928
complicated criteria.
929
930
For example, we create a new database for storing isomorphism classes
931
of simple graphs::
932
933
sage: D = SQLDatabase()
934
935
In order to generate representatives for the classes, we will import a
936
function which generates all labeled graphs (noting that this is not
937
the optimal way)::
938
939
sage: from sage.groups.perm_gps.partn_ref.refinement_graphs import all_labeled_graphs
940
941
We will need a table in the database in which to store the graphs, and
942
we specify its structure with a Python dictionary, each of whose keys
943
is the name of a column::
944
945
sage: from collections import OrderedDict
946
sage: table_skeleton = OrderedDict([
947
....: ('graph6',{'sql':'TEXT', 'index':True, 'primary_key':True}),
948
....: ('vertices', {'sql':'INTEGER'}),
949
....: ('edges', {'sql':'INTEGER'})
950
....: ])
951
952
Then we create the table::
953
954
sage: D.create_table('simon', table_skeleton)
955
sage: D.show('simon')
956
graph6 vertices edges
957
------------------------------------------------------------
958
959
Now that we have the table, we will begin to populate the table with
960
rows. First, add the graph on zero vertices.::
961
962
sage: G = Graph()
963
sage: D.add_row('simon',(G.graph6_string(), 0, 0))
964
sage: D.show('simon')
965
graph6 vertices edges
966
------------------------------------------------------------
967
? 0 0
968
969
Next, add the graph on one vertex.::
970
971
sage: G.add_vertex()
972
0
973
sage: D.add_row('simon',(G.graph6_string(), 1, 0))
974
sage: D.show('simon')
975
graph6 vertices edges
976
------------------------------------------------------------
977
? 0 0
978
@ 1 0
979
980
Say we want a database of graphs on four or less vertices::
981
982
sage: labels = {}
983
sage: for i in range(2, 5):
984
....: labels[i] = []
985
....: for g in all_labeled_graphs(i):
986
....: g = g.canonical_label(algorithm='sage')
987
....: if g not in labels[i]:
988
....: labels[i].append(g)
989
....: D.add_row('simon', (g.graph6_string(), g.order(), g.size()))
990
sage: D.show('simon')
991
graph6 vertices edges
992
------------------------------------------------------------
993
? 0 0
994
@ 1 0
995
A? 2 0
996
A_ 2 1
997
B? 3 0
998
BG 3 1
999
BW 3 2
1000
Bw 3 3
1001
C? 4 0
1002
C@ 4 1
1003
CB 4 2
1004
CF 4 3
1005
CJ 4 3
1006
CK 4 2
1007
CL 4 3
1008
CN 4 4
1009
C] 4 4
1010
C^ 4 5
1011
C~ 4 6
1012
1013
We can then query the database -- let's ask for all the graphs on four
1014
vertices with three edges. We do so by creating two queries and asking
1015
for rows that satisfy them both::
1016
1017
sage: Q = SQLQuery(D, {'table_name':'simon', 'display_cols':['graph6'], 'expression':['vertices','=',4]})
1018
sage: Q2 = SQLQuery(D, {'table_name':'simon', 'display_cols':['graph6'], 'expression':['edges','=',3]})
1019
sage: Q = Q.intersect(Q2)
1020
sage: len(Q.query_results())
1021
3
1022
sage: Q.query_results() # random
1023
[('CF', 'CF'), ('CJ', 'CJ'), ('CL', 'CL')]
1024
1025
NOTE: The values of ``display_cols`` are always concatenated in
1026
intersections and unions.
1027
1028
Of course, we can save the database to file. Here we use a
1029
temporary directory that we clean up afterwards::
1030
1031
sage: import tempfile
1032
sage: d = tempfile.TemporaryDirectory()
1033
sage: dbpath = os.path.join(d.name, 'simon.db')
1034
sage: D.save(dbpath)
1035
1036
Now the database's hard link is to this file, and not the temporary db
1037
file. For example, let's say we open the same file with another class
1038
instance. We can load the file as an immutable database::
1039
1040
sage: E = SQLDatabase(dbpath)
1041
sage: E.show('simon')
1042
graph6 vertices edges
1043
------------------------------------------------------------
1044
? 0 0
1045
@ 1 0
1046
A? 2 0
1047
A_ 2 1
1048
B? 3 0
1049
BG 3 1
1050
BW 3 2
1051
Bw 3 3
1052
C? 4 0
1053
C@ 4 1
1054
CB 4 2
1055
CF 4 3
1056
CJ 4 3
1057
CK 4 2
1058
CL 4 3
1059
CN 4 4
1060
C] 4 4
1061
C^ 4 5
1062
C~ 4 6
1063
sage: E.drop_table('simon')
1064
Traceback (most recent call last):
1065
...
1066
RuntimeError: Cannot drop tables from a read only database.
1067
1068
Call ``cleanup()`` on the temporary directory to, well, clean it up::
1069
1070
sage: d.cleanup()
1071
"""
1072
if filename is None:
1073
if read_only is None:
1074
read_only = False
1075
filename = tmp_filename() + '.db'
1076
elif (filename[-3:] != '.db'):
1077
raise ValueError('Please enter a valid database path (file name '
1078
+ '%s does not end in .db).' % filename)
1079
if read_only is None:
1080
read_only = True
1081
1082
self.__read_only__ = read_only
1083
self.ignore_warnings = False
1084
self.__dblocation__ = filename
1085
self.__connection__ = sqlite.connect(self.__dblocation__,
1086
check_same_thread=False)
1087
# this is to avoid the multiple thread problem with dsage:
1088
# pysqlite does not trust multiple threads for the same connection
1089
self.__connection__.create_function("regexp", 2, regexp)
1090
1091
# construct skeleton (from provided database)
1092
self.__skeleton__ = construct_skeleton(self)
1093
1094
# add bones from new skeleton to database,
1095
# without changing existing structure
1096
if skeleton is not None and not read_only:
1097
for table in skeleton:
1098
if table not in self.__skeleton__:
1099
self.create_table(table, skeleton[table])
1100
else:
1101
for column in skeleton[table]:
1102
if column not in self.__skeleton__[table]:
1103
self.add_column(table, column,
1104
skeleton[table][column])
1105
else:
1106
print('Column attributes were ignored for '
1107
'table {}, column {} -- column is '
1108
'already in table.'.format(table, column))
1109
elif skeleton is not None:
1110
raise RuntimeError('Cannot update skeleton of a read only '
1111
+ 'database.')
1112
1113
def __repr__(self):
1114
"""
1115
Override the print output to display useful info regarding the
1116
database.
1117
1118
EXAMPLES::
1119
1120
sage: import tempfile
1121
sage: with tempfile.TemporaryDirectory() as d:
1122
....: dbpath = os.path.join(d, "test.db")
1123
....: SD = SQLDatabase(dbpath, False)
1124
....: SD.create_table('simon', {'n':{'sql':'INTEGER', 'index':True}})
1125
....: print(SD)
1126
table simon:
1127
column n: index: True; primary_key: False; sql: INTEGER;
1128
unique: False;
1129
"""
1130
s = ''
1131
for table in self.__skeleton__:
1132
s += 'table ' + table + ':\n'
1133
for column in self.__skeleton__[table]:
1134
s += ' column ' + column + ': '
1135
for data in sorted(self.__skeleton__[table][column]):
1136
s += data + ': ' \
1137
+ str(self.__skeleton__[table][column][data]) + '; '
1138
s += '\n'
1139
return s
1140
1141
def __copy__(self):
1142
"""
1143
Return an instance of ``SQLDatabase`` that points to a copy database,
1144
and allows modification.
1145
1146
EXAMPLES::
1147
1148
sage: from collections import OrderedDict
1149
sage: DB = SQLDatabase()
1150
sage: DB.create_table('lucy', OrderedDict([
1151
....: ('id', {'sql':'INTEGER', 'primary_key':True, 'index':True}),
1152
....: ('a1', {'sql':'bool'}),
1153
....: ('b2', {'sql':'int', 'primary_key':False})
1154
....: ]))
1155
sage: DB.add_rows('lucy', [(0,1,1),(1,1,4),(2,0,7),(3,1,384), (4,1,978932)],['id','a1','b2'])
1156
sage: d = copy(DB)
1157
sage: d == DB
1158
False
1159
sage: d.show('lucy')
1160
id a1 b2
1161
------------------------------------------------------------
1162
0 1 1
1163
1 1 4
1164
2 0 7
1165
3 1 384
1166
4 1 978932
1167
sage: DB.show('lucy')
1168
id a1 b2
1169
------------------------------------------------------------
1170
0 1 1
1171
1 1 4
1172
2 0 7
1173
3 1 384
1174
4 1 978932
1175
"""
1176
# copy .db file
1177
new_loc = tmp_filename() + '.db'
1178
if not self.__read_only__:
1179
self.commit()
1180
os.system('cp ' + self.__dblocation__ + ' ' + new_loc)
1181
D = SQLDatabase(filename=new_loc, read_only=False)
1182
return D
1183
1184
def save(self, filename):
1185
"""
1186
Save the database to the specified location.
1187
1188
EXAMPLES::
1189
1190
sage: MonicPolys = SQLDatabase()
1191
sage: MonicPolys.create_table('simon', {'n':{'sql':'INTEGER', 'index':True}})
1192
sage: for n in range(20): MonicPolys.add_row('simon', (n,))
1193
sage: import tempfile
1194
sage: with tempfile.TemporaryDirectory() as d:
1195
....: dbpath = os.path.join(d, "sage.db")
1196
....: MonicPolys.save(dbpath)
1197
....: N = SQLDatabase(dbpath)
1198
....: N.show('simon')
1199
n
1200
--------------------
1201
0
1202
1
1203
2
1204
3
1205
4
1206
5
1207
6
1208
7
1209
8
1210
9
1211
10
1212
11
1213
12
1214
13
1215
14
1216
15
1217
16
1218
17
1219
18
1220
19
1221
"""
1222
if not self.__read_only__:
1223
self.commit()
1224
os.system('cp ' + self.__dblocation__ + ' ' + filename)
1225
1226
def get_skeleton(self, check=False):
1227
"""
1228
Return a dictionary representing the hierarchical structure of the
1229
database, in the following format::
1230
1231
| - skeleton -- a triple-indexed dictionary
1232
| - outer key -- table name
1233
| - inner key -- column name
1234
| - inner inner key -- one of the following:
1235
| - ``primary_key`` -- boolean, whether column has been set as
1236
primary key
1237
| - ``index`` -- boolean, whether column has been set as index
1238
| - ``unique`` -- boolean, whether column has been set as unique
1239
| - ``sql`` -- one of ``'TEXT'``, ``'BOOLEAN'``, ``'INTEGER'``,
1240
``'REAL'``, or other user defined type
1241
1242
For example::
1243
1244
{'table1':{'col1':{'primary_key':False, 'index':True, 'unique': False,'sql':'REAL'}}}
1245
1246
INPUT:
1247
1248
- ``check`` -- if ``True``, checks to make sure the database's actual
1249
structure matches the skeleton on record
1250
1251
EXAMPLES::
1252
1253
sage: GDB = GraphDatabase()
1254
sage: GDB.get_skeleton() # slightly random output
1255
{'aut_grp': {'aut_grp_size': {'index': True,
1256
'unique': False,
1257
'primary_key': False,
1258
'sql': 'INTEGER'},
1259
...
1260
'num_vertices': {'index': True,
1261
'unique': False,
1262
'primary_key': False,
1263
'sql': 'INTEGER'}}}
1264
"""
1265
if check:
1266
d = construct_skeleton(self)
1267
if d == self.__skeleton__:
1268
return d
1269
raise RuntimeError("skeleton structure is out of whack")
1270
return self.__skeleton__
1271
1272
def query(self, *args, **kwds):
1273
"""
1274
Create a ``SQLQuery`` on this database.
1275
1276
For full class details,
1277
type ``SQLQuery?`` and press :kbd:`Shift` + :kbd:`Enter`.
1278
1279
EXAMPLES::
1280
1281
sage: D = SQLDatabase()
1282
sage: D.create_table('simon', {'wolf':{'sql':'BOOLEAN'}, 'tag':{'sql':'INTEGER'}})
1283
sage: q = D.query({'table_name':'simon', 'display_cols':['tag'], 'expression':['wolf','=',1]})
1284
sage: q.get_query_string()
1285
'SELECT simon.tag FROM simon WHERE simon.wolf = ?'
1286
sage: q.__param_tuple__
1287
('1',)
1288
sage: q = D.query(query_string='SELECT tag FROM simon WHERE wolf=?',param_tuple=(1,))
1289
sage: q.get_query_string()
1290
'SELECT tag FROM simon WHERE wolf=?'
1291
sage: q.__param_tuple__
1292
('1',)
1293
"""
1294
return SQLQuery(self, *args, **kwds)
1295
1296
__call__ = query
1297
1298
def show(self, table_name, **kwds):
1299
"""
1300
Show an entire table from the database.
1301
1302
EXAMPLES::
1303
1304
sage: DB = SQLDatabase()
1305
sage: DB.create_table('simon',{'a1':{'sql':'bool'}, 'b2':{'sql':'int'}})
1306
sage: DB.add_data('simon',[(0,0),(1,1),(1,2)])
1307
sage: DB.show('simon')
1308
a1 b2
1309
----------------------------------------
1310
0 0
1311
1 1
1312
1 2
1313
"""
1314
try:
1315
cur = self.__connection__.cursor()
1316
cur.execute('SELECT * FROM ' + table_name)
1317
except Exception:
1318
raise RuntimeError('Failure to fetch data.')
1319
print(_create_print_table(cur, [des[0] for des in cur.description],
1320
**kwds))
1321
1322
def get_cursor(self, ignore_warning=None):
1323
"""
1324
Return a pysqlite cursor for the database connection.
1325
1326
A cursor is an input from which you can execute sqlite commands on the
1327
database.
1328
1329
Recommended for more advanced users only.
1330
1331
EXAMPLES::
1332
1333
sage: DB = SQLDatabase()
1334
sage: DB.create_table('simon',{'a1':{'sql':'bool'}, 'b2':{'sql':'int'}})
1335
sage: DB.add_row('simon',(0,1))
1336
sage: DB.add_rows('simon',[(0,0),(1,1),(1,2)])
1337
sage: DB.add_rows('simon',[(0,0),(4,0),(5,1)], ['b2','a1'])
1338
sage: cur = DB.get_cursor()
1339
sage: (cur.execute('select * from simon')).fetchall()
1340
[(0, 1), (0, 0), (1, 1), (1, 2), (0, 0), (0, 4), (1, 5)]
1341
"""
1342
if self.__read_only__:
1343
if ignore_warning is None:
1344
ignore_warning = self.ignore_warnings
1345
if not ignore_warning:
1346
import warnings
1347
warnings.warn('Database is read only, using the cursor can '
1348
+ 'alter the stored data. Set self.ignore_warnings to '
1349
+ 'True in order to mute future warnings.', RuntimeWarning)
1350
return self.__connection__.cursor()
1351
1352
def get_connection(self, ignore_warning=None):
1353
"""
1354
Return a pysqlite connection to the database.
1355
1356
You most likely want ``get_cursor()`` instead, which is used for
1357
executing sqlite commands on the database.
1358
1359
Recommended for more advanced users only.
1360
1361
EXAMPLES::
1362
1363
sage: D = SQLDatabase(read_only=True)
1364
sage: con = D.get_connection()
1365
doctest:...: RuntimeWarning: Database is read only, using the connection can alter the stored data. Set self.ignore_warnings to True in order to mute future warnings.
1366
sage: con = D.get_connection(True)
1367
sage: D.ignore_warnings = True
1368
sage: con = D.get_connection()
1369
sage: t = con.execute('CREATE TABLE simon(n INTEGER, n2 INTEGER)')
1370
sage: for n in range(10):
1371
....: t = con.execute('INSERT INTO simon VALUES(%d,%d)'%(n,n^2))
1372
sage: D.show('simon')
1373
n n2
1374
----------------------------------------
1375
0 0
1376
1 1
1377
2 4
1378
3 9
1379
4 16
1380
5 25
1381
6 36
1382
7 49
1383
8 64
1384
9 81
1385
"""
1386
if self.__read_only__:
1387
if ignore_warning is None:
1388
ignore_warning = self.ignore_warnings
1389
if not ignore_warning:
1390
import warnings
1391
warnings.warn('Database is read only, using the connection '
1392
'can alter the stored data. Set self.ignore_warnings '
1393
'to True in order to mute future warnings.',
1394
RuntimeWarning)
1395
return self.__connection__
1396
1397
def create_table(self, table_name, table_skeleton):
1398
"""
1399
Create a new table in the database.
1400
1401
To create a table, a column structure must be specified. The form for
1402
this is a Python dict, for example::
1403
1404
{'col1': {'sql':'INTEGER', 'index':False, 'unique':True, 'primary_key':False}, ...}
1405
1406
INPUT:
1407
1408
- ``table_name`` -- string
1409
- ``table_skeleton`` -- a double-indexed dictionary
1410
1411
- outer key -- column name
1412
1413
- inner key -- one of the following:
1414
1415
- ``primary_key`` -- boolean, whether column has been set
1416
asprimary key
1417
- ``index`` -- boolean, whether column has been set as
1418
index
1419
- ``unique`` -- boolean, whether column has been set as
1420
unique
1421
- ``sql`` -- one of ``'TEXT'``, ``'BOOLEAN'``,
1422
``'INTEGER'``, ``'REAL'``, or other user defined type
1423
1424
NOTE:
1425
1426
Some SQL features, such as automatically incrementing primary key,
1427
require the full word ``'INTEGER'``, not just ``'INT'``.
1428
1429
EXAMPLES::
1430
1431
sage: from collections import OrderedDict
1432
sage: D = SQLDatabase()
1433
sage: table_skeleton = OrderedDict([
1434
....: ('graph6', {'sql':'TEXT', 'index':True, 'primary_key':True}),
1435
....: ('vertices', {'sql':'INTEGER'}),
1436
....: ('edges', {'sql':'INTEGER'})
1437
....: ])
1438
sage: D.create_table('simon', table_skeleton)
1439
sage: D.show('simon')
1440
graph6 vertices edges
1441
------------------------------------------------------------
1442
"""
1443
if self.__read_only__:
1444
raise RuntimeError('Cannot add table to a read only database.')
1445
if table_name in self.__skeleton__:
1446
raise ValueError('Database already has a table named %s'
1447
% table_name)
1448
if table_name.find(' ') != -1:
1449
raise ValueError('Table names cannot contain spaces.')
1450
if table_name.upper() in sqlite_keywords:
1451
raise ValueError('Table names cannot be a SQLite keyword.')
1452
create_statement = 'CREATE TABLE ' + table_name + '('
1453
statement = []
1454
index_statement = ''
1455
for col in table_skeleton:
1456
if col.find('sqlite') != -1:
1457
raise ValueError("Column names cannot contain 'sqlite'.")
1458
if col.upper() in sqlite_keywords:
1459
raise ValueError('Column names cannot be a SQLite keyword.')
1460
table_skeleton[col] = verify_column(table_skeleton[col])
1461
typ = table_skeleton[col]['sql']
1462
if verify_type(typ):
1463
if typ.upper() == 'NOTYPE':
1464
typ = ''
1465
if table_skeleton[col]['primary_key']:
1466
statement.append(col + ' ' + typ + ' PRIMARY KEY')
1467
elif table_skeleton[col]['unique']:
1468
statement.append(col + ' ' + typ + ' UNIQUE')
1469
else:
1470
statement.append(col + ' ' + typ)
1471
if table_skeleton[col]['index']:
1472
index_statement += 'CREATE INDEX i_%s_%s' % (table_name,
1473
col) + ' ON %s(%s);\n' % (table_name, col)
1474
create_statement += ', '.join(statement) + ') '
1475
1476
self.__connection__.execute(create_statement)
1477
if index_statement:
1478
self.__connection__.executescript(index_statement)
1479
1480
self.__skeleton__[table_name] = table_skeleton
1481
1482
def add_column(self, table_name, col_name, col_dict, default='NULL'):
1483
"""
1484
Add a column named ``col_name`` to table ``table_name``, whose data
1485
types are described by ``col_dict``. The format for this is::
1486
1487
{'col1':{'primary_key':False, 'unique': True, 'index':True, 'sql':'REAL'}}
1488
1489
INPUT:
1490
1491
- ``col_dict`` -- dictionary:
1492
1493
- key -- column name
1494
1495
- inner key -- one of the following:
1496
1497
- ``primary_key`` -- boolean, whether column has been set
1498
as primary key
1499
- ``index`` -- boolean, whether column has been set as
1500
index
1501
- ``unique`` -- boolean, whether column has been set as
1502
unique
1503
- ``sql`` -- one of ``'TEXT'``, ``'BOOLEAN'``,
1504
``'INTEGER'``, ``'REAL'``, or other user defined type
1505
1506
EXAMPLES::
1507
1508
sage: from collections import OrderedDict
1509
sage: MonicPolys = SQLDatabase()
1510
sage: MonicPolys.create_table('simon', OrderedDict([('n', {'sql':'INTEGER', 'index':True})]))
1511
sage: for n in range(20): MonicPolys.add_row('simon', (n,))
1512
sage: MonicPolys.add_column('simon', 'n_squared', {'sql':'INTEGER', 'index':False}, 0)
1513
sage: MonicPolys.show('simon')
1514
n n_squared
1515
----------------------------------------
1516
0 0
1517
1 0
1518
2 0
1519
3 0
1520
4 0
1521
5 0
1522
6 0
1523
7 0
1524
8 0
1525
9 0
1526
10 0
1527
11 0
1528
12 0
1529
13 0
1530
14 0
1531
15 0
1532
16 0
1533
17 0
1534
18 0
1535
19 0
1536
"""
1537
if self.__read_only__:
1538
raise RuntimeError('Cannot add columns to a read only database.')
1539
# Check input:
1540
if col_name.find('sqlite') != -1:
1541
raise ValueError("Column names cannot contain 'sqlite'.")
1542
if col_name.upper() in sqlite_keywords:
1543
raise ValueError("Column names cannot be SQLite keywords.")
1544
if table_name not in self.__skeleton__:
1545
raise ValueError("Database has no table %s." % table_name)
1546
if col_name in self.__skeleton__[table_name]:
1547
raise ValueError("Table %s already has column %s." % (table_name,col_name))
1548
1549
# Update the skeleton:
1550
self.__skeleton__[table_name][col_name] = verify_column(col_dict)
1551
1552
try:
1553
self._rebuild_table(table_name, col_name, default)
1554
except sqlite.Error as e:
1555
# delete added column from skeleton
1556
self.__skeleton__[table_name].pop(col_name)
1557
1558
print('A sqlite error occurred: ', e.args[0])
1559
1560
def _rebuild_table(self, table_name, col_name=None, default=''):
1561
"""
1562
Rebuild the table ``table_name`` adding column ``col_name`` if not
1563
``None``. If a new column is added, each rows' value is set to
1564
``default``.
1565
1566
Used in the methods ``add_column``, ``drop_column``, ``make_unique``,
1567
``drop_unique``, ``make_primary_key``, and ``drop_primary_key`` to get
1568
around the limitations of SQLite's ``ALTER TABLE``. We have set up the
1569
creation of a temporary database in this method. Please feel free to
1570
improve on this by sending a patch or suggestion.
1571
1572
EXAMPLES::
1573
1574
sage: MonicPolys = SQLDatabase()
1575
sage: MonicPolys.create_table('simon', {'n':{'sql':'INTEGER', 'index':True}})
1576
sage: for n in range(20): MonicPolys.add_row('simon', (n,))
1577
sage: MonicPolys.show('simon')
1578
n
1579
--------------------
1580
0
1581
1
1582
2
1583
3
1584
4
1585
5
1586
6
1587
7
1588
8
1589
9
1590
10
1591
11
1592
12
1593
13
1594
14
1595
15
1596
16
1597
17
1598
18
1599
19
1600
sage: MonicPolys._rebuild_table('simon')
1601
sage: MonicPolys.show('simon')
1602
n
1603
--------------------
1604
0
1605
1
1606
2
1607
3
1608
4
1609
5
1610
6
1611
7
1612
8
1613
9
1614
10
1615
11
1616
12
1617
13
1618
14
1619
15
1620
16
1621
17
1622
18
1623
19
1624
"""
1625
cols = []
1626
cols_attr = []
1627
table_skeleton = self.__skeleton__[table_name]
1628
# gather column names and attributes for new table
1629
for col in table_skeleton:
1630
cols.append(col)
1631
attr_str = col + ' ' + table_skeleton[col]['sql']
1632
if table_skeleton[col]['primary_key']:
1633
attr_str += ' PRIMARY KEY'
1634
elif table_skeleton[col]['unique']:
1635
attr_str += ' UNIQUE'
1636
cols_attr.append(attr_str)
1637
1638
original = list(cols)
1639
1640
if col_name is not None:
1641
original[original.index(col_name)] = str(default)
1642
1643
original = ', '.join(original)
1644
cols = ', '.join(cols)
1645
cols_attr = ', '.join(cols_attr)
1646
1647
# Silly SQLite -- we have to make a temp table to hold info...
1648
self.__connection__.executescript("""
1649
CREATE TEMPORARY TABLE spam(%s);
1650
INSERT INTO spam SELECT %s FROM %s;
1651
DROP TABLE %s;
1652
CREATE TABLE %s (%s);
1653
""" % (cols_attr, original, table_name, table_name, table_name, cols_attr))
1654
# Update indices in new table
1655
skeleton = self.__skeleton__[table_name]
1656
index_statement = ''.join(f'CREATE INDEX i_{table_name}_{col} ON '
1657
+ f'{table_name}({col});\n'
1658
for col in skeleton
1659
if skeleton[col]['index']
1660
and not skeleton[col]['primary_key'])
1661
if index_statement:
1662
self.__connection__.executescript(index_statement)
1663
1664
# Now we can plop our data into the *new* table:
1665
self.__connection__.executescript("""
1666
INSERT INTO %s SELECT %s FROM spam;
1667
DROP TABLE spam;
1668
""" % (table_name, cols))
1669
1670
self.vacuum()
1671
1672
def drop_column(self, table_name, col_name):
1673
"""
1674
Drop the column ``col_name`` from table ``table_name``.
1675
1676
EXAMPLES::
1677
1678
sage: MonicPolys = SQLDatabase()
1679
sage: MonicPolys.create_table('simon', {'n':{'sql':'INTEGER', 'index':True}})
1680
sage: for n in range(20): MonicPolys.add_row('simon', (n,))
1681
sage: MonicPolys.add_column('simon', 'n_squared', {'sql':'INTEGER'}, 0)
1682
sage: MonicPolys.drop_column('simon', 'n_squared')
1683
sage: MonicPolys.show('simon')
1684
n
1685
--------------------
1686
0
1687
1
1688
2
1689
3
1690
4
1691
5
1692
6
1693
7
1694
8
1695
9
1696
10
1697
11
1698
12
1699
13
1700
14
1701
15
1702
16
1703
17
1704
18
1705
19
1706
"""
1707
if self.__read_only__:
1708
raise RuntimeError('Cannot drop columns in a read only database.')
1709
# Check input:
1710
if table_name not in self.__skeleton__:
1711
raise ValueError("Database has no table %s." % table_name)
1712
if col_name not in self.__skeleton__[table_name]:
1713
raise ValueError("Table %s has no column %s." % (table_name,col_name))
1714
1715
# Update the skeleton:
1716
self.__skeleton__[table_name].pop(col_name)
1717
1718
self._rebuild_table(table_name)
1719
1720
def rename_table(self, table_name, new_name):
1721
"""
1722
Rename the table ``table_name`` to ``new_name``.
1723
1724
EXAMPLES::
1725
1726
sage: D = SQLDatabase()
1727
sage: D.create_table('simon',{'col1':{'sql':'INTEGER'}})
1728
sage: D.show('simon')
1729
col1
1730
--------------------
1731
sage: D.rename_table('simon', 'lucy')
1732
sage: D.show('simon')
1733
Traceback (most recent call last):
1734
...
1735
RuntimeError: Failure to fetch data.
1736
sage: D.show('lucy')
1737
col1
1738
--------------------
1739
"""
1740
if self.__read_only__:
1741
raise RuntimeError('Cannot rename tables in a read only database.')
1742
# Check input:
1743
if table_name not in self.__skeleton__:
1744
raise ValueError('Database has no table %s.' % table_name)
1745
if new_name in self.__skeleton__:
1746
raise ValueError('Database already has table %s.' % new_name)
1747
1748
self.__connection__.execute('ALTER TABLE %s RENAME TO ' % table_name
1749
+ new_name)
1750
1751
# Update skeleton:
1752
self.__skeleton__[new_name] = self.__skeleton__.pop(table_name)
1753
1754
def drop_table(self, table_name):
1755
"""
1756
Delete table ``table_name`` from database.
1757
1758
INPUT:
1759
1760
- ``table_name`` -- string
1761
1762
EXAMPLES::
1763
1764
sage: D = SQLDatabase()
1765
sage: D.create_table('simon',{'col1':{'sql':'INTEGER'}})
1766
sage: D.show('simon')
1767
col1
1768
--------------------
1769
sage: D.drop_table('simon')
1770
sage: D.get_skeleton()
1771
{}
1772
"""
1773
if self.__read_only__:
1774
raise RuntimeError('Cannot drop tables from a read only '
1775
+ 'database.')
1776
if table_name not in self.__skeleton__:
1777
raise ValueError("Database has no table %s." % table_name)
1778
1779
self.__connection__.execute('DROP TABLE ' + table_name)
1780
1781
# Update Skeleton
1782
self.__skeleton__.pop(table_name)
1783
1784
def drop_data_from_table(self, table_name):
1785
"""
1786
Remove all rows from ``table_name``.
1787
1788
EXAMPLES::
1789
1790
sage: D = SQLDatabase()
1791
sage: D.create_table('simon',{'col1':{'sql':'INTEGER'}})
1792
sage: D.add_row('simon',(9,))
1793
sage: D.show('simon')
1794
col1
1795
--------------------
1796
9
1797
sage: D.drop_data_from_table('simon')
1798
sage: D.show('simon')
1799
col1
1800
--------------------
1801
"""
1802
if self.__read_only__:
1803
raise RuntimeError('Cannot remove data from a read only database.')
1804
if table_name not in self.__skeleton__:
1805
raise ValueError("Database has no table %s." % table_name)
1806
self.__connection__.execute('DELETE FROM ' + table_name)
1807
1808
def make_index(self, col_name, table_name, unique=False):
1809
"""
1810
Set the column ``col_name`` in table ``table_name`` to be an index,
1811
that is, a column set up to do quick searches on.
1812
1813
INPUT:
1814
1815
- ``col_name`` -- string
1816
- ``table_name`` -- string
1817
- ``unique`` -- requires that there are no multiple entries in the
1818
column, makes searching faster
1819
1820
EXAMPLES::
1821
1822
sage: MonicPolys = SQLDatabase()
1823
sage: MonicPolys.create_table('simon', {'n':{'sql':'INTEGER', 'index':True}, 'n2':{'sql':'INTEGER'}})
1824
sage: MonicPolys.make_index('n2','simon')
1825
sage: MonicPolys.get_skeleton()
1826
{'simon': {'n': {'index': True,
1827
'primary_key': False,
1828
'sql': 'INTEGER',
1829
'unique': False},
1830
'n2': {'index': True,
1831
'primary_key': False,
1832
'sql': 'INTEGER',
1833
'unique': False}}}
1834
"""
1835
if self.__read_only__:
1836
raise RuntimeError('Cannot modify a read only database.')
1837
if table_name not in self.__skeleton__:
1838
raise ValueError("Database has no table %s." % table_name)
1839
if col_name not in self.__skeleton__[table_name]:
1840
raise ValueError("Table %s has no column %s." % (table_name,col_name))
1841
1842
if unique:
1843
index_string = 'CREATE UNIQUE INDEX ' + col_name + ' ON ' \
1844
+ table_name + ' (' + col_name + ')'
1845
else:
1846
index_string = 'CREATE INDEX ' + col_name + ' ON ' + table_name \
1847
+ ' (' + col_name + ')'
1848
cur = self.__connection__.cursor()
1849
cur.execute(index_string)
1850
1851
# Update Skeleton
1852
self.__skeleton__[table_name][col_name]['index'] = True
1853
if unique:
1854
self.__skeleton[table_name][col_name]['unique'] = True
1855
1856
def drop_index(self, table_name, index_name):
1857
"""
1858
Set the column ``index_name`` in table ``table_name`` to not be an
1859
index. See ``make_index()``
1860
1861
EXAMPLES::
1862
1863
sage: MonicPolys = SQLDatabase()
1864
sage: MonicPolys.create_table('simon', {'n':{'sql':'INTEGER', 'index':True}, 'n2':{'sql':'INTEGER'}})
1865
sage: MonicPolys.drop_index('simon', 'n')
1866
sage: MonicPolys.get_skeleton()
1867
{'simon': {'n': {'index': False,
1868
'primary_key': False,
1869
'sql': 'INTEGER',
1870
'unique': False},
1871
'n2': {'index': False,
1872
'primary_key': False,
1873
'sql': 'INTEGER',
1874
'unique': False}}}
1875
"""
1876
if self.__read_only__:
1877
raise RuntimeError('Cannot modify a read only database.')
1878
if table_name not in self.__skeleton__:
1879
raise ValueError("Database has no table %s." % table_name)
1880
if index_name not in self.__skeleton__[table_name]:
1881
raise ValueError("Table %s has no column %s." % (table_name,
1882
index_name))
1883
if not self.__skeleton__[table_name][index_name]['index']:
1884
return # silently
1885
1886
cur = self.__connection__.cursor()
1887
cur.execute('DROP INDEX i_' + table_name + '_' + index_name)
1888
1889
# Update Skeleton
1890
self.__skeleton__[table_name][index_name]['index'] = False
1891
1892
def make_unique(self, table_name, col_name):
1893
"""
1894
Set the column ``col_name`` in table ``table_name`` to store unique
1895
values.
1896
1897
NOTE:
1898
1899
This function only adds the requirement for entries in ``col_name`` to
1900
be unique, it does not change the values.
1901
1902
EXAMPLES::
1903
1904
sage: MonicPolys = SQLDatabase()
1905
sage: MonicPolys.create_table('simon', {'n':{'sql':'INTEGER', 'index':True}, 'n2':{'sql':'INTEGER'}})
1906
sage: MonicPolys.make_unique('simon', 'n2')
1907
sage: MonicPolys.get_skeleton()
1908
{'simon': {'n': {'index': True,
1909
'primary_key': False,
1910
'sql': 'INTEGER',
1911
'unique': False},
1912
'n2': {'index': False,
1913
'primary_key': False,
1914
'sql': 'INTEGER',
1915
'unique': True}}}
1916
"""
1917
if self.__read_only__:
1918
raise RuntimeError('Cannot modify a read only database')
1919
if table_name not in self.__skeleton__:
1920
raise ValueError("Database has no table %s." % table_name)
1921
if col_name not in self.__skeleton__[table_name]:
1922
raise ValueError("Table %s has no column %s." % (table_name, col_name))
1923
1924
# Update the skeleton:
1925
self.__skeleton__[table_name][col_name]['unique'] = True
1926
1927
self._rebuild_table(table_name)
1928
1929
def drop_unique(self, table_name, col_name):
1930
"""
1931
Set the column ``col_name`` in table ``table_name`` not store unique
1932
values.
1933
1934
NOTE:
1935
1936
This function only removes the requirement for entries in ``col_name``
1937
to be unique, it does not delete it.
1938
1939
EXAMPLES::
1940
1941
sage: MonicPolys = SQLDatabase()
1942
sage: MonicPolys.create_table('simon', {'n':{'sql':'INTEGER', 'index':True}, 'n2':{'sql':'INTEGER'}})
1943
sage: MonicPolys.make_unique('simon', 'n2')
1944
sage: MonicPolys.drop_unique('simon', 'n2')
1945
sage: MonicPolys.get_skeleton()
1946
{'simon': {'n': {'index': True,
1947
'primary_key': False,
1948
'sql': 'INTEGER',
1949
'unique': False},
1950
'n2': {'index': False,
1951
'primary_key': False,
1952
'sql': 'INTEGER',
1953
'unique': False}}}
1954
"""
1955
if self.__read_only__:
1956
raise RuntimeError('Cannot modify a read only database.')
1957
if table_name not in self.__skeleton__:
1958
raise ValueError("Database has no table %s." % table_name)
1959
if col_name not in self.__skeleton__[table_name]:
1960
raise ValueError("Table %s has no column %s." % (table_name, col_name))
1961
if self.__skeleton__[table_name][col_name]['primary_key']:
1962
raise ValueError("Primary keys must be unique.")
1963
1964
# Update the skeleton:
1965
self.__skeleton__[table_name][col_name]['unique'] = False
1966
1967
self._rebuild_table(table_name)
1968
1969
def make_primary_key(self, table_name, col_name):
1970
"""
1971
Set the column ``col_name`` in table ``table_name`` to be a primary key.
1972
1973
A primary key is something like an index, but its main purpose is to
1974
link different tables together. This allows searches to be executed on
1975
multiple tables that represent maybe different data about the same
1976
objects.
1977
1978
NOTE:
1979
1980
Some SQL features, such as automatically incrementing primary key,
1981
require the full word ``'INTEGER'``, not just ``'INT'``.
1982
1983
EXAMPLES::
1984
1985
sage: MonicPolys = SQLDatabase()
1986
sage: MonicPolys.create_table('simon', {'n':{'sql':'INTEGER', 'index':True}, 'n2':{'sql':'INTEGER'}})
1987
sage: MonicPolys.make_primary_key('simon', 'n2')
1988
sage: MonicPolys.get_skeleton()
1989
{'simon': {'n': {'index': True,
1990
'primary_key': False,
1991
'sql': 'INTEGER',
1992
'unique': False},
1993
'n2': {'index': False,
1994
'primary_key': True,
1995
'sql': 'INTEGER',
1996
'unique': True}}}
1997
"""
1998
if self.__read_only__:
1999
raise RuntimeError('Cannot modify a read only database.')
2000
if table_name not in self.__skeleton__:
2001
raise ValueError("Database has no table %s." % table_name)
2002
if col_name not in self.__skeleton__[table_name]:
2003
raise ValueError("Table %s has no column %s." % (table_name, col_name))
2004
2005
# Update the skeleton:
2006
self.__skeleton__[table_name][col_name]['primary_key'] = True
2007
self.__skeleton__[table_name][col_name]['unique'] = True
2008
2009
self._rebuild_table(table_name)
2010
2011
def drop_primary_key(self, table_name, col_name):
2012
"""
2013
Set the column ``col_name`` in table ``table_name`` not to be a primary
2014
key.
2015
2016
A primary key is something like an index, but its main purpose is to
2017
link different tables together. This allows searches to be executed on
2018
multiple tables that represent maybe different data about the same
2019
objects.
2020
2021
NOTE:
2022
2023
This function only changes the column to be non-primary, it does not
2024
delete it.
2025
2026
EXAMPLES::
2027
2028
sage: MonicPolys = SQLDatabase()
2029
sage: MonicPolys.create_table('simon', {'n':{'sql':'INTEGER', 'index':True}, 'n2':{'sql':'INTEGER'}})
2030
sage: MonicPolys.make_primary_key('simon', 'n2')
2031
sage: MonicPolys.drop_primary_key('simon', 'n2')
2032
sage: MonicPolys.get_skeleton()
2033
{'simon': {'n': {'index': True,
2034
'primary_key': False,
2035
'sql': 'INTEGER',
2036
'unique': False},
2037
'n2': {'index': False,
2038
'primary_key': False,
2039
'sql': 'INTEGER',
2040
'unique': True}}}
2041
"""
2042
if self.__read_only__:
2043
raise RuntimeError('Cannot modify a read only database.')
2044
if table_name not in self.__skeleton__:
2045
raise ValueError("Database has no table %s." % table_name)
2046
if col_name not in self.__skeleton__[table_name]:
2047
raise ValueError("Table %s has no column %s." % (table_name,col_name))
2048
if not self.__skeleton__[table_name][col_name]['primary_key']:
2049
return # silently
2050
2051
# Update the skeleton:
2052
self.__skeleton__[table_name][col_name]['primary_key'] = False
2053
2054
self._rebuild_table(table_name)
2055
2056
def add_row(self, table_name, values, entry_order=None):
2057
"""
2058
Add the row described by ``values`` to the table ``table_name``. Values
2059
should be a tuple, of same length and order as columns in given table.
2060
2061
NOTE:
2062
2063
If ``values`` is of length one, be sure to specify that it is a tuple of
2064
length one, by using a comma, e.g.::
2065
2066
sage: values = (6,)
2067
2068
EXAMPLES::
2069
2070
sage: DB = SQLDatabase()
2071
sage: DB.create_table('simon',{'a1':{'sql':'bool'}, 'b2':{'sql':'int'}})
2072
sage: DB.add_row('simon',(0,1))
2073
sage: cur = DB.get_cursor()
2074
sage: (cur.execute('select * from simon')).fetchall()
2075
[(0, 1)]
2076
"""
2077
self.add_rows(table_name, [values], entry_order)
2078
2079
def delete_rows(self, query):
2080
"""
2081
Use a ``SQLQuery`` instance to modify (delete rows from) the
2082
database.
2083
2084
``SQLQuery`` must have no join statements. (As of now, you can only
2085
delete from one table at a time -- ideas and patches are welcome).
2086
2087
To remove all data that satisfies a ``SQLQuery``, send the query as an
2088
argument to ``delete_rows``. Be careful, test your query first.
2089
2090
Recommended use: have some kind of row identification primary
2091
key column that you use as a parameter in the query. (See example
2092
below).
2093
2094
INPUT:
2095
2096
- ``query`` -- a ``SQLQuery`` (Delete the rows returned when query is
2097
run)
2098
2099
EXAMPLES::
2100
2101
sage: from collections import OrderedDict
2102
sage: DB = SQLDatabase()
2103
sage: DB.create_table('lucy', OrderedDict([
2104
....: ('id', {'sql':'INTEGER', 'primary_key':True, 'index':True}),
2105
....: ('a1', {'sql':'bool'}),
2106
....: ('b2', {'sql':'int'})]))
2107
sage: DB.add_rows('lucy', [(0,1,1),(1,1,4),(2,0,7),(3,1,384), (4,1,978932)],['id','a1','b2'])
2108
sage: DB.show('lucy')
2109
id a1 b2
2110
------------------------------------------------------------
2111
0 1 1
2112
1 1 4
2113
2 0 7
2114
3 1 384
2115
4 1 978932
2116
sage: Q = SQLQuery(DB, {'table_name':'lucy', 'display_cols':['id','a1','b2'], 'expression':['id','>=',3]})
2117
sage: DB.delete_rows(Q)
2118
sage: DB.show('lucy')
2119
id a1 b2
2120
------------------------------------------------------------
2121
0 1 1
2122
1 1 4
2123
2 0 7
2124
"""
2125
if self.__read_only__:
2126
raise RuntimeError('Cannot delete rows from a read only database.')
2127
# Check query is associated with this database
2128
if not isinstance(query, SQLQuery):
2129
raise TypeError('%s is not a valid SQLQuery' % query)
2130
if query.__database__ is not self:
2131
raise ValueError('%s is not associated to this database.' % query)
2132
if (query.__query_string__).find(' JOIN ') != -1:
2133
raise ValueError(f'{query} is not a valid query. Can only '
2134
'delete from one table at a time.')
2135
2136
delete_statement = re.sub('SELECT .* FROM', 'DELETE FROM',
2137
query.__query_string__)
2138
2139
try:
2140
cur = self.get_cursor()
2141
cur.execute(delete_statement, query.__param_tuple__)
2142
except Exception:
2143
raise RuntimeError('Failure to complete delete. Check your data.')
2144
2145
def add_rows(self, table_name, rows, entry_order=None):
2146
"""
2147
INPUT:
2148
2149
- ``rows`` -- list of tuples that represent one row of data to add
2150
(types should match col types in order)
2151
- ``entry_order`` -- an ordered list or tuple overrides normal order
2152
with user defined order
2153
2154
EXAMPLES::
2155
2156
sage: DB = SQLDatabase()
2157
sage: DB.create_table('simon',{'a1':{'sql':'bool'}, 'b2':{'sql':'int'}})
2158
sage: DB.add_rows('simon',[(0,0),(1,1),(1,2)])
2159
sage: DB.add_rows('simon',[(0,0),(4,0),(5,1)], ['b2','a1'])
2160
sage: cur = DB.get_cursor()
2161
sage: (cur.execute('select * from simon')).fetchall()
2162
[(0, 0), (1, 1), (1, 2), (0, 0), (0, 4), (1, 5)]
2163
"""
2164
if self.__read_only__:
2165
raise RuntimeError('Cannot add rows to read only database.')
2166
quest = '(' + ', '.join('?' for i in rows[0]) + ')'
2167
strows = [tuple(str(entry) for entry in row) for row in rows]
2168
2169
if entry_order is not None:
2170
self.__connection__.executemany('INSERT INTO ' + table_name
2171
+ str(tuple(entry_order)) + ' VALUES ' + quest, strows)
2172
else:
2173
self.__connection__.executemany('INSERT INTO ' + table_name
2174
+ ' VALUES ' + quest, strows)
2175
2176
add_data = add_rows
2177
2178
def vacuum(self):
2179
"""
2180
Clean the extra hard disk space used up by a database that has
2181
recently shrunk.
2182
2183
EXAMPLES::
2184
2185
sage: DB = SQLDatabase()
2186
sage: DB.create_table('simon',{'a1':{'sql':'bool'}, 'b2':{'sql':'int'}})
2187
sage: DB.add_row('simon',(0,1))
2188
sage: DB.add_data('simon',[(0,0),(1,1),(1,2)])
2189
sage: DB.add_data('simon',[(0,0),(4,0),(5,1)], ['b2','a1'])
2190
sage: DB.drop_column('simon','b2')
2191
sage: DB.commit()
2192
sage: DB.vacuum()
2193
"""
2194
self.__connection__.execute('VACUUM')
2195
2196
def commit(self):
2197
"""
2198
Commit changes to file.
2199
2200
EXAMPLES::
2201
2202
sage: DB = SQLDatabase()
2203
sage: DB.create_table('simon',{'a1':{'sql':'bool'}, 'b2':{'sql':'int'}})
2204
sage: DB.add_row('simon',(0,1))
2205
sage: DB.add_data('simon',[(0,0),(1,1),(1,2)])
2206
sage: DB.add_data('simon',[(0,0),(4,0),(5,1)], ['b2','a1'])
2207
sage: DB.drop_column('simon','b2')
2208
sage: DB.commit()
2209
sage: DB.vacuum()
2210
"""
2211
if self.__read_only__:
2212
raise RuntimeError("Cannot commit read only database.")
2213
try:
2214
self.__connection__.execute('COMMIT')
2215
except sqlite.OperationalError:
2216
# Not sure why this throws an exception - but without it,
2217
# the changes are not committed so it is necessary.
2218
pass
2219
2220