shareddata 6.85.7__tar.gz → 6.85.8__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 (187) hide show
  1. {shareddata-6.85.7/src/shareddata.egg-info → shareddata-6.85.8}/PKG-INFO +1 -1
  2. {shareddata-6.85.7 → shareddata-6.85.8}/setup.py +1 -1
  3. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/API/routes/cache.py +11 -1
  4. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/IO/SaveTables.py +1 -1
  5. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/SharedData.py +5 -1
  6. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/SharedNumpy.py +2 -2
  7. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Table.py +219 -32
  8. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/TableRewrite.py +99 -6
  9. {shareddata-6.85.7 → shareddata-6.85.8/src/shareddata.egg-info}/PKG-INFO +1 -1
  10. {shareddata-6.85.7 → shareddata-6.85.8}/LICENSE +0 -0
  11. {shareddata-6.85.7 → shareddata-6.85.8}/MANIFEST.in +0 -0
  12. {shareddata-6.85.7 → shareddata-6.85.8}/README.md +0 -0
  13. {shareddata-6.85.7 → shareddata-6.85.8}/pyproject.toml +0 -0
  14. {shareddata-6.85.7 → shareddata-6.85.8}/setup.cfg +0 -0
  15. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/API/ServerGunicorn.py +0 -0
  16. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/API/ServerWaitress.py +0 -0
  17. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/API/__init__.py +0 -0
  18. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/API/apidocs-dark.css +0 -0
  19. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/API/apidocs.yml +0 -0
  20. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/API/auth.py +0 -0
  21. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/API/constants.py +0 -0
  22. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/API/download_paths.py +0 -0
  23. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/API/download_slots.py +0 -0
  24. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/API/install/worker.ps1 +0 -0
  25. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/API/install/worker.sh +0 -0
  26. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/API/listcache.py +0 -0
  27. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/API/routes/__init__.py +0 -0
  28. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/API/routes/collections.py +0 -0
  29. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/API/routes/data.py +0 -0
  30. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/API/routes/downloads.py +0 -0
  31. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/API/routes/metadata.py +0 -0
  32. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/API/routes/operations.py +0 -0
  33. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/API/routes/schedules.py +0 -0
  34. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/API/routes/sqlite.py +0 -0
  35. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/API/routes/system.py +0 -0
  36. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/API/routes/tables.py +0 -0
  37. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/API/routes/timeseries.py +0 -0
  38. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/API/routes/web_routes.py +0 -0
  39. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/API/routes/workers.py +0 -0
  40. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/API/utils.py +0 -0
  41. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Auth/__init__.py +0 -0
  42. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Auth/bootstrap.py +0 -0
  43. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Auth/migrate.py +0 -0
  44. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Auth/store.py +0 -0
  45. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Auth/workers.py +0 -0
  46. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/CacheRedis.py +0 -0
  47. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/CollectionMongoDB.py +0 -0
  48. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/DataAPI/__init__.py +0 -0
  49. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/DataAPI/catalog.py +0 -0
  50. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/DataAPI/config.py +0 -0
  51. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/DataAPI/errors.py +0 -0
  52. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/DataAPI/indexes.py +0 -0
  53. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/DataAPI/readers.py +0 -0
  54. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/DataAPI/resolve.py +0 -0
  55. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/DataAPI/service.py +0 -0
  56. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Database.py +0 -0
  57. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Defaults.py +0 -0
  58. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/IO/AWSEC2.py +0 -0
  59. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/IO/AWSS3.py +0 -0
  60. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/IO/AutoDocstrings.py +0 -0
  61. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/IO/ClientAPI.py +0 -0
  62. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/IO/ClientSocket.py +0 -0
  63. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/IO/ClientWebSocket.py +0 -0
  64. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/IO/LogHandlerAPI.py +0 -0
  65. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/IO/LogReader.py +0 -0
  66. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/IO/MongoDBClient.py +0 -0
  67. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/IO/ServerSocket.py +0 -0
  68. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/IO/ServerWebSocket.py +0 -0
  69. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/IO/StreamsCache.py +0 -0
  70. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/IO/StreamsPersist.py +0 -0
  71. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/IO/SyncTable.py +0 -0
  72. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/IO/TunnelWebSocket.py +0 -0
  73. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/IO/__init__.py +0 -0
  74. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Listing.py +0 -0
  75. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/LockWaiters.py +0 -0
  76. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Logger.py +0 -0
  77. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/MCP/__init__.py +0 -0
  78. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/MCP/__main__.py +0 -0
  79. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/MCP/client.py +0 -0
  80. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/MCP/server.py +0 -0
  81. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Metadata.py +0 -0
  82. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/MultiProc.py +0 -0
  83. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/OpenFIGI.py +0 -0
  84. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Redaction.py +0 -0
  85. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Routines/ArchiveOldCollections.py +0 -0
  86. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Routines/BatchJob.py +0 -0
  87. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Routines/ContainerHealth.py +0 -0
  88. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Routines/JobStore.py +0 -0
  89. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Routines/RoutineCommand.py +0 -0
  90. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Routines/RoutineOrigin.py +0 -0
  91. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Routines/Schedule.py +0 -0
  92. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Routines/ScheduleMirror.py +0 -0
  93. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Routines/ScheduleMonitor.py +0 -0
  94. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Routines/ScheduleStore.py +0 -0
  95. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Routines/Scheduler.py +0 -0
  96. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Routines/UpdateActiveJobs.py +0 -0
  97. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Routines/Worker.py +0 -0
  98. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Routines/WorkerBootstrap.py +0 -0
  99. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Routines/WorkerLib.py +0 -0
  100. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Routines/WorkerPool.py +0 -0
  101. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Routines/__init__.py +0 -0
  102. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/SQLite.py +0 -0
  103. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/StreamKafka.py +0 -0
  104. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Symbol.py +0 -0
  105. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/TableDisk.py +0 -0
  106. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/TableIndex.py +0 -0
  107. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/TableIndexJitFunctions.py +0 -0
  108. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/TableIndexJitFunctionsManual.py +0 -0
  109. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/TableIndexJitGenerate.py +0 -0
  110. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/TableIndexJitHash.py +0 -0
  111. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/TableIndexJitLoc.py +0 -0
  112. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/TableProtocol.py +0 -0
  113. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/TableRead.py +0 -0
  114. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/TimeSeriesDisk.py +0 -0
  115. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/TimeseriesContainer.py +0 -0
  116. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Users.py +0 -0
  117. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Utils.py +0 -0
  118. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Web/ADDING_A_PAGE.md +0 -0
  119. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Web/__init__.py +0 -0
  120. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Web/activity_logger.py +0 -0
  121. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Web/app.py +0 -0
  122. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Web/assets/dashAgGridComponentFunctions.js +0 -0
  123. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Web/assets/dashAgGridFunctions.js +0 -0
  124. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Web/assets/google_logo.png +0 -0
  125. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Web/assets/kafka_keys.js +0 -0
  126. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Web/assets/logs.css +0 -0
  127. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Web/assets/sched.css +0 -0
  128. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Web/assets/styles.css +0 -0
  129. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Web/auth.py +0 -0
  130. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Web/backends.py +0 -0
  131. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Web/config.py +0 -0
  132. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Web/containers.py +0 -0
  133. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Web/identity.py +0 -0
  134. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Web/kafka_view.py +0 -0
  135. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Web/mongo_view.py +0 -0
  136. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Web/proc.py +0 -0
  137. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Web/redis_view.py +0 -0
  138. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Web/route_health.py +0 -0
  139. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Web/route_registry.py +0 -0
  140. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Web/sched.py +0 -0
  141. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Web/schedule_editor.py +0 -0
  142. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Web/server.py +0 -0
  143. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Web/services.py +0 -0
  144. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Web/shell.py +0 -0
  145. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Web/standalone.py +0 -0
  146. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Web/static/account.js +0 -0
  147. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Web/static/custom.css +0 -0
  148. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Web/static/favicon.ico +0 -0
  149. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Web/static/favicon.png +0 -0
  150. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Web/static/logo.png +0 -0
  151. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Web/static/logo_96x96.png +0 -0
  152. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Web/static/shareddata-mark.svg +0 -0
  153. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Web/static/shell.css +0 -0
  154. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Web/static/shell.js +0 -0
  155. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Web/static/standalone.css +0 -0
  156. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Web/static/standalone.js +0 -0
  157. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Web/suggest.py +0 -0
  158. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Web/templates/account.html +0 -0
  159. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Web/templates/base.html +0 -0
  160. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Web/templates/claim.html +0 -0
  161. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Web/templates/claim_done.html +0 -0
  162. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Web/templates/dash.html +0 -0
  163. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Web/templates/index.html +0 -0
  164. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Web/templates/login.html +0 -0
  165. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Web/templates/login_2fa.html +0 -0
  166. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Web/templates/partials/navbar.html +0 -0
  167. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Web/templates/partials/scripts.html +0 -0
  168. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Web/templates/recovery_codes.html +0 -0
  169. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Web/templates/shell.html +0 -0
  170. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Web/templates/standalone.html +0 -0
  171. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Web/twofactor.py +0 -0
  172. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Web/utils.py +0 -0
  173. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/Web/workerpool.py +0 -0
  174. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/__init__.py +0 -0
  175. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/build_info.py +0 -0
  176. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/fleet.py +0 -0
  177. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/preflight.py +0 -0
  178. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/private_files.py +0 -0
  179. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/release_backup.py +0 -0
  180. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/server.py +0 -0
  181. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/services.py +0 -0
  182. {shareddata-6.85.7 → shareddata-6.85.8}/src/SharedData/sharedmutexwin.pyd +0 -0
  183. {shareddata-6.85.7 → shareddata-6.85.8}/src/shareddata.egg-info/SOURCES.txt +0 -0
  184. {shareddata-6.85.7 → shareddata-6.85.8}/src/shareddata.egg-info/dependency_links.txt +0 -0
  185. {shareddata-6.85.7 → shareddata-6.85.8}/src/shareddata.egg-info/entry_points.txt +0 -0
  186. {shareddata-6.85.7 → shareddata-6.85.8}/src/shareddata.egg-info/requires.txt +0 -0
  187. {shareddata-6.85.7 → shareddata-6.85.8}/src/shareddata.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: shareddata
