queryapigate 0.48.0__tar.gz → 0.49.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (245) hide show
  1. {queryapigate-0.48.0/queryapigate.egg-info → queryapigate-0.49.0}/PKG-INFO +1 -1
  2. {queryapigate-0.48.0 → queryapigate-0.49.0}/queryapigate/__init__.py +1 -1
  3. {queryapigate-0.48.0 → queryapigate-0.49.0}/queryapigate/experimental.py +2 -8
  4. {queryapigate-0.48.0 → queryapigate-0.49.0}/queryapigate/masking.py +4 -1
  5. {queryapigate-0.48.0 → queryapigate-0.49.0}/queryapigate/runstore.py +1 -1
  6. {queryapigate-0.48.0 → queryapigate-0.49.0/queryapigate.egg-info}/PKG-INFO +1 -1
  7. {queryapigate-0.48.0 → queryapigate-0.49.0}/tests/test_db.py +4 -1
  8. {queryapigate-0.48.0 → queryapigate-0.49.0}/tests/test_events_server.py +1 -0
  9. {queryapigate-0.48.0 → queryapigate-0.49.0}/tests/test_experimental.py +2 -1
  10. {queryapigate-0.48.0 → queryapigate-0.49.0}/tests/test_history.py +1 -0
  11. {queryapigate-0.48.0 → queryapigate-0.49.0}/tests/test_masking.py +11 -0
  12. {queryapigate-0.48.0 → queryapigate-0.49.0}/tests/test_rolling_upgrade.py +19 -2
  13. {queryapigate-0.48.0 → queryapigate-0.49.0}/tests/test_upgrades.py +98 -11
  14. {queryapigate-0.48.0 → queryapigate-0.49.0}/LICENSE +0 -0
  15. {queryapigate-0.48.0 → queryapigate-0.49.0}/NOTICE +0 -0
  16. {queryapigate-0.48.0 → queryapigate-0.49.0}/README.md +0 -0
  17. {queryapigate-0.48.0 → queryapigate-0.49.0}/pyproject.toml +0 -0
  18. {queryapigate-0.48.0 → queryapigate-0.49.0}/queryapigate/__main__.py +0 -0
  19. {queryapigate-0.48.0 → queryapigate-0.49.0}/queryapigate/accesslog.py +0 -0
  20. {queryapigate-0.48.0 → queryapigate-0.49.0}/queryapigate/adminroles.py +0 -0
  21. {queryapigate-0.48.0 → queryapigate-0.49.0}/queryapigate/admins.py +0 -0
  22. {queryapigate-0.48.0 → queryapigate-0.49.0}/queryapigate/agentwrites.py +0 -0
  23. {queryapigate-0.48.0 → queryapigate-0.49.0}/queryapigate/alerts.py +0 -0
  24. {queryapigate-0.48.0 → queryapigate-0.49.0}/queryapigate/apikeys.py +0 -0
  25. {queryapigate-0.48.0 → queryapigate-0.49.0}/queryapigate/app.py +0 -0
  26. {queryapigate-0.48.0 → queryapigate-0.49.0}/queryapigate/auditchain.py +0 -0
  27. {queryapigate-0.48.0 → queryapigate-0.49.0}/queryapigate/auditstream.py +0 -0
  28. {queryapigate-0.48.0 → queryapigate-0.49.0}/queryapigate/aws.py +0 -0
  29. {queryapigate-0.48.0 → queryapigate-0.49.0}/queryapigate/broadcast.py +0 -0
  30. {queryapigate-0.48.0 → queryapigate-0.49.0}/queryapigate/bundle.py +0 -0
  31. {queryapigate-0.48.0 → queryapigate-0.49.0}/queryapigate/cache.py +0 -0
  32. {queryapigate-0.48.0 → queryapigate-0.49.0}/queryapigate/cli.py +0 -0
  33. {queryapigate-0.48.0 → queryapigate-0.49.0}/queryapigate/collection_admin.py +0 -0
  34. {queryapigate-0.48.0 → queryapigate-0.49.0}/queryapigate/config.py +0 -0
  35. {queryapigate-0.48.0 → queryapigate-0.49.0}/queryapigate/configcode.py +0 -0
  36. {queryapigate-0.48.0 → queryapigate-0.49.0}/queryapigate/console.py +0 -0
  37. {queryapigate-0.48.0 → queryapigate-0.49.0}/queryapigate/console_dist/assets/AccessMapPage-CvmP9Sfw.js +0 -0
  38. {queryapigate-0.48.0 → queryapigate-0.49.0}/queryapigate/console_dist/assets/AdministratorsPage-B1G7Dgxv.js +0 -0
  39. {queryapigate-0.48.0 → queryapigate-0.49.0}/queryapigate/console_dist/assets/AgentsPage-ex_lhxAd.js +0 -0
  40. {queryapigate-0.48.0 → queryapigate-0.49.0}/queryapigate/console_dist/assets/AlertsPage-CkFfLPBu.js +0 -0
  41. {queryapigate-0.48.0 → queryapigate-0.49.0}/queryapigate/console_dist/assets/ApiKeyDetailPage-DluS3aag.js +0 -0
  42. {queryapigate-0.48.0 → queryapigate-0.49.0}/queryapigate/console_dist/assets/ApiKeysPage-D3xeaQlS.js +0 -0
  43. {queryapigate-0.48.0 → queryapigate-0.49.0}/queryapigate/console_dist/assets/ApprovalsPage-BYL5hXGT.js +0 -0
  44. {queryapigate-0.48.0 → queryapigate-0.49.0}/queryapigate/console_dist/assets/AuditLogPage-TAMz5LIn.js +0 -0
  45. {queryapigate-0.48.0 → queryapigate-0.49.0}/queryapigate/console_dist/assets/CachingPage-8OhOUgxW.js +0 -0
  46. {queryapigate-0.48.0 → queryapigate-0.49.0}/queryapigate/console_dist/assets/CodeBox-Cwh5KHLp.js +0 -0
  47. {queryapigate-0.48.0 → queryapigate-0.49.0}/queryapigate/console_dist/assets/ConnectionsPage-mHkn5I8c.js +0 -0
  48. {queryapigate-0.48.0 → queryapigate-0.49.0}/queryapigate/console_dist/assets/DesignerPage-CPZneqCQ.js +0 -0
  49. {queryapigate-0.48.0 → queryapigate-0.49.0}/queryapigate/console_dist/assets/DestinationsPage-Dy6NYnv3.js +0 -0
  50. {queryapigate-0.48.0 → queryapigate-0.49.0}/queryapigate/console_dist/assets/Experimental-B3dnaeV7.js +0 -0
  51. {queryapigate-0.48.0 → queryapigate-0.49.0}/queryapigate/console_dist/assets/ExportsPage-BzDOR2fh.js +0 -0
  52. {queryapigate-0.48.0 → queryapigate-0.49.0}/queryapigate/console_dist/assets/HelpPage-BV_YH8GV.js +0 -0
  53. {queryapigate-0.48.0 → queryapigate-0.49.0}/queryapigate/console_dist/assets/HomePage-BShpQIil.js +0 -0
  54. {queryapigate-0.48.0 → queryapigate-0.49.0}/queryapigate/console_dist/assets/KeyActivity-DuTyhSGT.js +0 -0
  55. {queryapigate-0.48.0 → queryapigate-0.49.0}/queryapigate/console_dist/assets/Managed-CQ0VXNNZ.js +0 -0
  56. {queryapigate-0.48.0 → queryapigate-0.49.0}/queryapigate/console_dist/assets/McpPage-JMpMLJIz.js +0 -0
  57. {queryapigate-0.48.0 → queryapigate-0.49.0}/queryapigate/console_dist/assets/MetricsPage-BOUZqgao.js +0 -0
  58. {queryapigate-0.48.0 → queryapigate-0.49.0}/queryapigate/console_dist/assets/QueryInfo-n4U1MkL1.js +0 -0
  59. {queryapigate-0.48.0 → queryapigate-0.49.0}/queryapigate/console_dist/assets/RepositoryPage-Cj_ZwMQj.js +0 -0
  60. {queryapigate-0.48.0 → queryapigate-0.49.0}/queryapigate/console_dist/assets/Results-6QW3JK2B.js +0 -0
  61. {queryapigate-0.48.0 → queryapigate-0.49.0}/queryapigate/console_dist/assets/RolesPage-Dq83u-am.js +0 -0
  62. {queryapigate-0.48.0 → queryapigate-0.49.0}/queryapigate/console_dist/assets/SchemaBrowser-BzZ6DZjO.js +0 -0
  63. {queryapigate-0.48.0 → queryapigate-0.49.0}/queryapigate/console_dist/assets/SettingsPage-CpjtKQ1P.js +0 -0
  64. {queryapigate-0.48.0 → queryapigate-0.49.0}/queryapigate/console_dist/assets/StatTile-Cft4kJPH.js +0 -0
  65. {queryapigate-0.48.0 → queryapigate-0.49.0}/queryapigate/console_dist/assets/Time-DurHvFXj.js +0 -0
  66. {queryapigate-0.48.0 → queryapigate-0.49.0}/queryapigate/console_dist/assets/WorkspacesPage-Bfe_wYYe.js +0 -0
  67. {queryapigate-0.48.0 → queryapigate-0.49.0}/queryapigate/console_dist/assets/api-CgyYL3VS.js +0 -0
  68. {queryapigate-0.48.0 → queryapigate-0.49.0}/queryapigate/console_dist/assets/client-ZHNiMG8H.js +0 -0
  69. {queryapigate-0.48.0 → queryapigate-0.49.0}/queryapigate/console_dist/assets/forms-DR_JyNks.js +0 -0
  70. {queryapigate-0.48.0 → queryapigate-0.49.0}/queryapigate/console_dist/assets/forms-DdFqq646.js +0 -0
  71. {queryapigate-0.48.0 → queryapigate-0.49.0}/queryapigate/console_dist/assets/icons-CFXE8hAz.js +0 -0
  72. {queryapigate-0.48.0 → queryapigate-0.49.0}/queryapigate/console_dist/assets/index-0v-rImZs.js +0 -0
  73. {queryapigate-0.48.0 → queryapigate-0.49.0}/queryapigate/console_dist/assets/index-Cv64XKNs.css +0 -0
  74. {queryapigate-0.48.0 → queryapigate-0.49.0}/queryapigate/console_dist/assets/maskRules-CvKQIPgN.js +0 -0
  75. {queryapigate-0.48.0 → queryapigate-0.49.0}/queryapigate/console_dist/assets/reach-BAOLd75Y.js +0 -0
  76. {queryapigate-0.48.0 → queryapigate-0.49.0}/queryapigate/console_dist/assets/shared-D8E1i8vU.js +0 -0
  77. {queryapigate-0.48.0 → queryapigate-0.49.0}/queryapigate/console_dist/assets/state-O13cl7cO.js +0 -0
  78. {queryapigate-0.48.0 → queryapigate-0.49.0}/queryapigate/console_dist/assets/useMutation-D4M562eQ.js +0 -0
  79. {queryapigate-0.48.0 → queryapigate-0.49.0}/queryapigate/console_dist/favicon.svg +0 -0
  80. {queryapigate-0.48.0 → queryapigate-0.49.0}/queryapigate/console_dist/index.html +0 -0
  81. {queryapigate-0.48.0 → queryapigate-0.49.0}/queryapigate/cors.py +0 -0
  82. {queryapigate-0.48.0 → queryapigate-0.49.0}/queryapigate/databases.py +0 -0
  83. {queryapigate-0.48.0 → queryapigate-0.49.0}/queryapigate/dataplane.py +0 -0
  84. {queryapigate-0.48.0 → queryapigate-0.49.0}/queryapigate/db.py +0 -0
  85. {queryapigate-0.48.0 → queryapigate-0.49.0}/queryapigate/definitions.py +0 -0
  86. {queryapigate-0.48.0 → queryapigate-0.49.0}/queryapigate/deprecations.py +0 -0
  87. {queryapigate-0.48.0 → queryapigate-0.49.0}/queryapigate/descriptions.py +0 -0
  88. {queryapigate-0.48.0 → queryapigate-0.49.0}/queryapigate/destinations.py +0 -0
  89. {queryapigate-0.48.0 → queryapigate-0.49.0}/queryapigate/duckfiles.py +0 -0
  90. {queryapigate-0.48.0 → queryapigate-0.49.0}/queryapigate/embeddings.py +0 -0
  91. {queryapigate-0.48.0 → queryapigate-0.49.0}/queryapigate/engine.py +0 -0
  92. {queryapigate-0.48.0 → queryapigate-0.49.0}/queryapigate/errors.py +0 -0
  93. {queryapigate-0.48.0 → queryapigate-0.49.0}/queryapigate/events.py +0 -0
  94. {queryapigate-0.48.0 → queryapigate-0.49.0}/queryapigate/examples.py +0 -0
  95. {queryapigate-0.48.0 → queryapigate-0.49.0}/queryapigate/exporting.py +0 -0
  96. {queryapigate-0.48.0 → queryapigate-0.49.0}/queryapigate/exports.py +0 -0
  97. {queryapigate-0.48.0 → queryapigate-0.49.0}/queryapigate/formats.py +0 -0
  98. {queryapigate-0.48.0 → queryapigate-0.49.0}/queryapigate/governance.py +0 -0
  99. {queryapigate-0.48.0 → queryapigate-0.49.0}/queryapigate/gunicorn_conf.py +0 -0
  100. {queryapigate-0.48.0 → queryapigate-0.49.0}/queryapigate/history.py +0 -0
  101. {queryapigate-0.48.0 → queryapigate-0.49.0}/queryapigate/instances.py +0 -0
  102. {queryapigate-0.48.0 → queryapigate-0.49.0}/queryapigate/jwtauth.py +0 -0
  103. {queryapigate-0.48.0 → queryapigate-0.49.0}/queryapigate/lib/h2-2.2.224.jar +0 -0
  104. {queryapigate-0.48.0 → queryapigate-0.49.0}/queryapigate/limits.py +0 -0
  105. {queryapigate-0.48.0 → queryapigate-0.49.0}/queryapigate/logging_setup.py +0 -0
  106. {queryapigate-0.48.0 → queryapigate-0.49.0}/queryapigate/mcp_server.py +0 -0
  107. {queryapigate-0.48.0 → queryapigate-0.49.0}/queryapigate/metacache.py +0 -0
  108. {queryapigate-0.48.0 → queryapigate-0.49.0}/queryapigate/metrics.py +0 -0
  109. {queryapigate-0.48.0 → queryapigate-0.49.0}/queryapigate/mongodocs.py +0 -0
  110. {queryapigate-0.48.0 → queryapigate-0.49.0}/queryapigate/mongotools.py +0 -0
  111. {queryapigate-0.48.0 → queryapigate-0.49.0}/queryapigate/oidc.py +0 -0
  112. {queryapigate-0.48.0 → queryapigate-0.49.0}/queryapigate/openapi.py +0 -0
  113. {queryapigate-0.48.0 → queryapigate-0.49.0}/queryapigate/params.py +0 -0
  114. {queryapigate-0.48.0 → queryapigate-0.49.0}/queryapigate/parquet.py +0 -0
  115. {queryapigate-0.48.0 → queryapigate-0.49.0}/queryapigate/pool.py +0 -0
  116. {queryapigate-0.48.0 → queryapigate-0.49.0}/queryapigate/postman.py +0 -0
  117. {queryapigate-0.48.0 → queryapigate-0.49.0}/queryapigate/quotas.py +0 -0
  118. {queryapigate-0.48.0 → queryapigate-0.49.0}/queryapigate/ratelimit.py +0 -0
  119. {queryapigate-0.48.0 → queryapigate-0.49.0}/queryapigate/rediscache.py +0 -0
  120. {queryapigate-0.48.0 → queryapigate-0.49.0}/queryapigate/rekey.py +0 -0
  121. {queryapigate-0.48.0 → queryapigate-0.49.0}/queryapigate/runners.py +0 -0
  122. {queryapigate-0.48.0 → queryapigate-0.49.0}/queryapigate/schema.py +0 -0
  123. {queryapigate-0.48.0 → queryapigate-0.49.0}/queryapigate/scope.py +0 -0
  124. {queryapigate-0.48.0 → queryapigate-0.49.0}/queryapigate/secheaders.py +0 -0
  125. {queryapigate-0.48.0 → queryapigate-0.49.0}/queryapigate/secretrefs.py +0 -0
  126. {queryapigate-0.48.0 → queryapigate-0.49.0}/queryapigate/services/__init__.py +0 -0
  127. {queryapigate-0.48.0 → queryapigate-0.49.0}/queryapigate/services/access.py +0 -0
  128. {queryapigate-0.48.0 → queryapigate-0.49.0}/queryapigate/services/administrators.py +0 -0
  129. {queryapigate-0.48.0 → queryapigate-0.49.0}/queryapigate/services/audit.py +0 -0
  130. {queryapigate-0.48.0 → queryapigate-0.49.0}/queryapigate/services/collections.py +0 -0
  131. {queryapigate-0.48.0 → queryapigate-0.49.0}/queryapigate/services/connections.py +0 -0
  132. {queryapigate-0.48.0 → queryapigate-0.49.0}/queryapigate/services/descriptions.py +0 -0
  133. {queryapigate-0.48.0 → queryapigate-0.49.0}/queryapigate/services/destinations.py +0 -0
  134. {queryapigate-0.48.0 → queryapigate-0.49.0}/queryapigate/services/exports.py +0 -0
  135. {queryapigate-0.48.0 → queryapigate-0.49.0}/queryapigate/services/mcp.py +0 -0
  136. {queryapigate-0.48.0 → queryapigate-0.49.0}/queryapigate/services/queries.py +0 -0
  137. {queryapigate-0.48.0 → queryapigate-0.49.0}/queryapigate/services/workspaces.py +0 -0
  138. {queryapigate-0.48.0 → queryapigate-0.49.0}/queryapigate/sqlflow.py +0 -0
  139. {queryapigate-0.48.0 → queryapigate-0.49.0}/queryapigate/sqltools.py +0 -0
  140. {queryapigate-0.48.0 → queryapigate-0.49.0}/queryapigate/sso.py +0 -0
  141. {queryapigate-0.48.0 → queryapigate-0.49.0}/queryapigate/store.py +0 -0
  142. {queryapigate-0.48.0 → queryapigate-0.49.0}/queryapigate/tableguard.py +0 -0
  143. {queryapigate-0.48.0 → queryapigate-0.49.0}/queryapigate/tracing.py +0 -0
  144. {queryapigate-0.48.0 → queryapigate-0.49.0}/queryapigate/usage.py +0 -0
  145. {queryapigate-0.48.0 → queryapigate-0.49.0}/queryapigate/v1.py +0 -0
  146. {queryapigate-0.48.0 → queryapigate-0.49.0}/queryapigate/v1_spec.py +0 -0
  147. {queryapigate-0.48.0 → queryapigate-0.49.0}/queryapigate/vault.py +0 -0
  148. {queryapigate-0.48.0 → queryapigate-0.49.0}/queryapigate/workspaces.py +0 -0
  149. {queryapigate-0.48.0 → queryapigate-0.49.0}/queryapigate.egg-info/SOURCES.txt +0 -0
  150. {queryapigate-0.48.0 → queryapigate-0.49.0}/queryapigate.egg-info/dependency_links.txt +0 -0
  151. {queryapigate-0.48.0 → queryapigate-0.49.0}/queryapigate.egg-info/entry_points.txt +0 -0
  152. {queryapigate-0.48.0 → queryapigate-0.49.0}/queryapigate.egg-info/requires.txt +0 -0
  153. {queryapigate-0.48.0 → queryapigate-0.49.0}/queryapigate.egg-info/top_level.txt +0 -0
  154. {queryapigate-0.48.0 → queryapigate-0.49.0}/setup.cfg +0 -0
  155. {queryapigate-0.48.0 → queryapigate-0.49.0}/tests/test_adhoc_history.py +0 -0
  156. {queryapigate-0.48.0 → queryapigate-0.49.0}/tests/test_adminroles.py +0 -0
  157. {queryapigate-0.48.0 → queryapigate-0.49.0}/tests/test_admins.py +0 -0
  158. {queryapigate-0.48.0 → queryapigate-0.49.0}/tests/test_agent_writes.py +0 -0
  159. {queryapigate-0.48.0 → queryapigate-0.49.0}/tests/test_alerts.py +0 -0
  160. {queryapigate-0.48.0 → queryapigate-0.49.0}/tests/test_api_administrators.py +0 -0
  161. {queryapigate-0.48.0 → queryapigate-0.49.0}/tests/test_api_keys.py +0 -0
  162. {queryapigate-0.48.0 → queryapigate-0.49.0}/tests/test_api_v1.py +0 -0
  163. {queryapigate-0.48.0 → queryapigate-0.49.0}/tests/test_audit_chain.py +0 -0
  164. {queryapigate-0.48.0 → queryapigate-0.49.0}/tests/test_audit_log.py +0 -0
  165. {queryapigate-0.48.0 → queryapigate-0.49.0}/tests/test_audit_siem.py +0 -0
  166. {queryapigate-0.48.0 → queryapigate-0.49.0}/tests/test_audit_stream.py +0 -0
  167. {queryapigate-0.48.0 → queryapigate-0.49.0}/tests/test_aws.py +0 -0
  168. {queryapigate-0.48.0 → queryapigate-0.49.0}/tests/test_backup_restore.py +0 -0
  169. {queryapigate-0.48.0 → queryapigate-0.49.0}/tests/test_broadcast.py +0 -0
  170. {queryapigate-0.48.0 → queryapigate-0.49.0}/tests/test_cli.py +0 -0
  171. {queryapigate-0.48.0 → queryapigate-0.49.0}/tests/test_collection_bundle.py +0 -0
  172. {queryapigate-0.48.0 → queryapigate-0.49.0}/tests/test_collection_postman.py +0 -0
  173. {queryapigate-0.48.0 → queryapigate-0.49.0}/tests/test_collections.py +0 -0
  174. {queryapigate-0.48.0 → queryapigate-0.49.0}/tests/test_configcode.py +0 -0
  175. {queryapigate-0.48.0 → queryapigate-0.49.0}/tests/test_connection_encryption.py +0 -0
  176. {queryapigate-0.48.0 → queryapigate-0.49.0}/tests/test_console.py +0 -0
  177. {queryapigate-0.48.0 → queryapigate-0.49.0}/tests/test_cors_and_rate_limit.py +0 -0
  178. {queryapigate-0.48.0 → queryapigate-0.49.0}/tests/test_data_protection.py +0 -0
  179. {queryapigate-0.48.0 → queryapigate-0.49.0}/tests/test_databases.py +0 -0
  180. {queryapigate-0.48.0 → queryapigate-0.49.0}/tests/test_dataplane.py +0 -0
  181. {queryapigate-0.48.0 → queryapigate-0.49.0}/tests/test_dependency_floors.py +0 -0
  182. {queryapigate-0.48.0 → queryapigate-0.49.0}/tests/test_deprecations.py +0 -0
  183. {queryapigate-0.48.0 → queryapigate-0.49.0}/tests/test_descriptions.py +0 -0
  184. {queryapigate-0.48.0 → queryapigate-0.49.0}/tests/test_destinations.py +0 -0
  185. {queryapigate-0.48.0 → queryapigate-0.49.0}/tests/test_docs_site.py +0 -0
  186. {queryapigate-0.48.0 → queryapigate-0.49.0}/tests/test_duckdb.py +0 -0
  187. {queryapigate-0.48.0 → queryapigate-0.49.0}/tests/test_duckdb_files.py +0 -0
  188. {queryapigate-0.48.0 → queryapigate-0.49.0}/tests/test_embeddings.py +0 -0
  189. {queryapigate-0.48.0 → queryapigate-0.49.0}/tests/test_errors.py +0 -0
  190. {queryapigate-0.48.0 → queryapigate-0.49.0}/tests/test_events.py +0 -0
  191. {queryapigate-0.48.0 → queryapigate-0.49.0}/tests/test_examples.py +0 -0
  192. {queryapigate-0.48.0 → queryapigate-0.49.0}/tests/test_export_security.py +0 -0
  193. {queryapigate-0.48.0 → queryapigate-0.49.0}/tests/test_exporting.py +0 -0
  194. {queryapigate-0.48.0 → queryapigate-0.49.0}/tests/test_exports.py +0 -0
  195. {queryapigate-0.48.0 → queryapigate-0.49.0}/tests/test_fips.py +0 -0
  196. {queryapigate-0.48.0 → queryapigate-0.49.0}/tests/test_helm.py +0 -0
  197. {queryapigate-0.48.0 → queryapigate-0.49.0}/tests/test_howto.py +0 -0
  198. {queryapigate-0.48.0 → queryapigate-0.49.0}/tests/test_instances.py +0 -0
  199. {queryapigate-0.48.0 → queryapigate-0.49.0}/tests/test_integration.py +0 -0
  200. {queryapigate-0.48.0 → queryapigate-0.49.0}/tests/test_jdbc.py +0 -0
  201. {queryapigate-0.48.0 → queryapigate-0.49.0}/tests/test_jwt.py +0 -0
  202. {queryapigate-0.48.0 → queryapigate-0.49.0}/tests/test_key_rotation.py +0 -0
  203. {queryapigate-0.48.0 → queryapigate-0.49.0}/tests/test_limits.py +0 -0
  204. {queryapigate-0.48.0 → queryapigate-0.49.0}/tests/test_masking_requests.py +0 -0
  205. {queryapigate-0.48.0 → queryapigate-0.49.0}/tests/test_mcp.py +0 -0
  206. {queryapigate-0.48.0 → queryapigate-0.49.0}/tests/test_mcp_keycloak.py +0 -0
  207. {queryapigate-0.48.0 → queryapigate-0.49.0}/tests/test_mcp_oauth.py +0 -0
  208. {queryapigate-0.48.0 → queryapigate-0.49.0}/tests/test_metacache.py +0 -0
  209. {queryapigate-0.48.0 → queryapigate-0.49.0}/tests/test_mongo_streaming.py +0 -0
  210. {queryapigate-0.48.0 → queryapigate-0.49.0}/tests/test_mongotools.py +0 -0
  211. {queryapigate-0.48.0 → queryapigate-0.49.0}/tests/test_multinode.py +0 -0
  212. {queryapigate-0.48.0 → queryapigate-0.49.0}/tests/test_observability.py +0 -0
  213. {queryapigate-0.48.0 → queryapigate-0.49.0}/tests/test_oidc.py +0 -0
  214. {queryapigate-0.48.0 → queryapigate-0.49.0}/tests/test_params.py +0 -0
  215. {queryapigate-0.48.0 → queryapigate-0.49.0}/tests/test_parquet.py +0 -0
  216. {queryapigate-0.48.0 → queryapigate-0.49.0}/tests/test_pool.py +0 -0
  217. {queryapigate-0.48.0 → queryapigate-0.49.0}/tests/test_publishing.py +0 -0
  218. {queryapigate-0.48.0 → queryapigate-0.49.0}/tests/test_query_window.py +0 -0
  219. {queryapigate-0.48.0 → queryapigate-0.49.0}/tests/test_queryapigate.py +0 -0
  220. {queryapigate-0.48.0 → queryapigate-0.49.0}/tests/test_quotas.py +0 -0
  221. {queryapigate-0.48.0 → queryapigate-0.49.0}/tests/test_rekey.py +0 -0
  222. {queryapigate-0.48.0 → queryapigate-0.49.0}/tests/test_release_check.py +0 -0
  223. {queryapigate-0.48.0 → queryapigate-0.49.0}/tests/test_response_cache.py +0 -0
  224. {queryapigate-0.48.0 → queryapigate-0.49.0}/tests/test_roles.py +0 -0
  225. {queryapigate-0.48.0 → queryapigate-0.49.0}/tests/test_runstore.py +0 -0
  226. {queryapigate-0.48.0 → queryapigate-0.49.0}/tests/test_runstore_differential.py +0 -0
  227. {queryapigate-0.48.0 → queryapigate-0.49.0}/tests/test_secretrefs.py +0 -0
  228. {queryapigate-0.48.0 → queryapigate-0.49.0}/tests/test_security_headers.py +0 -0
  229. {queryapigate-0.48.0 → queryapigate-0.49.0}/tests/test_settings.py +0 -0
  230. {queryapigate-0.48.0 → queryapigate-0.49.0}/tests/test_shared_rate_limit.py +0 -0
  231. {queryapigate-0.48.0 → queryapigate-0.49.0}/tests/test_sql_guard_fuzz.py +0 -0
  232. {queryapigate-0.48.0 → queryapigate-0.49.0}/tests/test_sql_guard_writes.py +0 -0
  233. {queryapigate-0.48.0 → queryapigate-0.49.0}/tests/test_sqlflow.py +0 -0
  234. {queryapigate-0.48.0 → queryapigate-0.49.0}/tests/test_sso.py +0 -0
  235. {queryapigate-0.48.0 → queryapigate-0.49.0}/tests/test_sso_keycloak.py +0 -0
  236. {queryapigate-0.48.0 → queryapigate-0.49.0}/tests/test_supply_chain.py +0 -0
  237. {queryapigate-0.48.0 → queryapigate-0.49.0}/tests/test_tableguard.py +0 -0
  238. {queryapigate-0.48.0 → queryapigate-0.49.0}/tests/test_tracing.py +0 -0
  239. {queryapigate-0.48.0 → queryapigate-0.49.0}/tests/test_usage.py +0 -0
  240. {queryapigate-0.48.0 → queryapigate-0.49.0}/tests/test_vault.py +0 -0
  241. {queryapigate-0.48.0 → queryapigate-0.49.0}/tests/test_workspace_config_cli.py +0 -0
  242. {queryapigate-0.48.0 → queryapigate-0.49.0}/tests/test_workspace_isolation.py +0 -0
  243. {queryapigate-0.48.0 → queryapigate-0.49.0}/tests/test_workspace_observability.py +0 -0
  244. {queryapigate-0.48.0 → queryapigate-0.49.0}/tests/test_workspace_store.py +0 -0
  245. {queryapigate-0.48.0 → queryapigate-0.49.0}/tests/test_workspaces_api.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: queryapigate
