solentware-base 5.0__tar.gz → 5.2__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.
- solentware-base-5.2/MANIFEST.in +2 -0
- solentware-base-5.2/PKG-INFO +68 -0
- {solentware-base-5.0 → solentware-base-5.2}/README +1 -1
- solentware-base-5.2/pyproject.toml +42 -0
- solentware-base-5.2/setup.cfg +4 -0
- {solentware-base-5.0 → solentware-base-5.2}/solentware_base/core/_bytebit.py +6 -4
- {solentware-base-5.0 → solentware-base-5.2}/solentware_base/core/_database.py +120 -5
- solentware-base-5.2/solentware_base/core/_databasedu.py +272 -0
- {solentware-base-5.0 → solentware-base-5.2}/solentware_base/core/_db.py +168 -79
- {solentware-base-5.0 → solentware-base-5.2}/solentware_base/core/_db_tkinter.py +191 -99
- {solentware-base-5.0 → solentware-base-5.2}/solentware_base/core/_dbdu.py +254 -256
- {solentware-base-5.0 → solentware-base-5.2}/solentware_base/core/_dbdu_tkinter.py +333 -315
- {solentware-base-5.0 → solentware-base-5.2}/solentware_base/core/_dpt.py +513 -281
- {solentware-base-5.0 → solentware-base-5.2}/solentware_base/core/_lmdb.py +229 -239
- {solentware-base-5.0 → solentware-base-5.2}/solentware_base/core/_lmdbdu.py +234 -53
- {solentware-base-5.0 → solentware-base-5.2}/solentware_base/core/_nosql.py +194 -53
- {solentware-base-5.0 → solentware-base-5.2}/solentware_base/core/_nosqldu.py +41 -43
- {solentware-base-5.0 → solentware-base-5.2}/solentware_base/core/_sqlite.py +63 -54
- {solentware-base-5.0 → solentware-base-5.2}/solentware_base/core/_sqlitedu.py +358 -242
- {solentware-base-5.0 → solentware-base-5.2}/solentware_base/core/bytebit.py +0 -1
- {solentware-base-5.0 → solentware-base-5.2}/solentware_base/core/constants.py +8 -0
- {solentware-base-5.0 → solentware-base-5.2}/solentware_base/core/cursor.py +19 -8
- {solentware-base-5.0 → solentware-base-5.2}/solentware_base/core/filespec.py +10 -11
- {solentware-base-5.0 → solentware-base-5.2}/solentware_base/core/find.py +10 -5
- solentware-base-5.2/solentware_base/core/merge.py +166 -0
- {solentware-base-5.0 → solentware-base-5.2}/solentware_base/core/record.py +15 -12
- {solentware-base-5.0 → solentware-base-5.2}/solentware_base/core/recordset.py +264 -375
- solentware-base-5.2/solentware_base/core/recordsetbasecursor.py +120 -0
- solentware-base-5.2/solentware_base/core/recordsetcursor.py +167 -0
- {solentware-base-5.0 → solentware-base-5.2}/solentware_base/core/tree.py +3 -4
- {solentware-base-5.0 → solentware-base-5.2}/solentware_base/core/where.py +3 -4
- {solentware-base-5.0 → solentware-base-5.2}/solentware_base/core/wherevalues.py +2 -1
- solentware-base-5.2/solentware_base/dptdu_database.py +234 -0
- solentware-base-5.2/solentware_base/dptfastload_database.py +225 -0
- {solentware-base-5.0 → solentware-base-5.2}/solentware_base/gnu_database.py +10 -0
- {solentware-base-5.0 → solentware-base-5.2}/solentware_base/gnu_module.py +7 -0
- {solentware-base-5.0 → solentware-base-5.2}/solentware_base/modulequery.py +27 -31
- {solentware-base-5.0 → solentware-base-5.2}/solentware_base/ndbm_database.py +15 -0
- {solentware-base-5.0 → solentware-base-5.2}/solentware_base/ndbm_module.py +7 -0
- {solentware-base-5.0 → solentware-base-5.2}/solentware_base/tools/base_3_to_4.py +4 -12
- {solentware-base-5.0 → solentware-base-5.2}/solentware_base/tools/base_3_to_4_db.py +4 -14
- {solentware-base-5.0 → solentware-base-5.2}/solentware_base/tools/base_3_to_4_sqlite.py +5 -13
- {solentware-base-5.0 → solentware-base-5.2}/solentware_base/tools/berkeleydb_du_splice_fix.py +5 -5
- solentware-base-5.2/solentware_base/tools/compare_dpt_tape_files.py +279 -0
- {solentware-base-5.0 → solentware-base-5.2}/solentware_base/tools/create_database.py +15 -7
- {solentware-base-5.0 → solentware-base-5.2}/solentware_base/tools/sqlite_table_name_upgrade.py +1 -2
- solentware-base-5.2/solentware_base/tools/traverse_dpt_tape_files.py +294 -0
- {solentware-base-5.0 → solentware-base-5.2}/solentware_base/tools/ui_base_3_to_4.py +12 -6
- solentware-base-5.2/solentware_base.egg-info/PKG-INFO +68 -0
- {solentware-base-5.0 → solentware-base-5.2}/solentware_base.egg-info/SOURCES.txt +7 -3
- solentware-base-5.2/tox.ini +22 -0
- solentware-base-5.0/MANIFEST.in +0 -1
- solentware-base-5.0/PKG-INFO +0 -53
- solentware-base-5.0/pyproject.toml +0 -9
- solentware-base-5.0/setup.cfg +0 -31
- solentware-base-5.0/setup.py +0 -16
- solentware-base-5.0/solentware_base/core/_databasedu.py +0 -135
- solentware-base-5.0/solentware_base/do_deferred_updates.py +0 -62
- solentware-base-5.0/solentware_base/dptdu_database.py +0 -225
- solentware-base-5.0/solentware_base.egg-info/PKG-INFO +0 -53
- {solentware-base-5.0 → solentware-base-5.2}/LICENCE +0 -0
- {solentware-base-5.0 → solentware-base-5.2}/solentware_base/__init__.py +0 -0
- {solentware-base-5.0 → solentware-base-5.2}/solentware_base/apsw_database.py +0 -0
- {solentware-base-5.0 → solentware-base-5.2}/solentware_base/apswdu_database.py +0 -0
- {solentware-base-5.0 → solentware-base-5.2}/solentware_base/berkeleydb_database.py +0 -0
- {solentware-base-5.0 → solentware-base-5.2}/solentware_base/berkeleydbdu_database.py +0 -0
- {solentware-base-5.0 → solentware-base-5.2}/solentware_base/bsddb3_database.py +0 -0
- {solentware-base-5.0 → solentware-base-5.2}/solentware_base/bsddb3du_database.py +0 -0
- {solentware-base-5.0 → solentware-base-5.2}/solentware_base/core/__init__.py +0 -0
- {solentware-base-5.0 → solentware-base-5.2}/solentware_base/core/findvalues.py +0 -0
- {solentware-base-5.0 → solentware-base-5.2}/solentware_base/core/segmentsize.py +0 -0
- {solentware-base-5.0 → solentware-base-5.2}/solentware_base/db_tcl.py +0 -0
- {solentware-base-5.0 → solentware-base-5.2}/solentware_base/db_tkinter_database.py +0 -0
- {solentware-base-5.0 → solentware-base-5.2}/solentware_base/db_tkinterdu_database.py +0 -0
- {solentware-base-5.0 → solentware-base-5.2}/solentware_base/dpt_database.py +0 -0
- {solentware-base-5.0 → solentware-base-5.2}/solentware_base/gnudu_database.py +0 -0
- {solentware-base-5.0 → solentware-base-5.2}/solentware_base/lmdb_database.py +0 -0
- {solentware-base-5.0 → solentware-base-5.2}/solentware_base/lmdbdu_database.py +0 -0
- {solentware-base-5.0 → solentware-base-5.2}/solentware_base/ndbmdu_database.py +0 -0
- {solentware-base-5.0 → solentware-base-5.2}/solentware_base/sqlite3_database.py +0 -0
- {solentware-base-5.0 → solentware-base-5.2}/solentware_base/sqlite3du_database.py +0 -0
- {solentware-base-5.0 → solentware-base-5.2}/solentware_base/tools/__init__.py +0 -0
- {solentware-base-5.0 → solentware-base-5.2}/solentware_base/unqlite_database.py +0 -0
- {solentware-base-5.0 → solentware-base-5.2}/solentware_base/unqlitedu_database.py +0 -0
- {solentware-base-5.0 → solentware-base-5.2}/solentware_base/vedis_database.py +0 -0
- {solentware-base-5.0 → solentware-base-5.2}/solentware_base/vedisdu_database.py +0 -0
- {solentware-base-5.0 → solentware-base-5.2}/solentware_base.egg-info/dependency_links.txt +0 -0
- {solentware-base-5.0 → solentware-base-5.2}/solentware_base.egg-info/top_level.txt +0 -0
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: solentware-base
|
|
3
|
+
Version: 5.2
|
|
4
|
+
Summary: Bitmapped record number databases.
|
|
5
|
+
Author-email: Roger Marsh <roger.marsh@solentware.co.uk>
|
|
6
|
+
License: Licence for solentware_base package
|
|
7
|
+
Copyright (c) 2009, Roger Marsh
|
|
8
|
+
All rights reserved.
|
|
9
|
+
|
|
10
|
+
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
|
|
14
|
+
|
|
15
|
+
Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
|
|
16
|
+
|
|
17
|
+
Neither the name of the owner nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
21
|
+
|
|
22
|
+
Project-URL: Homepage, http://www.solentware.co.uk
|
|
23
|
+
Project-URL: Repository, https://github.com/RogerMarsh/solentware-base.git
|
|
24
|
+
Project-URL: Bug Tracker, https://github.com/RogerMarsh/solentware-base/issues
|
|
25
|
+
Classifier: License :: OSI Approved :: BSD License
|
|
26
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
27
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
28
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
29
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
30
|
+
Classifier: Operating System :: OS Independent
|
|
31
|
+
Classifier: Topic :: Database
|
|
32
|
+
Classifier: Intended Audience :: Developers
|
|
33
|
+
Classifier: Development Status :: 3 - Alpha
|
|
34
|
+
License-File: LICENCE
|
|
35
|
+
|
|
36
|
+
=========================================================================
|
|
37
|
+
A bitmapped record number database interface for various database engines
|
|
38
|
+
=========================================================================
|
|
39
|
+
|
|
40
|
+
.. contents::
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
Description
|
|
44
|
+
===========
|
|
45
|
+
|
|
46
|
+
A record number database is seen as a set of fixed size segments, and an existence bitmap is associated with each segment which contains any records. The idea is from Model 204 (Rocket Software).
|
|
47
|
+
|
|
48
|
+
Where an index value refers to many records in a segment these references are encoded as a bitmap or a list of record numbers, in a single <index value> : <bitmap or list> record, rather than as many <index value> : <record number> records. When 'many' is small a list is favoured to save the space needed for large numbers of unset bits.
|
|
49
|
+
|
|
50
|
+
The interfaces provided by the apsw, berkeleydb, bsddb3, dptdb, lmdb, and sqlite3, Python modules are supported.
|
|
51
|
+
|
|
52
|
+
The interfaces provided by the dbm.gnu, dbm.ndbm, unqlite, and vedis, Python modules are available for experiment.
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
Installation Instructions
|
|
56
|
+
=========================
|
|
57
|
+
|
|
58
|
+
This package is available on PyPI so can be installed by typing
|
|
59
|
+
|
|
60
|
+
python -m pip install --user solentware-base
|
|
61
|
+
|
|
62
|
+
at the command prompt.
|
|
63
|
+
|
|
64
|
+
See `Python`_ and `pip`_ documentation for alternatives and platform-specific points.
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
.. _Python: https://python.org
|
|
68
|
+
.. _pip: https://packaging.python.org/installing
|
|
@@ -12,7 +12,7 @@ A record number database is seen as a set of fixed size segments, and an existen
|
|
|
12
12
|
|
|
13
13
|
Where an index value refers to many records in a segment these references are encoded as a bitmap or a list of record numbers, in a single <index value> : <bitmap or list> record, rather than as many <index value> : <record number> records. When 'many' is small a list is favoured to save the space needed for large numbers of unset bits.
|
|
14
14
|
|
|
15
|
-
The interfaces provided by the apsw, berkeleydb, bsddb3, dptdb, and sqlite3, Python modules are supported.
|
|
15
|
+
The interfaces provided by the apsw, berkeleydb, bsddb3, dptdb, lmdb, and sqlite3, Python modules are supported.
|
|
16
16
|
|
|
17
17
|
The interfaces provided by the dbm.gnu, dbm.ndbm, unqlite, and vedis, Python modules are available for experiment.
|
|
18
18
|
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = [
|
|
3
|
+
"setuptools>=61.0",
|
|
4
|
+
"wheel"
|
|
5
|
+
]
|
|
6
|
+
build-backend = "setuptools.build_meta"
|
|
7
|
+
|
|
8
|
+
[tool.black]
|
|
9
|
+
line-length = 79
|
|
10
|
+
|
|
11
|
+
[project]
|
|
12
|
+
name = "solentware-base"
|
|
13
|
+
version = "5.2"
|
|
14
|
+
authors = [
|
|
15
|
+
{ name="Roger Marsh", email="roger.marsh@solentware.co.uk" },
|
|
16
|
+
]
|
|
17
|
+
description = "Bitmapped record number databases."
|
|
18
|
+
classifiers = [
|
|
19
|
+
"License :: OSI Approved :: BSD License",
|
|
20
|
+
"Programming Language :: Python :: 3.8",
|
|
21
|
+
"Programming Language :: Python :: 3.9",
|
|
22
|
+
"Programming Language :: Python :: 3.10",
|
|
23
|
+
"Programming Language :: Python :: 3.11",
|
|
24
|
+
"Operating System :: OS Independent",
|
|
25
|
+
"Topic :: Database",
|
|
26
|
+
"Intended Audience :: Developers",
|
|
27
|
+
"Development Status :: 3 - Alpha",
|
|
28
|
+
]
|
|
29
|
+
license = {file = "LICENCE"}
|
|
30
|
+
readme = "README"
|
|
31
|
+
|
|
32
|
+
[project.urls]
|
|
33
|
+
Homepage = "http://www.solentware.co.uk"
|
|
34
|
+
Repository = "https://github.com/RogerMarsh/solentware-base.git"
|
|
35
|
+
"Bug Tracker" = "https://github.com/RogerMarsh/solentware-base/issues"
|
|
36
|
+
|
|
37
|
+
[tool.setuptools]
|
|
38
|
+
packages = [
|
|
39
|
+
"solentware_base",
|
|
40
|
+
"solentware_base.core",
|
|
41
|
+
"solentware_base.tools",
|
|
42
|
+
]
|
|
@@ -72,6 +72,7 @@ class Bitarray:
|
|
|
72
72
|
|
|
73
73
|
"""
|
|
74
74
|
# Time taken proportional to number of non-zero bytes.
|
|
75
|
+
del value
|
|
75
76
|
return sum(self.bitarray_bytes.translate(_bits_count, b"\x00"))
|
|
76
77
|
|
|
77
78
|
def frombytes(self, from_):
|
|
@@ -118,8 +119,8 @@ class Bitarray:
|
|
|
118
119
|
):
|
|
119
120
|
if self.bitarray_bytes[start_byte] & 128 >> bit:
|
|
120
121
|
return 8 * start_byte + bit
|
|
121
|
-
except IndexError:
|
|
122
|
-
raise ValueError("Set bit (True) not found")
|
|
122
|
+
except IndexError as exc:
|
|
123
|
+
raise ValueError("Set bit (True) not found") from exc
|
|
123
124
|
for byte in range(1 + start_byte, stop_byte):
|
|
124
125
|
if self.bitarray_bytes[byte] != 0:
|
|
125
126
|
return 8 * byte + _first_set_bit[self.bitarray_bytes[byte]]
|
|
@@ -137,8 +138,8 @@ class Bitarray:
|
|
|
137
138
|
):
|
|
138
139
|
if not self.bitarray_bytes[start_byte] & 128 >> bit:
|
|
139
140
|
return 8 * start_byte + bit
|
|
140
|
-
except IndexError:
|
|
141
|
-
raise ValueError("Unset bit (False) not found")
|
|
141
|
+
except IndexError as exc:
|
|
142
|
+
raise ValueError("Unset bit (False) not found") from exc
|
|
142
143
|
for byte in range(1 + start_byte, stop_byte):
|
|
143
144
|
if self.bitarray_bytes[byte] != 255:
|
|
144
145
|
for bit in range(0, 8):
|
|
@@ -183,6 +184,7 @@ class Bitarray:
|
|
|
183
184
|
in bitarray-0.8.1 is search(bitarray('1')).
|
|
184
185
|
|
|
185
186
|
"""
|
|
187
|
+
del bitarray, limit
|
|
186
188
|
bitscan = []
|
|
187
189
|
for j, byte in enumerate(self.bitarray_bytes):
|
|
188
190
|
if not byte:
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
# Copyright 2008, 2019 Roger Marsh
|
|
3
3
|
# Licence: See LICENCE (BSD licence)
|
|
4
4
|
|
|
5
|
-
"""Define the
|
|
5
|
+
"""Define the interface shared by python database modules.
|
|
6
6
|
|
|
7
7
|
The major components are the delete_instance, edit_instance, and put_instance,
|
|
8
8
|
methods. The _dpt module's versions of these methods are too different to
|
|
@@ -24,8 +24,53 @@ from .recordset import (
|
|
|
24
24
|
)
|
|
25
25
|
|
|
26
26
|
|
|
27
|
+
class DataSourceCursorError(Exception):
|
|
28
|
+
"""Exception for database-specific DataSourceCursor setting."""
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# The Database class was added to the ignored-classes list of pylint.conf for
|
|
32
|
+
# solentware_base at a time when all the no-member errors (E1101) referred
|
|
33
|
+
# to it or one of it's subclasses with the same name in another module.
|
|
27
34
|
class Database:
|
|
28
|
-
"""Define file and record access methods; which subclasses may override.
|
|
35
|
+
"""Define file and record access methods; which subclasses may override.
|
|
36
|
+
|
|
37
|
+
The set_segment_size() method is used to set the segment size to fit
|
|
38
|
+
the segment size recorded on a database. There is no segment_size_bytes
|
|
39
|
+
class attribute.
|
|
40
|
+
|
|
41
|
+
Property file_per_database returns False because all 'key:value' sets,
|
|
42
|
+
and the associated inverted list indicies, are held in one file by
|
|
43
|
+
default. Berkeley DB is the known example where it is reasonable to
|
|
44
|
+
return True: override the class attribute, if this case is wanted.
|
|
45
|
+
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
_file_per_database = False
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def file_per_database(self):
|
|
52
|
+
"""Return True if each database is in a separate file.
|
|
53
|
+
|
|
54
|
+
Berkeley DB is the known cases where True is reasonable (see ._db
|
|
55
|
+
module), and DPT is the known case where True is mandatory (see
|
|
56
|
+
._dpt module).
|
|
57
|
+
|
|
58
|
+
"""
|
|
59
|
+
return self._file_per_database
|
|
60
|
+
|
|
61
|
+
def _generate_database_file_name(self, name):
|
|
62
|
+
"""Return path to database file.
|
|
63
|
+
|
|
64
|
+
By default this is a file where os.path.basename(path) is same as
|
|
65
|
+
os.path.dirname(path); and path is assumed to follow this rule.
|
|
66
|
+
|
|
67
|
+
However some database engines mangle os.path.basename(path) to
|
|
68
|
+
give the file name. Override this method if necessary to apply
|
|
69
|
+
the required mangle to os.path.basename(path).
|
|
70
|
+
|
|
71
|
+
"""
|
|
72
|
+
del name
|
|
73
|
+
return self.database_file
|
|
29
74
|
|
|
30
75
|
def delete_instance(self, dbset, instance):
|
|
31
76
|
"""Delete an existing instance on databases in dbset.
|
|
@@ -90,7 +135,6 @@ class Database:
|
|
|
90
135
|
self.delete(dbset, oldkey, instance.srvalue)
|
|
91
136
|
key = self.put(dbset, newkey, instance.newrecord.srvalue)
|
|
92
137
|
if key is not None:
|
|
93
|
-
|
|
94
138
|
# put was append to record number database and
|
|
95
139
|
# returned the new primary key. Adjust record key
|
|
96
140
|
# for secondary updates.
|
|
@@ -180,7 +224,6 @@ class Database:
|
|
|
180
224
|
putkey = instance.key.pack()
|
|
181
225
|
instance.set_packed_value_and_indexes()
|
|
182
226
|
if putkey is None:
|
|
183
|
-
|
|
184
227
|
# reuse record number if possible
|
|
185
228
|
putkey = self.get_lowest_freed_record_number(dbset)
|
|
186
229
|
if putkey is not None:
|
|
@@ -188,7 +231,6 @@ class Database:
|
|
|
188
231
|
|
|
189
232
|
key = self.put(dbset, putkey, instance.srvalue)
|
|
190
233
|
if key is not None:
|
|
191
|
-
|
|
192
234
|
# put was append to record number database and
|
|
193
235
|
# returned the new primary key. Adjust record key
|
|
194
236
|
# for secondary updates.
|
|
@@ -229,6 +271,7 @@ class Database:
|
|
|
229
271
|
|
|
230
272
|
def make_segment(self, key, segment_number, record_count, records):
|
|
231
273
|
"""Return a Segment subclass instance created from arguments."""
|
|
274
|
+
del key # Looks reasonable to remove key from arguments.
|
|
232
275
|
if record_count == 1:
|
|
233
276
|
return RecordsetSegmentInt(
|
|
234
277
|
segment_number,
|
|
@@ -300,6 +343,7 @@ class Database:
|
|
|
300
343
|
*a absorbs the arguments needed by the DPT version of this method.
|
|
301
344
|
|
|
302
345
|
"""
|
|
346
|
+
del a
|
|
303
347
|
return oldcursor
|
|
304
348
|
|
|
305
349
|
def allocate_and_open_contexts(self, files=None):
|
|
@@ -311,6 +355,7 @@ class Database:
|
|
|
311
355
|
The Berkeley DB DBEnv or SQLite3 Connection object is assumed closed as
|
|
312
356
|
well so open all files in the specification.
|
|
313
357
|
"""
|
|
358
|
+
del files
|
|
314
359
|
self.open_database()
|
|
315
360
|
|
|
316
361
|
def open_database_contexts(self, files=None):
|
|
@@ -329,6 +374,76 @@ class Database:
|
|
|
329
374
|
def end_read_only_transaction(self):
|
|
330
375
|
"""Do nothing, present for compatibility with Symas LMMD."""
|
|
331
376
|
|
|
377
|
+
def increase_database_record_capacity(self, **kwargs):
|
|
378
|
+
"""Do nothing, present for compatibility with DPT."""
|
|
379
|
+
del kwargs
|
|
380
|
+
|
|
381
|
+
def get_database_table_sizes(self, **kwargs):
|
|
382
|
+
"""Return empty dict, present for compatibility with DPT."""
|
|
383
|
+
del kwargs
|
|
384
|
+
return {}
|
|
385
|
+
|
|
386
|
+
def close_datasourcecursor_recordset(self, datasourcecursor):
|
|
387
|
+
"""Do nothing. Override if necessary.
|
|
388
|
+
|
|
389
|
+
Present for compatibility with DPT database engine.
|
|
390
|
+
"""
|
|
391
|
+
del datasourcecursor
|
|
392
|
+
|
|
393
|
+
def set_datasourcecursor_recordset(self, datasourcecursor, recordset):
|
|
394
|
+
"""Validate and set recordset as datasourcecursor's recordset.
|
|
395
|
+
|
|
396
|
+
The recordset and datasourcecursor must be associated with the same
|
|
397
|
+
database identity and be the identity of self.
|
|
398
|
+
|
|
399
|
+
This method exists because in DPT these checks are implied when the
|
|
400
|
+
recordset and cursor are created, and both objects are details of
|
|
401
|
+
the implementation.
|
|
402
|
+
"""
|
|
403
|
+
if datasourcecursor.dbhome is not self:
|
|
404
|
+
raise DataSourceCursorError("DataSource is not for this database")
|
|
405
|
+
if datasourcecursor.recordset:
|
|
406
|
+
if (
|
|
407
|
+
datasourcecursor.recordset.dbidentity
|
|
408
|
+
== recordset.recordset.dbidentity
|
|
409
|
+
):
|
|
410
|
+
datasourcecursor.recordset.recordset.close()
|
|
411
|
+
datasourcecursor.recordset = recordset
|
|
412
|
+
else:
|
|
413
|
+
raise DataSourceCursorError(
|
|
414
|
+
"New and existing Recordsets are for different databases"
|
|
415
|
+
)
|
|
416
|
+
elif datasourcecursor.dbidentity == recordset.recordset.dbidentity:
|
|
417
|
+
datasourcecursor.recordset = recordset
|
|
418
|
+
else:
|
|
419
|
+
raise DataSourceCursorError(
|
|
420
|
+
"New Recordset and DataSource are for different databases"
|
|
421
|
+
)
|
|
422
|
+
|
|
423
|
+
def get_datasourcecursor_recordset_cursor(self, dsc):
|
|
424
|
+
"""Create and return cursor on dsc's recordset.
|
|
425
|
+
|
|
426
|
+
dsc not datasourcecursor to shorten argument name.
|
|
427
|
+
|
|
428
|
+
"""
|
|
429
|
+
if dsc.dbhome is not self:
|
|
430
|
+
raise DataSourceCursorError("DataSource is not for this database")
|
|
431
|
+
if dsc.recordset:
|
|
432
|
+
if dsc.dbidentity == dsc.recordset.recordset.dbidentity:
|
|
433
|
+
cursor = dsc.recordset.dbhome.create_recordset_cursor(
|
|
434
|
+
dsc.recordset.recordset
|
|
435
|
+
)
|
|
436
|
+
else:
|
|
437
|
+
raise DataSourceCursorError(
|
|
438
|
+
"Recordset and DataSource are for different databases"
|
|
439
|
+
)
|
|
440
|
+
else:
|
|
441
|
+
dsc.recordset = dsc.dbhome.recordlist_nil(dsc.dbset)
|
|
442
|
+
cursor = dsc.recordset.dbhome.create_recordset_cursor(
|
|
443
|
+
dsc.recordset.recordset
|
|
444
|
+
)
|
|
445
|
+
return cursor
|
|
446
|
+
|
|
332
447
|
|
|
333
448
|
class ExistenceBitmapControl:
|
|
334
449
|
"""Base class for managing existence bitmap of file in database.
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
# _databasedu.py
|
|
2
|
+
# Copyright 2008, 2019 Roger Marsh
|
|
3
|
+
# Licence: See LICENCE (BSD licence)
|
|
4
|
+
|
|
5
|
+
"""Define the database interface for deferred updates.
|
|
6
|
+
|
|
7
|
+
The major component is the put_instance method. The DPT database way of doing
|
|
8
|
+
deferred updates is so different there is no equivalent separate module for
|
|
9
|
+
DPT.
|
|
10
|
+
|
|
11
|
+
"""
|
|
12
|
+
from . import _database
|
|
13
|
+
from .segmentsize import SegmentSize
|
|
14
|
+
from .constants import SECONDARY
|
|
15
|
+
from .bytebit import Bitarray
|
|
16
|
+
from . import merge
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class DatabaseduError(Exception):
|
|
20
|
+
"""Exception for Database class."""
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class Database(_database.Database):
|
|
24
|
+
"""Provide deferred update versions of the record update methods."""
|
|
25
|
+
|
|
26
|
+
def put_instance(self, dbset, instance):
|
|
27
|
+
"""Put new instance on database dbset.
|
|
28
|
+
|
|
29
|
+
This method assumes all primary databases are integer primary key,
|
|
30
|
+
and there is enough memory to do a segment at a time.
|
|
31
|
+
|
|
32
|
+
"""
|
|
33
|
+
putkey = instance.key.pack()
|
|
34
|
+
instance.set_packed_value_and_indexes()
|
|
35
|
+
if putkey is not None:
|
|
36
|
+
# reuse record number is not allowed
|
|
37
|
+
raise DatabaseduError(
|
|
38
|
+
"Cannot reuse record number in deferred update."
|
|
39
|
+
)
|
|
40
|
+
key = self.put(dbset, putkey, instance.srvalue)
|
|
41
|
+
|
|
42
|
+
# put was append to record number database and
|
|
43
|
+
# returned the new primary key. Adjust record key
|
|
44
|
+
# for secondary updates.
|
|
45
|
+
instance.key.load(key)
|
|
46
|
+
putkey = key
|
|
47
|
+
|
|
48
|
+
instance.srkey = self.encode_record_number(putkey)
|
|
49
|
+
srindex = instance.srindex
|
|
50
|
+
segment, record_number = divmod(putkey, SegmentSize.db_segment_size)
|
|
51
|
+
self._defer_add_record_to_ebm(dbset, segment, record_number)
|
|
52
|
+
pcb = instance.putcallbacks
|
|
53
|
+
for secondary in srindex:
|
|
54
|
+
if secondary not in self.specification[dbset][SECONDARY]:
|
|
55
|
+
if secondary in pcb:
|
|
56
|
+
pcb[secondary](instance, srindex[secondary])
|
|
57
|
+
continue
|
|
58
|
+
for j in srindex[secondary]:
|
|
59
|
+
self._defer_add_record_to_field_value(
|
|
60
|
+
dbset, secondary, j, segment, record_number
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
if record_number in self.deferred_update_points:
|
|
64
|
+
self._write_existence_bit_map(dbset, segment)
|
|
65
|
+
for secondary in self.specification[dbset][SECONDARY]:
|
|
66
|
+
self.sort_and_write(dbset, secondary, segment)
|
|
67
|
+
if record_number == max(self.deferred_update_points):
|
|
68
|
+
self.first_chunk[dbset] = True
|
|
69
|
+
elif record_number == min(self.deferred_update_points):
|
|
70
|
+
self.first_chunk[dbset] = False
|
|
71
|
+
self.high_segment[dbset] = segment
|
|
72
|
+
|
|
73
|
+
def index_instance(self, dbset, instance):
|
|
74
|
+
"""Apply instance index values on database dbset.
|
|
75
|
+
|
|
76
|
+
This method assumes all primary databases are integer primary key,
|
|
77
|
+
and there is enough memory to do a segment at a time.
|
|
78
|
+
|
|
79
|
+
"""
|
|
80
|
+
putkey = instance.key.pack()
|
|
81
|
+
if putkey is None:
|
|
82
|
+
# Apply index values without record number is not allowed.
|
|
83
|
+
raise DatabaseduError(
|
|
84
|
+
"Cannot apply index values without a record number"
|
|
85
|
+
)
|
|
86
|
+
instance.set_packed_value_and_indexes()
|
|
87
|
+
instance.srkey = self.encode_record_number(putkey)
|
|
88
|
+
srindex = instance.srindex
|
|
89
|
+
segment, record_number = divmod(putkey, SegmentSize.db_segment_size)
|
|
90
|
+
pcb = instance.putcallbacks
|
|
91
|
+
for secondary in srindex:
|
|
92
|
+
if secondary not in self.specification[dbset][SECONDARY]:
|
|
93
|
+
if secondary in pcb:
|
|
94
|
+
pcb[secondary](instance, srindex[secondary])
|
|
95
|
+
continue
|
|
96
|
+
for j in srindex[secondary]:
|
|
97
|
+
self._defer_add_record_to_field_value(
|
|
98
|
+
dbset, secondary, j, segment, record_number
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
if record_number in self.deferred_update_points:
|
|
102
|
+
for secondary in srindex:
|
|
103
|
+
assert secondary in self.specification[dbset][SECONDARY]
|
|
104
|
+
self.sort_and_write(dbset, secondary, segment)
|
|
105
|
+
if record_number == max(self.deferred_update_points):
|
|
106
|
+
self.first_chunk[dbset] = True
|
|
107
|
+
elif record_number == min(self.deferred_update_points):
|
|
108
|
+
self.first_chunk[dbset] = False
|
|
109
|
+
self.high_segment[dbset] = segment
|
|
110
|
+
|
|
111
|
+
def _defer_add_record_to_ebm(self, file, segment, record_number):
|
|
112
|
+
"""Add bit to existence bit map for new record and defer update."""
|
|
113
|
+
assert file in self.specification
|
|
114
|
+
try:
|
|
115
|
+
# Assume cached segment existence bit map exists
|
|
116
|
+
self.existence_bit_maps[file][segment][record_number] = True
|
|
117
|
+
except KeyError:
|
|
118
|
+
# Get the segment existence bit map from database
|
|
119
|
+
ebmb = self.get_ebm_segment(self.ebm_control[file], segment)
|
|
120
|
+
if ebmb is None:
|
|
121
|
+
# It does not exist so create a new empty one
|
|
122
|
+
ebm = SegmentSize.empty_bitarray.copy()
|
|
123
|
+
else:
|
|
124
|
+
# It does exist so convert database representation to bitarray
|
|
125
|
+
ebm = Bitarray()
|
|
126
|
+
ebm.frombytes(ebmb)
|
|
127
|
+
# Set bit for record number and add segment to cache
|
|
128
|
+
ebm[record_number] = True
|
|
129
|
+
if file not in self.existence_bit_maps:
|
|
130
|
+
self.existence_bit_maps[file] = {}
|
|
131
|
+
self.existence_bit_maps[file][segment] = ebm
|
|
132
|
+
|
|
133
|
+
def _defer_add_record_to_field_value(
|
|
134
|
+
self, file, field, key, segment, record_number
|
|
135
|
+
):
|
|
136
|
+
"""Add record_number to cached segment for key."""
|
|
137
|
+
del segment
|
|
138
|
+
assert file in self.specification
|
|
139
|
+
try:
|
|
140
|
+
value_segments = self.value_segments[file][field]
|
|
141
|
+
except KeyError:
|
|
142
|
+
value_segments = self.value_segments.setdefault(
|
|
143
|
+
file, {}
|
|
144
|
+
).setdefault(field, {})
|
|
145
|
+
values = value_segments.get(key)
|
|
146
|
+
if values is None:
|
|
147
|
+
value_segments[key] = [record_number]
|
|
148
|
+
elif isinstance(values, list):
|
|
149
|
+
# A (value, record_number) can be given many times.
|
|
150
|
+
# Ensure a record_number appears in the list once only.
|
|
151
|
+
if values[-1] != record_number:
|
|
152
|
+
values.append(record_number)
|
|
153
|
+
if len(values) > SegmentSize.db_upper_conversion_limit:
|
|
154
|
+
vsk = value_segments[
|
|
155
|
+
key
|
|
156
|
+
] = SegmentSize.empty_bitarray.copy()
|
|
157
|
+
for j in values:
|
|
158
|
+
vsk[j] = True
|
|
159
|
+
vsk[record_number] = True
|
|
160
|
+
|
|
161
|
+
else:
|
|
162
|
+
values[record_number] = True
|
|
163
|
+
|
|
164
|
+
def _prepare_segment_record_list(self, file, field):
|
|
165
|
+
"""Convert dict of record number lists to database record format.
|
|
166
|
+
|
|
167
|
+
A single record in a segment for an index value is represented
|
|
168
|
+
as a number within the segment.
|
|
169
|
+
|
|
170
|
+
Multiple records are represented as lists of record numbers or
|
|
171
|
+
bitmaps depending on how many are in the segment.
|
|
172
|
+
|
|
173
|
+
"""
|
|
174
|
+
# Lookup table is much quicker, and noticeable, in bulk use.
|
|
175
|
+
int_to_bytes = self._int_to_bytes
|
|
176
|
+
|
|
177
|
+
segvalues = self.value_segments[file][field]
|
|
178
|
+
for k in segvalues:
|
|
179
|
+
value = segvalues[k]
|
|
180
|
+
if isinstance(value, list):
|
|
181
|
+
# A single record is presented as an integer: the
|
|
182
|
+
# database engine will decide the transformation.
|
|
183
|
+
if len(value) == 1:
|
|
184
|
+
segvalues[k] = [1, value[-1]]
|
|
185
|
+
else:
|
|
186
|
+
segvalues[k] = [
|
|
187
|
+
len(value),
|
|
188
|
+
b"".join([int_to_bytes[n] for n in value]),
|
|
189
|
+
]
|
|
190
|
+
|
|
191
|
+
else:
|
|
192
|
+
segvalues[k] = [
|
|
193
|
+
value.count(),
|
|
194
|
+
value.tobytes(),
|
|
195
|
+
]
|
|
196
|
+
|
|
197
|
+
def set_segment_size(self):
|
|
198
|
+
"""Extend and set a deferred update point at end of segment."""
|
|
199
|
+
super().set_segment_size()
|
|
200
|
+
|
|
201
|
+
# Override in subclasses if more frequent deferred update is required.
|
|
202
|
+
self.deferred_update_points = frozenset(
|
|
203
|
+
[SegmentSize.db_segment_size - 1]
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
def deferred_update_housekeeping(self):
|
|
207
|
+
"""Do nothing. Subclasses should override this method as required.
|
|
208
|
+
|
|
209
|
+
Actions are specific to a database engine.
|
|
210
|
+
|
|
211
|
+
"""
|
|
212
|
+
|
|
213
|
+
def encode_number_for_sequential_file_dump(self, number, bytes_):
|
|
214
|
+
"""Return big-endian encoding of number in bytes_ bytes."""
|
|
215
|
+
return number.to_bytes(bytes_, byteorder="big")
|
|
216
|
+
|
|
217
|
+
def encode_segment_for_sequential_file_dump(self, record_numbers):
|
|
218
|
+
"""Return encoding of record numbers appropriate to record count."""
|
|
219
|
+
if len(record_numbers) > SegmentSize.db_upper_conversion_limit:
|
|
220
|
+
seg = SegmentSize.empty_bitarray.copy()
|
|
221
|
+
for bit in record_numbers:
|
|
222
|
+
seg[bit] = True
|
|
223
|
+
return seg.tobytes()
|
|
224
|
+
int_to_bytes = self._int_to_bytes
|
|
225
|
+
return b"".join([int_to_bytes[n] for n in record_numbers])
|
|
226
|
+
|
|
227
|
+
def set_int_to_bytes_lookup(self, lookup=True):
|
|
228
|
+
"""Create lookup to convert integers to bytestring big-endian.
|
|
229
|
+
|
|
230
|
+
Set lookup table to None if bool(lookup) is False.
|
|
231
|
+
|
|
232
|
+
"""
|
|
233
|
+
if lookup:
|
|
234
|
+
self._int_to_bytes = [
|
|
235
|
+
n.to_bytes(2, byteorder="big")
|
|
236
|
+
for n in range(SegmentSize.db_segment_size)
|
|
237
|
+
]
|
|
238
|
+
else:
|
|
239
|
+
self._int_to_bytes = None
|
|
240
|
+
|
|
241
|
+
def merge_import(self, index_directory, file, field, commit_limit):
|
|
242
|
+
"""Yield count of sorted items written to an index at intervals.
|
|
243
|
+
|
|
244
|
+
Sorted items from files in index_directory are written to index
|
|
245
|
+
field in file, and the count of items added is yielded every
|
|
246
|
+
commit_limit items.
|
|
247
|
+
|
|
248
|
+
"""
|
|
249
|
+
writer = self.merge_writer(file, field)
|
|
250
|
+
merger = merge.Merge(index_directory)
|
|
251
|
+
commit_count = None
|
|
252
|
+
for commit_count, item in enumerate(merger.sorter()):
|
|
253
|
+
if not commit_count % commit_limit:
|
|
254
|
+
if commit_count:
|
|
255
|
+
writer.close_cursor()
|
|
256
|
+
self.commit()
|
|
257
|
+
self.deferred_update_housekeeping()
|
|
258
|
+
yield commit_count
|
|
259
|
+
self.start_transaction()
|
|
260
|
+
writer.make_new_cursor()
|
|
261
|
+
writer.write(item)
|
|
262
|
+
writer.close_cursor()
|
|
263
|
+
if commit_count is not None:
|
|
264
|
+
self.commit()
|
|
265
|
+
self.deferred_update_housekeeping()
|
|
266
|
+
self.start_transaction()
|
|
267
|
+
|
|
268
|
+
def next_sorted_item(self, index_directory):
|
|
269
|
+
"""Yield sorted items from files in index_directory."""
|
|
270
|
+
merger = merge.Merge(index_directory)
|
|
271
|
+
for item in merger.sorter():
|
|
272
|
+
yield item
|