3
- Version: 6.85.7
3
+ Version: 6.85.8
4
4
  Summary: Memory Mapped / Shared Memory Database with S3 repository
5
5
  Home-page: https://shareddata.net
6
6
  Author: shareddata
@@ -66,7 +66,7 @@ test_requires = [
66
66
 
67
67
  setup(
68
68
  name='shareddata',
69
- version='6.85.7',
69
+ version='6.85.8',
70
70
  description='Memory Mapped / Shared Memory Database with S3 repository',
71
71
  author='shareddata',
72
72
  author_email='admin@shareddata.net',
@@ -8,6 +8,9 @@ import datetime
8
8
  import json
9
9
  import time
10
10
 
11
+ import numpy as np
12
+ from bson.objectid import ObjectId
13
+
11
14
  from flask import Blueprint, Response, jsonify, request
12
15
 
13
16
  from SharedData.Logger import Logger
@@ -20,10 +23,17 @@ from SharedData.API.utils import (
20
23
 
21
24
 
22
25
  class _DatetimeEncoder(json.JSONEncoder):
23
- """JSON encoder that serializes datetime/date objects as ISO 8601 strings."""
26
+ """JSON encoder for cached values: datetime/date as ISO 8601 strings, a Mongo ObjectId as its hex
27
+ string, NumPy scalars and arrays as plain numbers and lists."""
24
28
  def default(self, obj):
25
29
  if isinstance(obj, (datetime.datetime, datetime.date)):
26
30
  return obj.isoformat()
31
+ if isinstance(obj, ObjectId):
32
+ return str(obj)
33
+ if isinstance(obj, np.generic):
34
+ return obj.item()
35
+ if isinstance(obj, np.ndarray):
36
+ return obj.tolist()
27
37
  return super().default(obj)
28
38
 
29
39
 
@@ -55,7 +55,7 @@ def write_table(dftable_row: Dict[str, Any]) -> str:
55
55
  dftable_row['source'],
56
56
  tablename
57
57
  )
58
- tbl.write(force_write=True)
58
+ tbl.write(force_write=True, strict=True)
59
59
  return f"Success: {tablename}"
60
60
  except Exception as e:
61
61
  Logger.log.error(f"Error saving {dftable_row} {e}")
@@ -253,7 +253,11 @@ class SharedData:
253
253
  elif len(partitioningstr) == 8:
254
254
  partitioning = 'daily'
255
255
  else:
256
- if hasindex:
256
+ # A new indexed table of a period shorter than a day must be partitioned. Opening an existing
257
+ # one (no schema given) is not creating: legacy unpartitioned tables stay readable and savable.
258
+ creating = (names is not None or formats is not None or value is not None
259
+ or is_schemaless or size is not None)
260
+ if hasindex and creating:
257
261
  if period != 'D1':
258
262
  raise Exception(f'Please specify partitioning for {period} tables')
259
263
 
@@ -285,7 +285,7 @@ class SharedNumpy(np.ndarray):
285
285
  """
286
286
  self.table.trim()
287
287
 
288
- def write(self,force_write=False):
288
+ def write(self,force_write=False,strict=False):
289
289
  """
290
290
  Writes the table data to storage, optionally forcing the write operation.
291
291
 
@@ -300,7 +300,7 @@ class SharedNumpy(np.ndarray):
300
300
  if (self.table.type != 1 or sys.platform == 'linux') and (
301
301
  self.table.hdr['hasindex'] == 1 and self.table.hdr['isidxsorted'] == 0):
302
302
  self.sort_index()
303
- self.table.write(force_write)
303
+ self.table.write(force_write, strict)
304
304
 
305
305
  def rememmap(self):
306
306
  """
@@ -34,6 +34,12 @@ from SharedData.TableProtocol import (IDLE, WRITE, STRUCT, DIRTY, mode, epoch,
34
34
  writer_lock, take_thread_lock, structure, structural)
35
35
 
36
36
 
37
+ # A BSON side file is compacted before every upload that has something to return: a table with nothing
38
+ # orphaned (append-only) has nothing to defragment, and rewriting it would only copy identical bytes.
39
+ BSON_DEFRAG_MIN_BYTES = 64 * 1024
40
+ BSON_DEFRAG_MIN_FRACTION = 0.0
41
+
42
+
37
43
  class TableNotFound(FileNotFoundError):
38
44
  """The requested table is absent locally and remotely, with no creation schema."""
39
45
  def __init__(self, table):
@@ -1373,6 +1379,23 @@ class Table:
1373
1379
  return h1[:l] == h2[:l]
1374
1380
 
1375
1381
  ############### BSON MANAGEMENT ###############
1382
+ def _reset_bson_mapping(self):
1383
+ """Drop the BSON mapping and reopen the file by path: after a paired publication the old
1384
+ mapping belongs to an unlinked inode and its offsets no longer match the records."""
1385
+ if self.bson_mmap is not None:
1386
+ try:
1387
+ self.bson_mmap.close()
1388
+ except (BufferError, ValueError):
1389
+ pass
1390
+ self.bson_mmap = None
1391
+ if self.bson_file_handle is not None:
1392
+ self.bson_file_handle.close()
1393
+ self.bson_file_handle = None
1394
+ self.curr_bson_size = 0
1395
+ self.prev_bson_ptr = 0
1396
+ if self.is_schemaless and self.bsonpath.exists() and self.bsonpath.stat().st_size > 0:
1397
+ self.init_bson_mmap()
1398
+
1376
1399
  def init_bson_mmap(self, from_download=False):
1377
1400
  """Initialize or reopen memory-mapped BSON file with proper sizing.
1378
1401
 
@@ -1572,6 +1595,155 @@ class Table:
1572
1595
  data = bytes(self.bson_mmap[offset:offset + size])
1573
1596
  return bson.decode(data)
1574
1597
 
1598
+ def _bson_columns(self, count):
1599
+ """bson_ptr and bson_size of the committed rows as int64 (legacy tables declare them '|V8')."""
1600
+ pointers = self.records['bson_ptr'][:count]
1601
+ sizes = self.records['bson_size'][:count]
1602
+ if sizes.dtype.kind == 'V' and sizes.dtype.itemsize == 8:
1603
+ sizes = np.ascontiguousarray(sizes).view(np.int64)
1604
+ if pointers.dtype.kind == 'V' and pointers.dtype.itemsize == 8:
1605
+ pointers = np.ascontiguousarray(pointers).view(np.int64)
1606
+ return np.asarray(pointers, dtype=np.int64), np.asarray(sizes, dtype=np.int64)
1607
+
1608
+ def bson_usage(self):
1609
+ """What the BSON file holds: bytes referenced by live rows, the span allocated, the file size."""
1610
+ count = int(self.hdr['count'])
1611
+ file_size = self.bsonpath.stat().st_size if self.bsonpath.exists() else 0
1612
+ usage = dict(file_size=file_size, used=0, live=0, orphaned=0, live_rows=0, count=count)
1613
+ if count == 0:
1614
+ usage['used'] = usage['orphaned'] = int(self.hdr['next_bson_ptr'])
1615
+ return usage
1616
+ pointers, sizes = self._bson_columns(count)
1617
+ live = sizes > 0
1618
+ usage['live_rows'] = int(live.sum())
1619
+ usage['used'] = int(self.hdr['next_bson_ptr']) # a cursor past cleared documents is orphaned space
1620
+ if usage['live_rows']:
1621
+ usage['live'] = int(sizes[live].sum())
1622
+ usage['used'] = max(int((pointers[live] + sizes[live]).max()), usage['used'])
1623
+ usage['orphaned'] = max(usage['used'] - usage['live'], 0)
1624
+ return usage
1625
+
1626
+ def compact_bson_if_worthwhile(self):
1627
+ """Compact the BSON file when enough of it is orphaned; None when it is not worth a rewrite."""
1628
+ usage = self.bson_usage()
1629
+ if usage['orphaned'] < BSON_DEFRAG_MIN_BYTES \
1630
+ or usage['orphaned'] < BSON_DEFRAG_MIN_FRACTION * max(usage['used'], 1):
1631
+ return None
1632
+ return self.defragment_bson(acquire=False)
1633
+
1634
+ def _defragment_bson_disk(self, chunk_size, acquire):
1635
+ """Compact bson.bin of a disk table and move every bson_ptr, as ONE crash-safe publication.
1636
+
1637
+ The compacted BSON and the record image with the new pointers are built beside the live files
1638
+ (nothing live is written), then published as a pair by TableRewrite.publish_rewrite: BSON first,
1639
+ records second, with a version 2 descriptor that recovery rolls forward. Other processes reopen
1640
+ both files when the epoch published on leaving STRUCT reaches them (_refresh_mapping).
1641
+ """
1642
+ import mmap
1643
+ import hashlib
1644
+ from SharedData.TableRewrite import (RewriteError, copy_metadata, file_identity, fsync_directory,
1645
+ pending_path, publish_rewrite, require_rewrite_support, write_all)
1646
+ from SharedData.TableProtocol import structure
1647
+ require_rewrite_support()
1648
+ if not self.is_schemaless:
1649
+ raise Exception(f'Cannot defragment non-schemaless table {self.relpath}')
1650
+ if acquire:
1651
+ self.acquire()
1652
+ stage = None
1653
+ token = None
1654
+ try:
1655
+ self._sync_held_mapping()
1656
+ # The old BSON is the (old, old) recovery baseline: make its recent writes durable before any
1657
+ # replacement exists, or a power loss could keep durable old records over lost BSON bytes.
1658
+ if self.bson_mmap is not None:
1659
+ self.bson_mmap.flush()
1660
+ if self.bsonpath.exists():
1661
+ with open(self.bsonpath, 'rb') as baseline:
1662
+ os.fsync(baseline.fileno())
1663
+ count = int(self.hdr['count'])
1664
+ pointers, sizes = self._bson_columns(count) if count else (np.zeros(0, np.int64), np.zeros(0, np.int64))
1665
+ live_idx = np.flatnonzero(sizes > 0)
1666
+ if not self.bsonpath.exists():
1667
+ if len(live_idx):
1668
+ raise RewriteError(f'{self.relpath}: records reference a BSON file that does not exist')
1669
+ Logger.log.warning(f'No BSON file to defragment for {self.relpath}')
1670
+ return {'original_size': 0, 'new_size': 0, 'reclaimed': 0}
1671
+ original_size = self.bsonpath.stat().st_size
1672
+ if len(live_idx) and (np.any(pointers[live_idx] < 0) or np.any(pointers[live_idx] + sizes[live_idx] > original_size)):
1673
+ raise RewriteError(f'{self.relpath}: a record points outside its BSON file; nothing was changed')
1674
+ new_offsets = np.zeros(len(live_idx), dtype=np.int64)
1675
+ if len(live_idx):
1676
+ new_offsets[1:] = np.cumsum(sizes[live_idx])[:-1]
1677
+ payload = int(sizes[live_idx].sum()) if len(live_idx) else 0
1678
+ token = uuid.uuid4().hex
1679
+ stage = self.bsonpath.parent / ('bson.bin.stage.' + token)
1680
+ digest = hashlib.sha256()
1681
+ fd = os.open(stage, os.O_RDWR | os.O_CREAT | os.O_EXCL, 0o600)
1682
+ try:
1683
+ copy_metadata(self.bsonpath, fd)
1684
+ if len(live_idx):
1685
+ with open(self.bsonpath, 'rb') as source:
1686
+ view = mmap.mmap(source.fileno(), 0, access=mmap.ACCESS_READ)
1687
+ try:
1688
+ message = f'Defragmenting BSON file: {self.relpath}'
1689
+ for first in tqdm(range(0, len(live_idx), chunk_size), desc=message, unit='chunk'):
1690
+ rows = live_idx[first:first + chunk_size]
1691
+ buffer = bytearray(int(sizes[rows].sum()))
1692
+ cursor = 0
1693
+ for pointer, size in zip(pointers[rows].tolist(), sizes[rows].tolist()):
1694
+ buffer[cursor:cursor + size] = view[pointer:pointer + size]
1695
+ cursor += size
1696
+ digest.update(buffer)
1697
+ write_all(fd, buffer)
1698
+ finally:
1699
+ view.close()
1700
+ if payload == 0: # keep the file mappable: a physical zero-length file is refused
1701
+ digest.update(b'\0')
1702
+ write_all(fd, b'\0')
1703
+ os.fsync(fd)
1704
+ finally:
1705
+ os.close(fd)
1706
+ fsync_directory(stage.parent)
1707
+ new_size = max(payload, 1)
1708
+ mapping = np.full(count, -1, dtype=np.int64)
1709
+ mapping[live_idx] = new_offsets
1710
+
1711
+ def move_pointers(chunk, first):
1712
+ moved = mapping[first:first + len(chunk)]
1713
+ selected = moved >= 0
1714
+ if selected.any():
1715
+ column = chunk['bson_ptr']
1716
+ if column.dtype.kind == 'V' and column.dtype.itemsize == 8:
1717
+ column = column.view(np.int64)
1718
+ column[selected] = moved[selected]
1719
+ return chunk
1720
+
1721
+ with structure(self):
1722
+ publish_rewrite(self, None, 'defrag', start=0, transform=move_pointers,
1723
+ header_updates={'next_bson_ptr': payload, 'mtime': max(datetime.now().timestamp(), float(self.hdr['mtime']) + 0.001)},
1724
+ bson=dict(token=token, stage=stage, sha256=digest.hexdigest(), size=new_size))
1725
+ stats = {'original_size': original_size, 'new_size': new_size,
1726
+ 'reclaimed': original_size - new_size, 'live_bytes': payload, 'live_rows': int(len(live_idx))}
1727
+ Logger.log.info(
1728
+ f'Defragmented {self.relpath} BSON file: {original_size/1024/1024:.2f}MB -> '
1729
+ f'{new_size/1024/1024:.2f}MB (reclaimed {stats["reclaimed"]/1024/1024:.2f}MB)')
1730
+ return stats
1731
+ except Exception as e:
1732
+ Logger.log.error(f'Failed to defragment BSON file for {self.relpath}: {e}')
1733
+ # With a pending descriptor the stages belong to recovery; without one nothing refers to them.
1734
+ if token is not None and not pending_path(self).exists():
1735
+ for leftover in (self.bsonpath.parent / ('bson.bin.stage.' + token),
1736
+ Path(str(self.filepath) + '.stage.' + token),
1737
+ Path(str(self.filepath) + '.rewrite.prepare.' + token)):
1738
+ try:
1739
+ leftover.unlink()
1740
+ except OSError:
1741
+ pass
1742
+ raise
1743
+ finally:
1744
+ if acquire:
1745
+ self.release()
1746
+
1575
1747
  def defragment_bson(self, chunk_size=100000, buffer_mb=512, acquire=True):
1576
1748
  """
1577
1749
  Defragments the BSON file IN-PLACE by creating a compacted copy, then copying it back to the original file.
@@ -1603,8 +1775,7 @@ class Table:
1603
1775
  Exception: If the table is not schemaless or if defragmentation fails.
1604
1776
  """
1605
1777
  if self.type == 1:
1606
- from SharedData.TableRewrite import RewriteError
1607
- raise RewriteError('disk BSON maintenance requires coordinated staged publication')
1778
+ return self._defragment_bson_disk(chunk_size, acquire)
1608
1779
  if not self.is_schemaless:
1609
1780
  raise Exception(f'Cannot defragment non-schemaless table {self.relpath}')
1610
1781
 
@@ -1795,7 +1966,7 @@ class Table:
1795
1966
  self.release()
1796
1967
 
1797
1968
  ############### WRITE ###############
1798
- def write(self, force_write=False):
1969
+ def write(self, force_write=False, strict=False):
1799
1970
  """
1800
1971
  Writes data to a file and uploads it concurrently using separate threads.
1801
1972
 
@@ -1811,19 +1982,41 @@ class Table:
1811
1982
  7. Handles exceptions by logging an error message and re-raising the exception.
1812
1983
  8. Releases the lock regardless of success or failure.
1813
1984
 
1985
+ A schemaless table whose BSON file is about to be uploaded is compacted first, in this thread
1986
+ and under this lock (the compaction is a STRUCT publication, so it cannot run in the upload
1987
+ thread), and before the header, checksums and timestamp are prepared, so that the uploaded head
1988
+ and tail carry the moved pointers.
1989
+
1814
1990
  Parameters:
1815
1991
  force_write (bool): If True, forces the header to be written even if not modified.
1992
+ strict (bool): If True an upload or flush failure raises. It is always logged with the table name.
1816
1993
 
1817
1994
  Raises:
1818
- Exception: If the write or upload operation fails.
1995
+ Exception: If the write fails (or, with strict, if the upload or the local flush fails).
1819
1996
  """
1820
1997
  errmsg = ''
1821
1998
  self.acquire()
1822
1999
  try:
1823
2000
 
1824
2001
  tini = time.time()
2002
+ from SharedData.services import s3_enabled
2003
+ remote = s3_enabled()
1825
2004
  # create header
1826
2005
  mtime = self.hdr['mtime']
2006
+ compacted = False
2007
+ if remote and self.is_schemaless and self.type == 1 and self.bsonpath.exists():
2008
+ remote_bson_mtime = S3GetMtime(str(self.bsonpath) + '.gzip', self.database_folder)
2009
+ if force_write or remote_bson_mtime is None or remote_bson_mtime < mtime:
2010
+ if self.compact_bson_if_worthwhile() is not None:
2011
+ compacted = True
2012
+ # New pointers: head, tail and BSON are all rewritten and all uploaded, under a
2013
+ # publication time later than anything the remote holds, so consumers see the change.
2014
+ newest = max([m for m in (remote_bson_mtime,
2015
+ S3GetMtime(str(self.headpath) + '.gzip', self.database_folder),
2016
+ S3GetMtime(str(self.tailpath) + '.gzip', self.database_folder))
2017
+ if m is not None] + [float(self.hdr['mtime'])])
2018
+ self.hdr['mtime'] = max(float(self.hdr['mtime']), newest + 0.001)
2019
+ mtime = self.hdr['mtime']
1827
2020
  write_head = False
1828
2021
  write_head = self.fill_header(force_write)
1829
2022
 
@@ -1833,21 +2026,32 @@ class Table:
1833
2026
  if self.bson_file_handle is not None:
1834
2027
  os.fsync(self.bson_file_handle.fileno())
1835
2028
 
1836
- from SharedData.services import s3_enabled
1837
- if not s3_enabled():
2029
+ if not remote:
1838
2030
  self.write_file()
1839
2031
  return
1840
2032
 
2033
+ failures = []
2034
+
2035
+ def guarded(label, target, *args):
2036
+ # A thread's exception never reaches join(): keep it, name the table, let the caller decide.
2037
+ try:
2038
+ target(*args)
2039
+ except BaseException as error:
2040
+ failures.append('%s: %s' % (label, error))
2041
+ Logger.log.error('%s %s failed: %s' % (self.relpath, label, error))
2042
+
1841
2043
  thread_s3 = threading.Thread(
1842
- target=self.upload, args=(write_head, mtime, force_write))
2044
+ target=guarded, args=('upload', self.upload, write_head, mtime, force_write or compacted, compacted))
1843
2045
  thread_s3.start()
1844
2046
 
1845
- thread_flush = threading.Thread(target=self.write_file)
2047
+ thread_flush = threading.Thread(target=guarded, args=('flush', self.write_file))
1846
2048
  thread_flush.start()
1847
2049
 
1848
2050
  # join threads
1849
2051
  thread_s3.join()
1850
2052
  thread_flush.join()
2053
+ if failures and strict:
2054
+ raise Exception('; '.join(failures))
1851
2055
 
1852
2056
  te = time.time() - tini
1853
2057
  datasize = self.hdr['count'] * self.hdr['itemsize'] / 1000000
@@ -2030,33 +2234,14 @@ class Table:
2030
2234
  return write_head
2031
2235
 
2032
2236
  ############### UPLOAD ###############
2033
- def upload(self, write_head, mtime, force_write=False):
2237
+ def upload(self, write_head, mtime, force_write=False, force_tail=False):
2034
2238
  """
2035
2239
  Uploads the head, tail, and BSON files to remote storage based on modification times and flags.
2036
2240
 
2037
- For schemaless tables, defragments the BSON file FIRST to ensure that the uploaded head/tail
2038
- contain the correct updated pointers for the defragmented BSON file.
2241
+ For schemaless tables the BSON file is uploaded after head and tail. Compaction is not done here:
2242
+ it is a structural publication and runs in write(), before the header is prepared.
2039
2243
  """
2040
- # DEFRAGMENT BSON FIRST (before uploading head/tail) for schemaless tables
2041
- # This ensures the pointers in records are updated before we upload them
2042
- if self.is_schemaless and self.bsonpath.exists():
2043
- bson_gzip_path = str(self.bsonpath) + '.gzip'
2044
- remote_bson_mtime = S3GetMtime(bson_gzip_path, self.database_folder)
2045
- remote_bson_is_updated = False
2046
- if remote_bson_mtime is not None:
2047
- remote_bson_is_updated = remote_bson_mtime >= mtime
2048
-
2049
- # Defragment if BSON needs upload (this updates pointers in records)
2050
- if not remote_bson_is_updated or force_write:
2051
- # Pass acquire=False because we're already in write() context with lock held
2052
- stats = self.defragment_bson(acquire=False)
2053
-
2054
- # Mark header as modified to force head/tail upload with new pointers
2055
- write_head = True
2056
- self.hdr['mtimehead'] = mtime
2057
- self.hdr['mtimetail'] = mtime
2058
-
2059
- # Now upload head/tail with UPDATED pointers
2244
+ # head/tail carry the pointers of the BSON uploaded below (compacted by write() when it was worth it)
2060
2245
  remote_head_mtime = S3GetMtime(str(self.headpath)+'.gzip', self.database_folder)
2061
2246
  remote_head_is_updated = False
2062
2247
  if not remote_head_mtime is None:
@@ -2070,7 +2255,7 @@ class Table:
2070
2255
  remote_tail_is_updated = False
2071
2256
  if not remote_tail_mtime is None:
2072
2257
  remote_tail_is_updated = remote_tail_mtime >= self.hdr['mtimetail']
2073
- if not remote_tail_is_updated:
2258
+ if force_tail or not remote_tail_is_updated:
2074
2259
  self.upload_tail(mtime)
2075
2260
  else:
2076
2261
  self.delete_tail()
@@ -2741,6 +2926,8 @@ class Table:
2741
2926
  if (dirty or self._mapping_epoch != current
2742
2927
  or self.size != int(self.hdr['recordssize'])):
2743
2928
  self.attach()
2929
+ if getattr(self, 'is_schemaless', False):
2930
+ self._reset_bson_mapping() # the BSON file is published together with data.bin
2744
2931
  if self.hasindex and not dirty:
2745
2932
  self.index.rememmap()
2746
2933
  self._mapping_epoch = current
@@ -134,6 +134,22 @@ def image(path):
134
134
  sha256=digest.hexdigest())