3
- Version: 0.48.0
3
+ Version: 0.49.0
4
4
  Summary: Turn SQL into governed REST APIs over MySQL, PostgreSQL, ClickHouse, SQLite, DuckDB, H2, MongoDB, and Parquet/CSV/JSON files - JSON, CSV, Parquet or Excel out, with scoped keys, an admin Console and MCP for AI agents.
5
5
  Author-email: Anantha Raju C <arcswdev@gmail.com>
6
6
  License-Expression: Apache-2.0
@@ -1,5 +1,5 @@
1
1
  """QueryAPIGate - expose SQL databases as a REST API."""
2
- __version__ = "0.48.0"
2
+ __version__ = "0.49.0"
3
3
 
4
4
  from .app import create_app # noqa: E402 (app imports __version__)
5
5
 
@@ -12,7 +12,8 @@ added - when it meets every one of GRADUATION. Alerts, remote files through Duck
12
12
  Console sign-in through SSO in 0.30 (ADR 0017), with the Helm chart; the audit log streamed to a SIEM in 0.32, once
13
13
  tested against real receivers (ADR 0019); live events, and secrets from files and Vault, in 0.34 (ADR 0021) - AWS's
14
14
  stay, until tested against AWS. The connection types have their own terms (databases.GRADUATION): MongoDB met them
