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.
Files changed (88) hide show
  1. solentware-base-5.2/MANIFEST.in +2 -0
  2. solentware-base-5.2/PKG-INFO +68 -0
  3. {solentware-base-5.0 → solentware-base-5.2}/README +1 -1
  4. solentware-base-5.2/pyproject.toml +42 -0
  5. solentware-base-5.2/setup.cfg +4 -0
  6. {solentware-base-5.0 → solentware-base-5.2}/solentware_base/core/_bytebit.py +6 -4
  7. {solentware-base-5.0 → solentware-base-5.2}/solentware_base/core/_database.py +120 -5
  8. solentware-base-5.2/solentware_base/core/_databasedu.py +272 -0
  9. {solentware-base-5.0 → solentware-base-5.2}/solentware_base/core/_db.py +168 -79
  10. {solentware-base-5.0 → solentware-base-5.2}/solentware_base/core/_db_tkinter.py +191 -99
  11. {solentware-base-5.0 → solentware-base-5.2}/solentware_base/core/_dbdu.py +254 -256
  12. {solentware-base-5.0 → solentware-base-5.2}/solentware_base/core/_dbdu_tkinter.py +333 -315
  13. {solentware-base-5.0 → solentware-base-5.2}/solentware_base/core/_dpt.py +513 -281
  14. {solentware-base-5.0 → solentware-base-5.2}/solentware_base/core/_lmdb.py +229 -239
  15. {solentware-base-5.0 → solentware-base-5.2}/solentware_base/core/_lmdbdu.py +234 -53
  16. {solentware-base-5.0 → solentware-base-5.2}/solentware_base/core/_nosql.py +194 -53
  17. {solentware-base-5.0 → solentware-base-5.2}/solentware_base/core/_nosqldu.py +41 -43
  18. {solentware-base-5.0 → solentware-base-5.2}/solentware_base/core/_sqlite.py +63 -54
  19. {solentware-base-5.0 → solentware-base-5.2}/solentware_base/core/_sqlitedu.py +358 -242
  20. {solentware-base-5.0 → solentware-base-5.2}/solentware_base/core/bytebit.py +0 -1
  21. {solentware-base-5.0 → solentware-base-5.2}/solentware_base/core/constants.py +8 -0
  22. {solentware-base-5.0 → solentware-base-5.2}/solentware_base/core/cursor.py +19 -8
  23. {solentware-base-5.0 → solentware-base-5.2}/solentware_base/core/filespec.py +10 -11
  24. {solentware-base-5.0 → solentware-base-5.2}/solentware_base/core/find.py +10 -5
  25. solentware-base-5.2/solentware_base/core/merge.py +166 -0
  26. {solentware-base-5.0 → solentware-base-5.2}/solentware_base/core/record.py +15 -12
  27. {solentware-base-5.0 → solentware-base-5.2}/solentware_base/core/recordset.py +264 -375
  28. solentware-base-5.2/solentware_base/core/recordsetbasecursor.py +120 -0
  29. solentware-base-5.2/solentware_base/core/recordsetcursor.py +167 -0
  30. {solentware-base-5.0 → solentware-base-5.2}/solentware_base/core/tree.py +3 -4
  31. {solentware-base-5.0 → solentware-base-5.2}/solentware_base/core/where.py +3 -4
  32. {solentware-base-5.0 → solentware-base-5.2}/solentware_base/core/wherevalues.py +2 -1
  33. solentware-base-5.2/solentware_base/dptdu_database.py +234 -0
  34. solentware-base-5.2/solentware_base/dptfastload_database.py +225 -0
  35. {solentware-base-5.0 → solentware-base-5.2}/solentware_base/gnu_database.py +10 -0
  36. {solentware-base-5.0 → solentware-base-5.2}/solentware_base/gnu_module.py +7 -0
  37. {solentware-base-5.0 → solentware-base-5.2}/solentware_base/modulequery.py +27 -31
  38. {solentware-base-5.0 → solentware-base-5.2}/solentware_base/ndbm_database.py +15 -0
  39. {solentware-base-5.0 → solentware-base-5.2}/solentware_base/ndbm_module.py +7 -0
  40. {solentware-base-5.0 → solentware-base-5.2}/solentware_base/tools/base_3_to_4.py +4 -12
  41. {solentware-base-5.0 → solentware-base-5.2}/solentware_base/tools/base_3_to_4_db.py +4 -14
  42. {solentware-base-5.0 → solentware-base-5.2}/solentware_base/tools/base_3_to_4_sqlite.py +5 -13
  43. {solentware-base-5.0 → solentware-base-5.2}/solentware_base/tools/berkeleydb_du_splice_fix.py +5 -5
  44. solentware-base-5.2/solentware_base/tools/compare_dpt_tape_files.py +279 -0
  45. {solentware-base-5.0 → solentware-base-5.2}/solentware_base/tools/create_database.py +15 -7
  46. {solentware-base-5.0 → solentware-base-5.2}/solentware_base/tools/sqlite_table_name_upgrade.py +1 -2
  47. solentware-base-5.2/solentware_base/tools/traverse_dpt_tape_files.py +294 -0
  48. {solentware-base-5.0 → solentware-base-5.2}/solentware_base/tools/ui_base_3_to_4.py +12 -6
  49. solentware-base-5.2/solentware_base.egg-info/PKG-INFO +68 -0
  50. {solentware-base-5.0 → solentware-base-5.2}/solentware_base.egg-info/SOURCES.txt +7 -3
  51. solentware-base-5.2/tox.ini +22 -0
  52. solentware-base-5.0/MANIFEST.in +0 -1
  53. solentware-base-5.0/PKG-INFO +0 -53
  54. solentware-base-5.0/pyproject.toml +0 -9
  55. solentware-base-5.0/setup.cfg +0 -31
  56. solentware-base-5.0/setup.py +0 -16
  57. solentware-base-5.0/solentware_base/core/_databasedu.py +0 -135
  58. solentware-base-5.0/solentware_base/do_deferred_updates.py +0 -62
  59. solentware-base-5.0/solentware_base/dptdu_database.py +0 -225
  60. solentware-base-5.0/solentware_base.egg-info/PKG-INFO +0 -53
  61. {solentware-base-5.0 → solentware-base-5.2}/LICENCE +0 -0
  62. {solentware-base-5.0 → solentware-base-5.2}/solentware_base/__init__.py +0 -0
  63. {solentware-base-5.0 → solentware-base-5.2}/solentware_base/apsw_database.py +0 -0
  64. {solentware-base-5.0 → solentware-base-5.2}/solentware_base/apswdu_database.py +0 -0
  65. {solentware-base-5.0 → solentware-base-5.2}/solentware_base/berkeleydb_database.py +0 -0
  66. {solentware-base-5.0 → solentware-base-5.2}/solentware_base/berkeleydbdu_database.py +0 -0
  67. {solentware-base-5.0 → solentware-base-5.2}/solentware_base/bsddb3_database.py +0 -0
  68. {solentware-base-5.0 → solentware-base-5.2}/solentware_base/bsddb3du_database.py +0 -0
  69. {solentware-base-5.0 → solentware-base-5.2}/solentware_base/core/__init__.py +0 -0
  70. {solentware-base-5.0 → solentware-base-5.2}/solentware_base/core/findvalues.py +0 -0
  71. {solentware-base-5.0 → solentware-base-5.2}/solentware_base/core/segmentsize.py +0 -0
  72. {solentware-base-5.0 → solentware-base-5.2}/solentware_base/db_tcl.py +0 -0
  73. {solentware-base-5.0 → solentware-base-5.2}/solentware_base/db_tkinter_database.py +0 -0
  74. {solentware-base-5.0 → solentware-base-5.2}/solentware_base/db_tkinterdu_database.py +0 -0
  75. {solentware-base-5.0 → solentware-base-5.2}/solentware_base/dpt_database.py +0 -0
  76. {solentware-base-5.0 → solentware-base-5.2}/solentware_base/gnudu_database.py +0 -0
  77. {solentware-base-5.0 → solentware-base-5.2}/solentware_base/lmdb_database.py +0 -0
  78. {solentware-base-5.0 → solentware-base-5.2}/solentware_base/lmdbdu_database.py +0 -0
  79. {solentware-base-5.0 → solentware-base-5.2}/solentware_base/ndbmdu_database.py +0 -0
  80. {solentware-base-5.0 → solentware-base-5.2}/solentware_base/sqlite3_database.py +0 -0
  81. {solentware-base-5.0 → solentware-base-5.2}/solentware_base/sqlite3du_database.py +0 -0
  82. {solentware-base-5.0 → solentware-base-5.2}/solentware_base/tools/__init__.py +0 -0
  83. {solentware-base-5.0 → solentware-base-5.2}/solentware_base/unqlite_database.py +0 -0
  84. {solentware-base-5.0 → solentware-base-5.2}/solentware_base/unqlitedu_database.py +0 -0
  85. {solentware-base-5.0 → solentware-base-5.2}/solentware_base/vedis_database.py +0 -0
  86. {solentware-base-5.0 → solentware-base-5.2}/solentware_base/vedisdu_database.py +0 -0
  87. {solentware-base-5.0 → solentware-base-5.2}/solentware_base.egg-info/dependency_links.txt +0 -0
  88. {solentware-base-5.0 → solentware-base-5.2}/solentware_base.egg-info/top_level.txt +0 -0
@@ -0,0 +1,2 @@
1
+ # MANIFEST.in exists to get tox.ini included in sdist distributions.
2
+ include tox.ini
@@ -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
+ ]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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 database interface shared by _db and _sqlite modules.
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