135
135
 
136
136
 
137
+ def file_identity(path):
138
+ """Cheap identity of a side file (BSON): where it lives and how big it is. Renames keep the inode."""
139
+ info = os.stat(path)
140
+ return dict(dev=info.st_dev, ino=info.st_ino, size=info.st_size)
141
+
142
+
143
+ def file_sha256(path):
144
+ digest = hashlib.sha256()
145
+ with open(path, 'rb') as stream:
146
+ while True:
147
+ chunk = stream.read(1 << 20)
148
+ if not chunk:
149
+ return digest.hexdigest()
150
+ digest.update(chunk)
151
+
152
+
137
153
  def canonical(value):
138
154
  return json.dumps(value, sort_keys=True, separators=(',', ':')).encode()
139
155
 
@@ -151,13 +167,24 @@ def _load(table):
151
167
  descriptor = envelope['descriptor']
152
168
  if hashlib.sha256(canonical(descriptor)).hexdigest() != envelope['sha256']:
153
169
  raise ValueError('checksum mismatch')
154
- if descriptor['version'] != 1 or descriptor['magic'] != 'SharedData-record-rewrite':
170
+ if descriptor['version'] not in (1, 2) or descriptor['magic'] != 'SharedData-record-rewrite':
155
171
  raise ValueError('unsupported descriptor')