15
- in 0.37, with streaming exports (ADR 0022).
15
+ in 0.37, with streaming exports (ADR 0022). The ClickHouse run store graduated in 0.49 (ADR 0029), once upgrades from
16
+ each release that had it were tested.
16
17
  """
17
18
  from typing import Any
18
19
 
@@ -75,13 +76,6 @@ FEATURES: dict[str, dict[str, Any]] = {
75
76
  'settings': ('QUERYAPIGATE_DATAPLANE_SECRET',),
76
77
  'docs': ('documentation/DEPLOYMENT.md', 'A Rust data plane in front of /q/'),
77
78
  },
78
- 'run_store': {
79
- 'name': 'A ClickHouse run store for history, the event log and usage',
80
- 'why': 'a prototype (ADR 0029, decided go): its tables, its event ids and how it is switched on may change '
81
- 'before it graduates',
82
- 'settings': ('QUERYAPIGATE_RUN_STORE_URL',),
83
- 'docs': ('documentation/DEPLOYMENT.md', 'A ClickHouse run store (prototype)'),
84
- },
85
79
  }
86
80
 
87
81
  DB_TYPES = frozenset(t for f in FEATURES.values() for t in f.get('db_types', ()))
@@ -404,7 +404,10 @@ def _store_hash_key():
404
404
  def _hash(value, key):
405
405
  if value is None:
406
406
  return None
407
- text = value.decode('utf-8', 'replace') if isinstance(value, (bytes, bytearray)) else str(value)
407
+ # Binary values as their text, as JSON shows them - a memoryview too, which psycopg2 hands over for bytea: str() of
408
+ # one is its address, not its value
409
+ text = bytes(value).decode('utf-8', 'replace') if isinstance(value, (bytes, bytearray, memoryview)) \
410
+ else str(value)
408
411
  return hmac.new(key, text.encode('utf-8'), hashlib.sha256).hexdigest()[:16]
409
412
 
410
413
 
@@ -1,4 +1,4 @@
1
- """The run store (ADR 0029, experimental): run history, the event log and usage rollups in ClickHouse.
1
+ """The run store (ADR 0029): run history, the event log and usage rollups in ClickHouse.
2
2
 
3
3
  With QUERYAPIGATE_RUN_STORE_URL set, history.py's writer thread sends its batches here instead of to the metadata
4
4
  store, and everything that reads runs or events - GET /api/v1/history, the Console's runs per version, usage
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: queryapigate
3
- Version: 0.48.0
3
+ Version: 0.49.0
4
4
  Summary: Turn SQL into governed REST APIs over MySQL, PostgreSQL, ClickHouse, SQLite, DuckDB, H2, MongoDB, and Parquet/CSV/JSON files - JSON, CSV, Parquet or Excel out, with scoped keys, an admin Console and MCP for AI agents.
5
5
  Author-email: Anantha Raju C <arcswdev@gmail.com>
6
6
  License-Expression: Apache-2.0
@@ -15,7 +15,7 @@ from unittest import mock
15
15
 
16
16
  from queryapigate import config, db, events, history
17
17
  from queryapigate.store import now as store_now
18
- from tests import TEST_DATABASE_URL
18
+ from tests import TEST_DATABASE_URL, TEST_RUN_STORE_URL
19
19
  from tests.helpers import save_query, write_connections
20
20
 
21
21
  sqlite_only = unittest.skipIf(TEST_DATABASE_URL, 'inspects the SQLite file itself')
@@ -316,6 +316,7 @@ class PostgresDbTests(unittest.TestCase):
316
316
  self.assertIs(db.connection(), parent)
317
317
  self.assertEqual(parent.execute('SELECT 1').fetchone()[0], 1) # the parent's is still usable
318
318
 
319
+ @unittest.skipIf(TEST_RUN_STORE_URL, 'a run store keeps history by time, not by count')
319
320
  def test_concurrent_history_writes_are_all_recorded_then_trimmed(self):
320
321
  """Runs recorded from many threads at once (queued, then batched by history.py, whose writes skip the
321
322
  store-wide lock on Postgres) must all land, and the per-version cap must still hold afterwards."""
@@ -456,6 +457,7 @@ class MigrateToPostgresTests(unittest.TestCase):
456
457
  db.close()
457
458
  return snapshot
458
459
 
460
+ @unittest.skipIf(TEST_RUN_STORE_URL, "the SQLite store's runs are in the run store, which the copy leaves there")
459
461
  def test_copies_everything_and_the_api_sees_the_same_store(self):
460
462
  before = os.path.getmtime(self.sqlite_path), os.path.getsize(self.sqlite_path)
461
463
  copied = db.migrate_sqlite_to_postgres(self.sqlite_path)
@@ -467,6 +469,7 @@ class MigrateToPostgresTests(unittest.TestCase):
467
469
  self.assertEqual((os.path.getmtime(self.sqlite_path), os.path.getsize(self.sqlite_path)), before)
468
470
  self.assertEqual(copied['events'], 3)
469
471
 
472
+ @unittest.skipIf(TEST_RUN_STORE_URL, "the SQLite store's events are in the run store, which the copy leaves there")
470
473
  def test_event_ids_and_their_sequence_come_across(self):
471
474
  """An id handed out and since swept is not handed out again: the sequence is SQLite's, not MAX(id)."""