156
172
  if descriptor['table'] != str(table.relpath).replace('\\', '/'):
157
173
  raise ValueError('wrong table')
158
174
  token = uuid.UUID(hex=descriptor['transaction']).hex
159
175
  if descriptor['stage'] != 'data.bin.stage.' + token:
160
176
  raise ValueError('invalid stage name')
177
+ if descriptor['version'] == 2: # a record image published together with its BSON side file
178
+ bson = descriptor['bson']
179
+ if bson['stage'] != 'bson.bin.stage.' + token:
180
+ raise ValueError('invalid BSON stage name')
181
+ for key in ('old', 'new'):
182
+ if not all(isinstance(bson[key][field], int) for field in ('dev', 'ino', 'size')):
183
+ raise ValueError('invalid BSON identity')
184
+ if not isinstance(bson['new']['sha256'], str) or len(bson['new']['sha256']) != 64:
185
+ raise ValueError('invalid BSON digest')
186
+ elif 'bson' in descriptor:
187
+ raise ValueError('BSON section requires version 2')
161
188
  return descriptor
162
189
  except Exception as error:
163
190
  raise RewriteError('invalid pending record rewrite; files preserved') from error
@@ -173,6 +200,8 @@ def _finish(table, descriptor, recovery=False):
173
200
  table.hasindex = bool(header['hasindex'])