472
475
  source = sqlite3.connect(self.sqlite_path)
@@ -247,6 +247,7 @@ class DeliveryTests(EventServerTestCase):
247
247
  self.assertTrue(stream.nothing_more())
248
248
 
249
249
  @unittest.skipUnless(TEST_DATABASE_URL, 'set QUERYAPIGATE_TEST_DATABASE_URL to run')
250
+ @unittest.skipIf(TEST_RUN_STORE_URL, "the metadata store's NOTIFY: with a run store, the events server tails it")
250
251
  def test_postgres_notify_delivers_without_waiting_for_the_poll(self):
251
252
  self.start_server(poll_interval=30) # only a NOTIFY can wake this one in time
252
253
  stream = self.open('admin')
@@ -80,7 +80,8 @@ class MarkedEverywhereTests(unittest.TestCase):
80
80
  ('documentation/MCP.md', 'Sign-in for AI clients (OAuth)', '0.30'),
81
81
  ('documentation/MCP.md', 'Write tools: agents that write, approved by a person',
82
82
  '0.30'),
83
- ('documentation/DEPLOYMENT.md', 'Kubernetes (Helm)', '0.30')):
83
+ ('documentation/DEPLOYMENT.md', 'Kubernetes (Helm)', '0.30'),
84
+ ('documentation/DEPLOYMENT.md', 'A ClickHouse run store', '0.49')):
84
85
  with self.subTest(heading=heading):