174
201
  table.attach()
175
202
  table._index_bundle = None
203
+ if 'bson' in descriptor:
204
+ table._reset_bson_mapping() # the BSON file was replaced together with the records
176
205
  checkpoint(prefix + 'data_attached')
177
206
  table.hdr['isidxcreated'] = 0
178
207
  sync_header(table)
@@ -188,6 +217,13 @@ def _finish(table, descriptor, recovery=False):
188
217
  if image(stage) != descriptor['new']:
189
218
  raise RewriteError('pending stage identity mismatch; files preserved')
190
219
  stage.unlink()
220
+ if 'bson' in descriptor:
221
+ bson_stage = Path(table.filepath).parent / descriptor['bson']['stage']
222
+ if bson_stage.exists():
223
+ if file_identity(bson_stage)['size'] != descriptor['bson']['new']['size'] \
224
+ or file_sha256(bson_stage) != descriptor['bson']['new']['sha256']:
225
+ raise RewriteError('pending BSON stage identity mismatch; files preserved')
226
+ bson_stage.unlink()
191
227
  pending_path(table).unlink()
192
228
  checkpoint(prefix + 'descriptor_unlinked')
193
229
  fsync_directory(Path(table.filepath).parent)
@@ -195,10 +231,44 @@ def _finish(table, descriptor, recovery=False):
195
231
  table._rewrite_epoch_pending = True