85
86
  text = read(path)
86
87
  match = re.search(rf'^#+ {re.escape(heading)}\n+(.+)', text, re.M)
@@ -239,6 +239,7 @@ class RetentionTests(HistoryTestCase):
239
239
  self.assertEqual(self.stored(), 2)
240
240
 
241
241
  @unittest.skipUnless(TEST_DATABASE_URL, 'several instances only share a PostgreSQL store')
242
+ @unittest.skipIf(TEST_RUN_STORE_URL, "a run store's TTL, not a sweep, keeps its retention")
242
243
  def test_while_another_instance_sweeps_this_one_skips(self):
243
244
  with mock.patch.dict(os.environ, {'QUERYAPIGATE_HISTORY_RETENTION_DAYS': '7'}):
244
245
  now = datetime(2026, 3, 1, 12, 0, 0)
@@ -221,6 +221,17 @@ class ApplyTests(unittest.TestCase):
221
221
  with mock.patch.dict(os.environ, {'QUERYAPIGATE_MASK_KEY': 'k2'}):
222
222
  self.assertNotEqual(hashed('jane@example.com'), before)
223
223
 
224
+ def test_binary_values_hash_by_their_value_whatever_the_driver_hands_over(self):
225
+ # psycopg2 gives a memoryview for bytea, mysql-connector bytes for a BLOB: the same value, the same hash -
226
+ # and two memoryviews of one value too, which str() would have told apart by their addresses
227
+ def hashed(value):
228
+ return list(masking.apply(['v'], [(value,)], ['hash'])[1])[0][0]
229
+ value = b'\x48\x65\x6c\x6c\x6f'
230
+ self.assertEqual(hashed(memoryview(value)), hashed(value))
231
+ self.assertEqual(hashed(memoryview(value)), hashed(memoryview(bytes(value))))
232
+ self.assertEqual(hashed(bytearray(value)), hashed('Hello'))
233
+ self.assertNotEqual(hashed(memoryview(b'Hello')), hashed(memoryview(b'World')))
234
+
224
235
  def test_mask(self):