196
232
 
197
233
 
234
+ def _bson_state(table, descriptor):
235
+ """'old' or 'new' for the installed BSON file; anything else is refused, never guessed."""
236
+ bson = descriptor['bson']
237
+ try:
238
+ installed = file_identity(Path(table.filepath).parent / 'bson.bin')
239
+ except OSError as error:
240
+ raise RewriteError('missing authoritative BSON file; files preserved') from error
241
+ if (installed['dev'], installed['ino'], installed['size']) == (bson['old']['dev'], bson['old']['ino'], bson['old']['size']):
242
+ return 'old'
243
+ if installed['size'] == bson['new']['size'] and installed['ino'] == bson['new']['ino'] \
244
+ and file_sha256(Path(table.filepath).parent / 'bson.bin') == bson['new']['sha256']:
245
+ return 'new'
246
+ raise RewriteError('unknown authoritative BSON file; files preserved')
247
+
248
+
249
+ def _roll_forward_pair(table, descriptor, actual, stage):
250
+ """Records and BSON are published as a pair: BSON first, then the records. A crash in between leaves
251
+ the new BSON next to the old records; the complete, validated stages are still there, so finish it.
252
+ (old, old): nothing was installed, the stages are discarded by _finish. (new, new): only cleanup is left.
253
+ (new records, old BSON) cannot arise from this order and is refused."""
254
+ folder = Path(table.filepath).parent
255
+ records = 'old' if actual == descriptor['old'] else 'new'
256
+ bson = _bson_state(table, descriptor)
257
+ if (records, bson) == ('new', 'old'):
258
+ raise RewriteError('unknown record/BSON pair state; files preserved')
259
+ if (records, bson) == ('old', 'new'):
260
+ if not stage.exists() or image(stage) != descriptor['new']:
261
+ raise RewriteError('missing record stage for the installed BSON file; files preserved')
262
+ checkpoint('recovery_pair_roll_forward')
263
+ os.replace(stage, table.filepath)
264
+ fsync_directory(folder)
265
+ checkpoint('recovery_pair_data_replaced')
266
+
267
+
198
268
  def recover_rewrite(table):
199
269
  """Resolve old/new identity under pid lock, even after loss of all shm."""
200
- if table.type != 1 or not pending_path(table).exists():
201
- return False
270
+ if table.type != 1 or getattr(table, 'filepath', None) is None or not pending_path(table).exists():
271
+ return False # no path yet: __init__ failed before naming the file (free() acquires such a table)
202
272
  require_rewrite_support()
203
273
  if not table._owns_lock():
204
274
  raise RewriteError('record recovery requires writer ownership')
@@ -214,6 +284,8 @@ def recover_rewrite(table):
214
284
  stage = Path(table.filepath).parent / descriptor['stage']
215
285
  if stage.exists() and image(stage) != descriptor['new']:
216
286
  raise RewriteError('pending stage identity mismatch; files preserved')
287
+ if 'bson' in descriptor:
288
+ _roll_forward_pair(table, descriptor, actual, stage)
217
289
  checkpoint('recovery_validated')
218
290
  table._rewrite_recovering = True
219
291
  try:
@@ -223,11 +295,17 @@ def recover_rewrite(table):
223
295
  return True
224
296
 
225
297
 
226
- def publish_rewrite(table, order, operation, *, capacity=None, start=0, sorted_flag=None):
298
+ def publish_rewrite(table, order, operation, *, capacity=None, start=0, sorted_flag=None,
299
+ transform=None, header_updates=None, bson=None):
227
300
  """Copy selected committed rows to a complete file, then atomically publish.
228
301
 
229
302
  `order` is a source-index vector, or None for the unchanged committed rows.
230
303
  No record writes target the live mmap. Exceptions retain pending state.
304
+
305
+ transform(chunk, first_row) may edit a private copy of each chunk (for example to move BSON pointers),
306
+ header_updates is a dict of header fields for the replacement. `bson` = dict(token, stage, sha256, size)
307
+ names a complete, synced BSON replacement built beside bson.bin: both files are then published as a pair
308
+ (BSON first, records second, a version 2 descriptor makes the pair recoverable).
231
309
  """