225
236
  for value, expected in (('jane@example.com', 'j***@example.com'), ('@x.org', '***@x.org'), ('Bo', 'B***'),
226
237
  ('', None), (12345, None), (None, None)):
@@ -7,7 +7,9 @@ What it can't do is start: a release refuses a store a newer one has upgraded (d
7
7
  restarts mid-upgrade must come back on the new version.
8
8
 
9
9
  This runs the real previous release, installed from PyPI - QUERYAPIGATE_TEST_OLD_BIN is its `queryapigate` - beside
10
- the current code, on one store. Runs when that and QUERYAPIGATE_TEST_DATABASE_URL are set (CI's PostgreSQL job).
10
+ the current code, on one store. Runs when that and QUERYAPIGATE_TEST_DATABASE_URL are set (CI's PostgreSQL job). With
11
+ QUERYAPIGATE_TEST_RUN_STORE_URL set too (CI's run store job on PostgreSQL), both keep their runs in one ClickHouse run
12
+ store (ADR 0029): each node's runs land there, with ids of its own, whichever release it is.
11
13
 
12
14
  The one exception: 0.38's store change (schema 17, workspaces - ADR 0023) isn't additive, so a 0.37 instance can't
13
15
  write to a store 0.38 has upgraded, and that upgrade stops every instance first (CHANGELOG). Against a previous release
@@ -23,6 +25,7 @@ import uuid
23
25
  from tests.test_multinode import ADMIN, call, eventually
24
26
 
25
27
  DATABASE_URL = os.environ.get('QUERYAPIGATE_TEST_DATABASE_URL')
28
+ RUN_STORE_URL = os.environ.get('QUERYAPIGATE_TEST_RUN_STORE_URL', '').strip() or None
26
29
  OLD_BIN = os.environ.get('QUERYAPIGATE_TEST_OLD_BIN')
27
30
  NOT_ADDITIVE_UNTIL = (0, 38, 0) # the release whose store change wasn't additive: one before it can't serve beside it
28
31
 
@@ -58,8 +61,10 @@ class RollingUpgradeTests(unittest.TestCase):
58
61
  separator = '&' if '?' in DATABASE_URL else '?'
59
62
  self.env = {**os.environ, 'QUERYAPIGATE_API_KEY': ADMIN, 'QUERYAPIGATE_HISTORY_FLUSH_INTERVAL': '0.1',
60
63
  'QUERYAPIGATE_DATABASE_URL': f'{DATABASE_URL}{separator}options=-csearch_path%3D{self.schema}'}
61
- for name in ('QUERYAPIGATE_TEST_DATABASE_URL', 'QUERYAPIGATE_REDIS_URL'):
64
+ for name in ('QUERYAPIGATE_TEST_DATABASE_URL', 'QUERYAPIGATE_TEST_RUN_STORE_URL', 'QUERYAPIGATE_REDIS_URL'):
62
65
  self.env.pop(name, None)
66
+ if RUN_STORE_URL: # a database of its own, named as the schema is
67
+ self.env['QUERYAPIGATE_RUN_STORE_URL'] = f'{RUN_STORE_URL.rstrip("/")}/{self.schema}'
63
68
  self.processes = []
64
69
  self.data = os.path.join(self.tmp.name, 'data.db')
65
70
  import sqlite3
@@ -82,6 +87,9 @@ class RollingUpgradeTests(unittest.TestCase):
82
87
  conn.autocommit = True
83
88
  conn.cursor().execute(f'DROP SCHEMA IF EXISTS {self.schema} CASCADE')
84
89
  conn.close()
90
+ if RUN_STORE_URL:
91
+ from queryapigate import runstore
92
+ runstore.execute(f'DROP DATABASE IF EXISTS {self.schema} SYNC', db_name='default')
85
93
  self.tmp.cleanup()
86
94
 
87
95
  def start(self, command, name):
@@ -146,6 +154,15 @@ class RollingUpgradeTests(unittest.TestCase):
146
154
  self.assertEqual(status, 201, body)
147
155
  self.assertEqual(call(old, 'GET', '/q/total', key=body['secret'])[1], [{'s': 30}])
148
156
  self.assertTrue(eventually(lambda: len(call(new, 'GET', '/api/v1/history?query=total')[1]['items']) >= 3))
157
+ if RUN_STORE_URL:
158
+ from queryapigate import runstore
159
+ self.assertEqual(runstore.query('SELECT max(version) AS v FROM run_store_schema', db_name=self.schema),
160
+ [{'v': runstore._SCHEMA_VERSION}])
161
+ [runs] = runstore.query("SELECT count() AS n, uniqExact(id) AS ids, groupUniqArray(key_name) AS keys "
162
+ "FROM events WHERE query_name = 'total'", db_name=self.schema)
163
+ self.assertGreaterEqual(runs['n'], 3)
164
+ self.assertEqual(runs['ids'], runs['n']) # the two releases' minters never handed out one id twice
165
+ self.assertEqual(sorted(runs['keys']), ['admin', 'made-by-new', 'made-by-old'])
149
166
 
150
167
  # A node that restarts mid-upgrade comes back on the old release only while the store is still at that
151
168
  # release's schema; once the new code has moved it on, the old release refuses, saying why. (Right after a
@@ -9,10 +9,17 @@ A new release adds its fixture; this test then covers it with no change here.
9
9
  Releases with the PostgreSQL store (0.12.0 on) also have <version>.postgres.tar.gz: the same estate in a PostgreSQL
10
10
  schema, dumped as plain SQL. When the suite runs against PostgreSQL, each is restored into the test's own schema and
11
11
  checked the same way - which is also a test of restoring a pg_dump (BACKLOG #71).
12
+
13
+ Releases with the ClickHouse run store (0.45.0 on, ADR 0029) also have <version>.runstore.tar.gz: the home, whose
14
+ metadata store says its runs are in ClickHouse, and runstore/, those tables and their rows. When the suite runs with
15
+ a run store, each is loaded into the test's own ClickHouse database and checked the same way, and its tables must be
16
+ at this code's run store schema: 0.45.0's are at version 1, so this tests that migration on a real release's tables.
12
17
  """
13
18
  import glob
19
+ import hashlib
14
20
  import json
15
21
  import os
22
+ import re
16
23
  import shutil
17
24
  import sqlite3
18
25
  import tarfile
@@ -20,7 +27,7 @@ import tempfile
20
27
  import unittest
21
28
  from unittest import mock
22
29
 
23
- from queryapigate import create_app, db, events, history
30
+ from queryapigate import create_app, db, events, history, runstore
24
31
  from tests import TEST_DATABASE_URL, TEST_RUN_STORE_URL
25
32
  from tests.helpers import needs_sqlite_335
26
33
 
@@ -52,6 +59,34 @@ def restore_postgres(sql):
52
59
  conn.close()
53
60
 
54
61
 
62
+ def restore_run_store(home):
63
+ """Load a fixture's run store (runstore/ in its home) into this test's own ClickHouse database: the tables, their
64
+ rows, then the views that write to them, so no row is counted twice. TTL merges stay stopped on it: the fixture's
65
+ events are older than the event log keeps, and ClickHouse would drop them partway through a test. The metadata
66
+ store's marker names the ClickHouse the fixture was made on; it's pointed at this one, as the connection is
67
+ pointed at this home's data.db."""
68
+ name = runstore.database()
69
+ runstore.execute(f'DROP DATABASE IF EXISTS {name} SYNC', db_name='default')
70
+ runstore.execute(f'CREATE DATABASE {name}', db_name='default')
71
+ with open(os.path.join(home, 'runstore', 'tables.json')) as f:
72
+ tables = json.load(f)
73
+ for table in tables:
74
+ ddl = table['ddl'].replace('qag_fixture.', name + '.')
75
+ runstore.execute(re.sub(r'DEFINER = \S+', 'DEFINER = CURRENT_USER', ddl), db_name=name)
76
+ if table['engine'].endswith('MergeTree'):
77
+ runstore.execute(f'SYSTEM STOP TTL MERGES {name}.{table["name"]}', db_name=name)
78
+ rows = os.path.join(home, 'runstore', f'{table["name"]}.native')
79
+ if os.path.exists(rows):
80
+ with open(rows, 'rb') as f:
81
+ runstore._client().request({'database': name, 'query': f'INSERT INTO {table["name"]} FORMAT Native'},
82
+ f.read())
83
+ conn = sqlite3.connect(os.path.join(home, 'queryapigate.db'))
84
+ conn.execute('UPDATE run_store SET location = ?', (runstore.location(),))
85
+ conn.commit()
86
+ conn.close()
87
+ return name
88
+
89
+
55
90
  class FixtureTestCase(unittest.TestCase):
56
91
  def open_home(self, version, kind=''):
57
92
  """A fresh home holding that version's store, with the test's QUERYAPIGATE_HOME pointed at it."""
@@ -69,9 +104,22 @@ class FixtureTestCase(unittest.TestCase):
69
104
 
70
105
 
71
106
  class StoreChecks(FixtureTestCase):
72
- """What every fixture must read back, whichever store it is in - run by the two classes below."""
107
+ """What every fixture must read back, whichever store it is in - run by the three classes below."""
73
108
  kind = ''
74
109
 
110
+ def restore(self, home):
111
+ """Put a fixture's PostgreSQL dump or run store where this run's settings look for them."""
112
+ if self.kind == '.postgres':
113
+ with open(os.path.join(home, 'store.sql')) as f:
114
+ restore_postgres(f.read())
115
+ elif self.kind == '.runstore':
116
+ name = restore_run_store(home)
117
+
118
+ def drop():
119
+ history.flush() # this test's last runs first, or they'd be written to a database just dropped
120
+ runstore.execute(f'DROP DATABASE IF EXISTS {name} SYNC', db_name='default')
121
+ self.addCleanup(drop)
122
+
75
123
  def check_every_release(self):
76
124
  for version in fixture_versions(self.kind):
77
125
  with self.subTest(version=version):
@@ -91,9 +139,7 @@ class StoreChecks(FixtureTestCase):
91
139
  create_app()
92
140
  return
93
141
  self.assertEqual(expected['skipped'], [])
94
- if self.kind == '.postgres':
95
- with open(os.path.join(home, 'store.sql')) as f:
96
- restore_postgres(f.read())
142
+ self.restore(home)
97
143
  client = create_app().test_client()
98
144
 
99
145
  connection = self.get(client, '/api/v1/connections/films')
@@ -121,16 +167,36 @@ class StoreChecks(FixtureTestCase):
121
167
  self.assertEqual([row['title'] for row in res.get_json()], ['Alpha', 'Gamma'])
122
168
 
123
169
  self.assertTrue(self.get(client, '/api/v1/audit')['items'])
170
+ if self.kind == '.runstore':
171
+ self.check_run_store(client, expected)
172
+
173
+ def check_run_store(self, client, expected):
174
+ """A run store's tables at this code's schema, whichever release made them, and nothing lost on the way."""
175
+ history.flush()
176
+ self.assertEqual(runstore.query('SELECT max(version) AS v FROM run_store_schema')[0]['v'],
177
+ runstore._SCHEMA_VERSION)
178
+ # The ad-hoc run's statement, in the hourly rollups as its hash whether the release kept the text (0.45) or not
179
+ sql = "SELECT title FROM film WHERE rating = 'R'"
180
+ self.assertEqual([row['statement'] for row in runstore.query(
181
+ 'SELECT DISTINCT statement FROM run_hours WHERE query_name IS NULL')],
182
+ ['sha:' + hashlib.sha256(sql.encode()).hexdigest()])
183
+ self.assertEqual(sum(row['runs'] for row in runstore.query('SELECT runs FROM run_hours')),
184
+ expected['run_store']['rows']['events'] + 1)
185
+ runs = self.get(client, '/api/v1/history')['items']
186
+ self.assertEqual(sorted(r['kind'] for r in runs), ['adhoc', 'saved', 'saved']) # and the run just made
187
+ self.assertEqual([r['sql'] for r in runs if r['kind'] == 'adhoc'], [sql])
124
188
 
125
189
  def check_event_ids(self, version):
126
190
  """ADR 0002 §3, ADR 0020 §4: the run's history row id was its event id, and stays so. A client holding the id
127
191
  before it resumes with the run; the next event's id is above every id the old release could have sent."""
128
192
  home, _ = self.open_home(version, self.kind)
129
- if self.kind == '.postgres':
130
- with open(os.path.join(home, 'store.sql')) as f:
131
- restore_postgres(f.read())
132
- newest = db.connection().execute('SELECT MAX(rowid) FROM execution_history').fetchone()[0]
133
- with mock.patch.dict(os.environ, {'QUERYAPIGATE_EVENTS_RETENTION_HOURS': '1000000'}): # however old the run
193
+ self.restore(home)
194
+ if self.kind == '.runstore':
195
+ newest = runstore.query('SELECT max(id) AS id FROM events')[0]['id']
196
+ else:
197
+ newest = db.connection().execute('SELECT MAX(rowid) FROM execution_history').fetchone()[0]
198
+ # However old the run: eleven years, within ClickHouse's DateTime, which ends in 2106
199
+ with mock.patch.dict(os.environ, {'QUERYAPIGATE_EVENTS_RETENTION_HOURS': '100000'}):
134
200
  client = create_app().test_client()
135
201
  [(event_id, payload_json)] = events._replay_page(newest - 1, None, {})
136
202
  payload = json.loads(payload_json)
@@ -140,7 +206,11 @@ class StoreChecks(FixtureTestCase):
140
206
  self.assertEqual(res.status_code, 200, res.get_data(as_text=True))
141
207
  self.assertEqual(client.get('/q/films_by_rating?rating=PG', headers=ADMIN).status_code, 200)
142
208
  history.flush()
143
- self.assertEqual([row[0] for row in events._replay_page(newest, None, {})], [newest + 1])
209
+ if self.kind == '.runstore': # a run store's ids are minted from the clock: above the old one, not next to it
210
+ [(after, _)] = events._replay_page(newest, None, {}, upto=runstore.MAX_ID)
211
+ self.assertGreater(after, newest)
212
+ else:
213
+ self.assertEqual([row[0] for row in events._replay_page(newest, None, {})], [newest + 1])
144
214
 
145
215
 
146
216
  @unittest.skipIf(TEST_DATABASE_URL, 'the SQLite stores and JSON homes')
@@ -175,6 +245,23 @@ class PostgresUpgradeTests(StoreChecks):
175
245
  self.check_event_ids(version)
176
246
 
177
247
 
248
+ @unittest.skipIf(TEST_DATABASE_URL, "the fixtures' metadata stores are SQLite")
249
+ @unittest.skipUnless(TEST_RUN_STORE_URL, 'set QUERYAPIGATE_TEST_RUN_STORE_URL to run')
250
+ class RunStoreUpgradeTests(StoreChecks):
251
+ kind = '.runstore'
252
+
253
+ def test_there_is_a_fixture_for_every_release_with_the_run_store(self):
254
+ self.assertLessEqual({'0.45.0', '0.46.0', '0.47.0', '0.48.0'}, set(fixture_versions(self.kind)))
255
+
256
+ def test_every_release_store_starts_with_its_data_intact(self):
257
+ self.check_every_release()
258
+
259
+ def test_every_release_store_keeps_its_event_ids(self):
260
+ for version in fixture_versions(self.kind):
261
+ with self.subTest(version=version):
262
+ self.check_event_ids(version)
263
+
264
+
178
265
  @unittest.skipIf(TEST_DATABASE_URL, 'the fixtures are SQLite stores')
179
266
  class RefusalTests(FixtureTestCase):
180
267
  """A store this version can't run on stops startup with a message saying what is wrong and what to do - before
File without changes
File without changes
File without changes