232
310
  require_rewrite_support()
233
311
  if table.type != 1 or not table._owns_lock():
@@ -265,7 +343,9 @@ def publish_rewrite(table, order, operation, *, capacity=None, start=0, sorted_f
265
343
  if sorted_flag is not None:
266
344
  header['isidxsorted'] = sorted_flag
267
345
  header['minchgid'] = min(int(header['minchgid']), start)
268
- token = uuid.uuid4().hex
346
+ token = bson['token'] if bson else uuid.uuid4().hex
347
+ for field, value in (header_updates or {}).items():
348
+ header[field] = value
269
349
  stage = Path(str(table.filepath) + '.stage.' + token)
270
350
  prepare = Path(str(table.filepath) + '.rewrite.prepare.' + token)
271
351
  source_stat = os.stat(table.filepath)
@@ -287,6 +367,8 @@ def publish_rewrite(table, order, operation, *, capacity=None, start=0, sorted_f
287
367
  for offset in range(0, count, chunk_size):
288
368
  end = min(count, offset + chunk_size)
289
369
  chunk = rows[offset:end] if order is None else rows[order[offset:end]]
370
+ if transform is not None:
371
+ chunk = transform(np.array(chunk, copy=True), offset)
290
372
  output = chunk.tobytes()
291
373
  expected_hash.update(output)
292
374
  write_all(fd, output)
@@ -302,10 +384,16 @@ def publish_rewrite(table, order, operation, *, capacity=None, start=0, sorted_f
302
384
  if (new['sha256'] != expected_hash.hexdigest() or new['count'] != count
303
385
  or new['capacity'] != capacity or image(table.filepath) != old):
304
386
  raise RewriteError('replacement validation failed; live records unchanged')
305
- descriptor = dict(magic='SharedData-record-rewrite', version=1,
387
+ descriptor = dict(magic='SharedData-record-rewrite', version=2 if bson else 1,
306
388
  transaction=token, table=str(table.relpath).replace('\\', '/'),
307
389
  operation=operation, start=int(start), stage=stage.name,
308
390
  old=old, new=new)
391
+ if bson:
392
+ bson_stage = Path(table.bsonpath).parent / ('bson.bin.stage.' + token)
393
+ if not bson_stage.is_file() or bson_stage.stat().st_size != bson['size']:
394
+ raise RewriteError('BSON stage missing or of the wrong size; live files unchanged')
395
+ descriptor['bson'] = dict(stage=bson_stage.name, old=file_identity(table.bsonpath),
396
+ new=dict(file_identity(bson_stage), sha256=bson['sha256']))
309
397
  envelope = dict(descriptor=descriptor, sha256=hashlib.sha256(canonical(descriptor)).hexdigest())
310
398
  fd = os.open(prepare, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
311
399
  try:
@@ -319,6 +407,11 @@ def publish_rewrite(table, order, operation, *, capacity=None, start=0, sorted_f
319
407
  checkpoint('prepare_installed')
320
408
  fsync_directory(stage.parent)
321
409
  checkpoint('prepare_dir_synced')
410
+ if bson:
411
+ os.replace(bson_stage, table.bsonpath)
412
+ checkpoint('bson_replaced')
413
+ fsync_directory(stage.parent)
414
+ checkpoint('bson_dir_synced')
322
415
  os.replace(stage, table.filepath)
323
416
  checkpoint('data_replaced')
324
417
  fsync_directory(stage.parent)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: shareddata
3
- Version: 6.85.7
3
+ Version: 6.85.8
4
4
  Summary: Memory Mapped / Shared Memory Database with S3 repository
5
5
  Home-page: https://shareddata.net
6
6
  Author: shareddata
File without changes
File without changes
File without changes
File without changes
File without changes