tai-sql 0.7.10__tar.gz → 0.7.12__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 (229) hide show
  1. {tai_sql-0.7.10 → tai_sql-0.7.12}/PKG-INFO +1 -1
  2. {tai_sql-0.7.10 → tai_sql-0.7.12}/pyproject.toml +1 -1
  3. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/__init__.py +4 -1
  4. tai_sql-0.7.12/tai_sql/cli/commands/cmd_pull/funcs.py +415 -0
  5. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_pull/main.py +75 -10
  6. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_rules/main.py +59 -4
  7. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/drivers/base.py +111 -4
  8. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/drivers/bigquery.py +12 -2
  9. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/drivers/mysql.py +155 -4
  10. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/drivers/postgresql.py +3 -2
  11. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/drivers/sqlserver.py +3 -2
  12. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/generators/python_client/context.py +25 -1
  13. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/generators/python_client/templates/base.py.jinja2 +20 -0
  14. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/generators/python_client/templates/domain/dao.py.jinja2 +4 -1
  15. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/generators/python_client/templates/domain/dtos.py.jinja2 +1 -1
  16. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/generators/python_client/templates/domain/model.py.jinja2 +2 -2
  17. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/generators/python_client/templates/facade.py.jinja2 +2 -0
  18. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/generators/python_client/triggers/transformer.py +8 -5
  19. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/generators/rules/__init__.py +6 -0
  20. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/generators/rules/templates/schema.md.jinja2 +6 -0
  21. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/introspect/README.md +35 -14
  22. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/introspect/blueprint.py +6 -0
  23. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/introspect/build.py +50 -32
  24. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/introspect/defaults.py +60 -4
  25. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/introspect/naming.py +35 -9
  26. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/introspect/templates/schema.py.jinja2 +7 -3
  27. tai_sql-0.7.12/tai_sql/introspect/types.py +228 -0
  28. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/introspect/writer.py +12 -3
  29. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/orm/__init__.py +2 -1
  30. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/orm/analysis/column_analyzer.py +81 -0
  31. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/orm/analysis/relation_analyzer.py +19 -1
  32. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/orm/analysis/schema_analyzer.py +6 -6
  33. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/orm/declarative/columns.py +21 -0
  34. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/orm/mapping/sqlalchemy_mapper.py +7 -1
  35. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/orm/model/column.py +11 -0
  36. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/orm/model/relation.py +23 -8
  37. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/orm/types.py +159 -1
  38. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/project/paths.py +37 -6
  39. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/rules/README.md +19 -1
  40. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/rules/__init__.py +12 -0
  41. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/rules/catalog.py +26 -9
  42. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/rules/installer.py +79 -5
  43. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/rules/service.py +41 -1
  44. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/rules/staleness.py +7 -3
  45. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/rules/static/comandos.md +52 -7
  46. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/rules/static/errores.md +16 -0
  47. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/rules/static/modelo.md +56 -3
  48. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/schema.py +29 -16
  49. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/sync/README.md +1 -0
  50. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/sync/diff/__init__.py +3 -1
  51. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/sync/diff/tables.py +17 -23
  52. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/sync/diff/views.py +3 -0
  53. tai_sql-0.7.12/tai_sql/sync/ownership.py +74 -0
  54. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/sync/plan/planner.py +1 -0
  55. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/sync/state/normalize.py +68 -1
  56. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/sync/state/remote.py +3 -2
  57. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/sync/synchronizer.py +3 -1
  58. tai_sql-0.7.10/tai_sql/cli/commands/cmd_pull/funcs.py +0 -232
  59. tai_sql-0.7.10/tai_sql/introspect/types.py +0 -143
  60. {tai_sql-0.7.10 → tai_sql-0.7.12}/LICENSE +0 -0
  61. {tai_sql-0.7.10 → tai_sql-0.7.12}/README.md +0 -0
  62. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/cli/commands/__init__.py +0 -0
  63. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_backup/__init__.py +0 -0
  64. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_backup/main.py +0 -0
  65. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_deploy/__init__.py +0 -0
  66. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_deploy/config.py +0 -0
  67. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_deploy/local.py +0 -0
  68. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_deploy/main.py +0 -0
  69. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_deploy/workflow.py +0 -0
  70. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_feed/__init__.py +0 -0
  71. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_feed/main.py +0 -0
  72. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_generate/__init__.py +0 -0
  73. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_generate/funcs.py +0 -0
  74. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_generate/main.py +0 -0
  75. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_hash_migrate/__init__.py +0 -0
  76. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_hash_migrate/main.py +0 -0
  77. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_info/__init__.py +0 -0
  78. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_info/main.py +0 -0
  79. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_info/status.py +0 -0
  80. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_init/__init__.py +0 -0
  81. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_init/main.py +0 -0
  82. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_init/model.py +0 -0
  83. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_install/__init__.py +0 -0
  84. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_install/main.py +0 -0
  85. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_ping/__init__.py +0 -0
  86. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_ping/main.py +0 -0
  87. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_pull/__init__.py +0 -0
  88. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_restore/__init__.py +0 -0
  89. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_restore/main.py +0 -0
  90. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_rules/__init__.py +0 -0
  91. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_schema/__init__.py +0 -0
  92. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_schema/model.py +0 -0
  93. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_schema/new.py +0 -0
  94. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_schema/schemas/__init__.py +0 -0
  95. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_schema/schemas/auth.py +0 -0
  96. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_schema/schemas/base.py +0 -0
  97. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_schema/schemas/chatbot.py +0 -0
  98. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_schema/schemas/default.py +0 -0
  99. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_schema/set_default.py +0 -0
  100. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/cli/commands/prompts.py +0 -0
  101. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/cli/commands/utils/__init__.py +0 -0
  102. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/cli/commands/utils/connectivity.py +0 -0
  103. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/cli/commands/utils/create.py +0 -0
  104. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/cli/commands/utils/github/__init__.py +0 -0
  105. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/cli/commands/utils/github/auth.py +0 -0
  106. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/cli/commands/utils/github/client.py +0 -0
  107. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/cli/commands/utils/github/models.py +0 -0
  108. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/cli/errors.py +0 -0
  109. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/cli/main.py +0 -0
  110. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/cli/preamble.py +0 -0
  111. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/cli/services/__init__.py +0 -0
  112. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/cli/services/backup.py +0 -0
  113. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/connection/README.md +0 -0
  114. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/connection/__init__.py +0 -0
  115. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/connection/provider.py +0 -0
  116. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/connection/recipe.py +0 -0
  117. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/connection/session.py +0 -0
  118. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/connection/sources.py +0 -0
  119. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/crypto.py +0 -0
  120. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/drivers/__init__.py +0 -0
  121. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/drivers/capabilities.py +0 -0
  122. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/errors/README.md +0 -0
  123. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/errors/__init__.py +0 -0
  124. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/errors/collector.py +0 -0
  125. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/errors/render.py +0 -0
  126. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/errors/types.py +0 -0
  127. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/feed/README.md +0 -0
  128. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/feed/__init__.py +0 -0
  129. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/feed/collect.py +0 -0
  130. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/feed/executor.py +0 -0
  131. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/feed/feeder.py +0 -0
  132. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/feed/plan.py +0 -0
  133. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/feed/presenter.py +0 -0
  134. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/feed/records.py +0 -0
  135. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/feed/transform.py +0 -0
  136. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/generators/__init__.py +0 -0
  137. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/generators/base.py +0 -0
  138. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/generators/diagram/README.md +0 -0
  139. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/generators/diagram/__init__.py +0 -0
  140. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/generators/diagram/assets/README.md +0 -0
  141. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/generators/diagram/assets/cytoscape.min.js +0 -0
  142. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/generators/diagram/builder.py +0 -0
  143. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/generators/diagram/generator.py +0 -0
  144. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/generators/diagram/graphviz_render.py +0 -0
  145. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/generators/diagram/html_render.py +0 -0
  146. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/generators/diagram/layout.py +0 -0
  147. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/generators/diagram/model.py +0 -0
  148. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/generators/diagram/templates/er.html.jinja2 +0 -0
  149. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/generators/diagram/themes.py +0 -0
  150. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/generators/python_client/__init__.py +0 -0
  151. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/generators/python_client/calculated.py +0 -0
  152. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/generators/python_client/templates/domain/__init__.py.jinja2 +0 -0
  153. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/generators/python_client/templates/macros/aggregation.jinja2 +0 -0
  154. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/generators/python_client/templates/macros/dataframe.jinja2 +0 -0
  155. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/generators/python_client/templates/macros/filters.jinja2 +0 -0
  156. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/generators/python_client/templates/macros/hierarchy.jinja2 +0 -0
  157. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/generators/python_client/templates/macros/read.jinja2 +0 -0
  158. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/generators/python_client/templates/macros/triggers.jinja2 +0 -0
  159. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/generators/python_client/templates/macros/upsert.jinja2 +0 -0
  160. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/generators/python_client/templates/macros/vector.jinja2 +0 -0
  161. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/generators/python_client/templates/macros/write.jinja2 +0 -0
  162. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/generators/python_client/templates/session.py.jinja2 +0 -0
  163. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/generators/python_client/templates/shared/__init__.py.jinja2 +0 -0
  164. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/generators/python_client/templates/shared/aggregation.py.jinja2 +0 -0
  165. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/generators/python_client/templates/shared/context.py.jinja2 +0 -0
  166. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/generators/python_client/templates/shared/utils/__init__.py.jinja2 +0 -0
  167. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/generators/python_client/templates/shared/utils/encoders.py.jinja2 +0 -0
  168. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/generators/python_client/templates/shared/utils/funcs.py.jinja2 +0 -0
  169. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/generators/python_client/templates/shared/utils/hierarchy.py.jinja2 +0 -0
  170. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/generators/python_client/templates/shared/utils/rls.py.jinja2 +0 -0
  171. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/generators/python_client/templates/shared/vector.py.jinja2 +0 -0
  172. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/generators/python_client/triggers/__init__.py +0 -0
  173. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/generators/python_client/triggers/analyzer.py +0 -0
  174. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/generators/python_client/triggers/renderer.py +0 -0
  175. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/generators/python_client/triggers/steps.py +0 -0
  176. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/introspect/__init__.py +0 -0
  177. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/introspect/reader.py +0 -0
  178. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/orm/README.md +0 -0
  179. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/orm/analysis/README.md +0 -0
  180. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/orm/analysis/__init__.py +0 -0
  181. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/orm/analysis/calculated.py +0 -0
  182. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/orm/analysis/collector.py +0 -0
  183. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/orm/analysis/constraints.py +0 -0
  184. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/orm/analysis/hints.py +0 -0
  185. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/orm/declarative/README.md +0 -0
  186. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/orm/declarative/__init__.py +0 -0
  187. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/orm/declarative/annotations.py +0 -0
  188. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/orm/declarative/base.py +0 -0
  189. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/orm/declarative/calculated.py +0 -0
  190. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/orm/declarative/constraints.py +0 -0
  191. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/orm/declarative/datasource.py +0 -0
  192. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/orm/declarative/descriptors.py +0 -0
  193. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/orm/declarative/encoders.py +0 -0
  194. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/orm/declarative/enumerations.py +0 -0
  195. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/orm/declarative/relations.py +0 -0
  196. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/orm/declarative/table.py +0 -0
  197. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/orm/declarative/trigger_api.py +0 -0
  198. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/orm/declarative/triggers.py +0 -0
  199. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/orm/declarative/view.py +0 -0
  200. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/orm/mapping/README.md +0 -0
  201. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/orm/mapping/__init__.py +0 -0
  202. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/orm/model/README.md +0 -0
  203. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/orm/model/__init__.py +0 -0
  204. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/orm/model/constraints.py +0 -0
  205. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/orm/model/entity.py +0 -0
  206. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/orm/model/enumeration.py +0 -0
  207. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/orm/model/schema.py +0 -0
  208. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/orm/model/trigger.py +0 -0
  209. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/orm/naming.py +0 -0
  210. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/orm/ordering.py +0 -0
  211. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/orm/registry.py +0 -0
  212. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/project/README.md +0 -0
  213. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/project/__init__.py +0 -0
  214. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/project/config.py +0 -0
  215. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/project/context.py +0 -0
  216. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/project/manager.py +0 -0
  217. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/rules/meta.py +0 -0
  218. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/sync/__init__.py +0 -0
  219. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/sync/diff/constraints.py +0 -0
  220. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/sync/diff/indexes.py +0 -0
  221. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/sync/executor.py +0 -0
  222. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/sync/plan/__init__.py +0 -0
  223. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/sync/plan/statements.py +0 -0
  224. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/sync/presenter.py +0 -0
  225. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/sync/report.py +0 -0
  226. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/sync/safety.py +0 -0
  227. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/sync/state/__init__.py +0 -0
  228. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/sync/state/local.py +0 -0
  229. {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/sync/state/model.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tai-sql
3
- Version: 0.7.10
3
+ Version: 0.7.12
4
4
  Summary: SQL database management and code generation tool
5
5
  License-Expression: MIT
6
6
  License-File: LICENSE
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "tai-sql"
3
- version = "0.7.10"
3
+ version = "0.7.12"
4
4
  description = "SQL database management and code generation tool"
5
5
  license = "MIT"
6
6
  license-files = ["LICENSE"]
@@ -6,6 +6,7 @@ models using SQLAlchemy's declarative system.
6
6
  from __future__ import annotations
7
7
  from sqlalchemy.types import (
8
8
  BigInteger as bigint,
9
+ SmallInteger as smallint,
9
10
  Text as text,
10
11
  Numeric as numeric,
11
12
  LargeBinary as largebinary,
@@ -25,7 +26,7 @@ from .orm import (
25
26
  # DSL de cabecera del schema
26
27
  datasource, generate, query,
27
28
  # Core ORM components
28
- Table, View, Enum, column, vector, vector_column, VectorColumn,
29
+ Table, View, Enum, column, vector, vector_column, VectorColumn, timestamptz,
29
30
  relation, unique_constraint, index_constraint,
30
31
  TriggerAPI, on_create, on_update, on_delete,
31
32
  trigger_api, col, onetomany, manytoone, onetoone, get_type_registry,
@@ -57,6 +58,8 @@ __all__ = [
57
58
  'date',
58
59
  'time',
59
60
  'bigint',
61
+ 'smallint',
62
+ 'timestamptz',
60
63
  'text',
61
64
  'numeric',
62
65
  'largebinary',
@@ -0,0 +1,415 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ La orquestación de `tai-sql pull`: conectar, leer, decidir, escribir.
4
+
5
+ El comando de arriba solo traduce banderas y presenta; aquí está el flujo, que se puede
6
+ ejecutar sin Click.
7
+
8
+ **Es el único comando que invierte el patrón universal** (`00-filosofia.md`): no resuelve
9
+ un schema para actuar sobre su mapeo, porque es el que fabrica el schema. La excepción está
10
+ acotada a este módulo: en cuanto el fichero existe, `generate` y `push` vuelven al camino
11
+ de siempre.
12
+ """
13
+ from __future__ import annotations
14
+
15
+ import os
16
+ import tempfile
17
+ from contextlib import contextmanager
18
+ from dataclasses import dataclass
19
+ from pathlib import Path
20
+ from typing import Iterator, Optional, Protocol, Sequence
21
+
22
+ from sqlalchemy import Engine, event, inspect
23
+
24
+ from tai_sql.connection import Provider
25
+ from tai_sql.errors import ErrorCode, ProjectError, TaiSqlError
26
+ from tai_sql.introspect import SchemaBlueprint, build, read_view_columns, render
27
+ from tai_sql.introspect import list_schemas as _list_schemas
28
+ from tai_sql.sync.state import build_remote_state
29
+
30
+
31
+ @dataclass(frozen=True)
32
+ class PullResult:
33
+ """
34
+ Lo que produjo un `pull`.
35
+
36
+ Attributes:
37
+ blueprint: Lo que se dedujo, con sus descartes.
38
+ content: El texto del fichero de schema.
39
+ schema_file: Dónde se escribió, o `None` con `--stdout`.
40
+ view_files: Los `.sql` escritos.
41
+ load_errors: Lo que falló al cargar el fichero antes de escribirlo. Solo puede
42
+ no estar vacío con `--stdout`: sin él, un fichero que no carga no se escribe.
43
+ """
44
+ blueprint: SchemaBlueprint
45
+ content: str
46
+ schema_file: Optional[Path] = None
47
+ view_files: tuple[Path, ...] = ()
48
+ load_errors: tuple[str, ...] = ()
49
+
50
+
51
+ class PullProgress(Protocol):
52
+ """
53
+ Lo que `run_pull` cuenta mientras trabaja, para quien quiera enseñarlo.
54
+
55
+ Leer el catálogo de una base de datos grande —y más detrás de un túnel— tarda: un
56
+ `pull` de cien tablas puede pasar un minuto sin escribir nada, y eso no se distingue de
57
+ uno colgado. El flujo anuncia en qué fase está y cada consulta que lanza; pintarlo es
58
+ cosa de la capa de presentación.
59
+ """
60
+
61
+ def step(self, message: str) -> None:
62
+ """Empieza una fase: 'Leyendo tablas…'."""
63
+
64
+ def query(self) -> None:
65
+ """Se ha lanzado una consulta contra la base de datos."""
66
+
67
+
68
+ class _Silent:
69
+ """El progreso que no se enseña: lo que recibe `run_pull` si nadie pide otro."""
70
+
71
+ def step(self, message: str) -> None:
72
+ pass
73
+
74
+ def query(self) -> None:
75
+ pass
76
+
77
+
78
+ @contextmanager
79
+ def _counting_queries(engine: Engine, progress: PullProgress) -> Iterator[None]:
80
+ """
81
+ Avisa al progreso de cada consulta que se lanza contra `engine` mientras dura.
82
+
83
+ Es la única señal de vida que da la reflexión: `MetaData.reflect` es una sola llamada
84
+ que no dice por qué tabla va. El `Engine` se cachea por provider, así que el listener
85
+ se retira al salir: no puede quedarse contando para el siguiente consumidor.
86
+ """
87
+ def _on_query(*_args, **_kwargs) -> None:
88
+ progress.query()
89
+
90
+ event.listen(engine, 'before_cursor_execute', _on_query)
91
+ try:
92
+ yield
93
+ finally:
94
+ event.remove(engine, 'before_cursor_execute', _on_query)
95
+
96
+
97
+ def resolve_provider(source: str) -> Provider:
98
+ """
99
+ El proveedor de conexión, a partir de lo que se pasó en `-p/--provider`.
100
+
101
+ Las dos formas se distinguen por el `://`: sin él es el **nombre de una variable de
102
+ entorno**, que es el caso normal y el único que acaba escrito en el fichero.
103
+ """
104
+ if '://' in source:
105
+ return Provider.from_connection_string(source)
106
+ return Provider.from_environment(source)
107
+
108
+
109
+ def resolve_db_schema(provider: Provider, requested: Optional[str]) -> str:
110
+ """
111
+ El schema de la base de datos que se va a leer.
112
+
113
+ Sin `--db-schema`, se usa el del motor (`public`, `dbo`) si existe, y si no el único
114
+ que haya. Con varios y ninguno por defecto, **se corta y se listan**: elegir uno por
115
+ orden alfabético sería adivinar, y leer el equivocado produce un schema completo y
116
+ equivocado, que es peor que un error.
117
+ """
118
+ driver = provider.driver
119
+ if driver is not None and not driver.supports_schemas():
120
+ # En MySQL el «schema» del datasource es la base de datos: es lo que declara
121
+ # cualquier schema de MySQL (`schema='mi_bd'`) y lo que el DDL usa de prefijo.
122
+ # Antes se devolvía `''`, que `datasource()` convierte en `'public'`: el push
123
+ # apuntaba a una base de datos `public` y `query()` buscaba las vistas en
124
+ # `views/public/`, así que el schema recién escrito ni siquiera cargaba.
125
+ return (
126
+ requested
127
+ or inspect(provider.engine).default_schema_name
128
+ or provider.database
129
+ or ''
130
+ )
131
+
132
+ if requested:
133
+ return requested
134
+
135
+ disponibles = _list_schemas(provider.engine)
136
+ por_defecto = inspect(provider.engine).default_schema_name
137
+
138
+ if por_defecto and por_defecto in disponibles:
139
+ return por_defecto
140
+
141
+ if len(disponibles) == 1:
142
+ return disponibles[0]
143
+
144
+ raise ProjectError(
145
+ message='La base de datos tiene varios schemas y ninguno es el del motor',
146
+ solution=(
147
+ f"Indica cuál leer con '--db-schema <nombre>'. Disponibles: "
148
+ f"{', '.join(disponibles) or '(ninguno)'}"
149
+ ),
150
+ code=ErrorCode.SCHEMA_NOT_FOUND,
151
+ )
152
+
153
+
154
+ def run_pull(
155
+ *,
156
+ source: str = 'MAIN_DATABASE_URL',
157
+ db_schema: Optional[str] = None,
158
+ schema_name: Optional[str] = None,
159
+ env_var: Optional[str] = None,
160
+ syntax: str = 'v2',
161
+ exclude: Sequence[str] = (),
162
+ force: bool = False,
163
+ to_stdout: bool = False,
164
+ progress: Optional[PullProgress] = None,
165
+ ) -> PullResult:
166
+ """
167
+ Construye el fichero de schema que describe una base de datos.
168
+
169
+ Args:
170
+ source: Nombre de variable de entorno o cadena de conexión.
171
+ db_schema: Schema de la base de datos a leer.
172
+ schema_name: Nombre del fichero de schema. Por defecto, el del schema de la BD.
173
+ env_var: Variable que se escribirá en `env(...)`. Por defecto, la de `source`
174
+ cuando se pasó una variable; `MAIN_DATABASE_URL` cuando se pasó una cadena.
175
+ syntax: 'v1' o 'v2'.
176
+ exclude: Tablas que no se declaran.
177
+ force: Sobrescribir un fichero de schema existente.
178
+ to_stdout: No escribir nada; devolver el contenido.
179
+ progress: A quién contarle en qué fase va. Ver `PullProgress`.
180
+
181
+ Returns:
182
+ El resultado, con el blueprint para poder informar de los descartes.
183
+ """
184
+ from tai_sql import pm
185
+
186
+ if pm.find_project_root() is None:
187
+ raise ProjectError(
188
+ message='No estás dentro de un proyecto tai-sql',
189
+ solution=(
190
+ "pull escribe en 'schemas/' y en 'views/', así que necesita un proyecto. "
191
+ "Ejecútalo desde el directorio del proyecto o crea uno con "
192
+ "'tai-sql init <nombre>'."
193
+ ),
194
+ code=ErrorCode.PROJECT_NOT_FOUND,
195
+ )
196
+
197
+ provider = resolve_provider(source)
198
+ driver = provider.driver
199
+
200
+ if driver is None:
201
+ raise TaiSqlError(
202
+ message=f"No hay driver para '{provider.drivername}'",
203
+ solution=(
204
+ 'Los motores soportados son postgresql, mysql, mssql y bigquery. '
205
+ 'Comprueba el prefijo de la URL de conexión.'
206
+ ),
207
+ code=ErrorCode.INVALID_PROVIDER,
208
+ )
209
+
210
+ progress = progress or _Silent()
211
+
212
+ with _counting_queries(provider.engine, progress):
213
+ return _pull(
214
+ pm, provider, driver, progress,
215
+ source=source, db_schema=db_schema, schema_name=schema_name,
216
+ env_var=env_var, syntax=syntax, exclude=exclude, force=force,
217
+ to_stdout=to_stdout,
218
+ )
219
+
220
+
221
+ def _pull(
222
+ pm, provider: Provider, driver, progress: PullProgress, *,
223
+ source: str, db_schema: Optional[str], schema_name: Optional[str],
224
+ env_var: Optional[str], syntax: str, exclude: Sequence[str], force: bool,
225
+ to_stdout: bool,
226
+ ) -> PullResult:
227
+ """El flujo de `run_pull`, una vez resuelto el origen."""
228
+ progress.step(f'Conectando con {provider.database or provider.drivername}')
229
+ destino_db = resolve_db_schema(provider, db_schema)
230
+ nombre = schema_name or destino_db or 'main'
231
+
232
+ fichero = pm.paths.schema_file(nombre)
233
+ if fichero.exists() and not (force or to_stdout):
234
+ raise ProjectError(
235
+ message=f"El schema '{nombre}' ya existe en este proyecto",
236
+ solution=(
237
+ "pull no sobrescribe: un schema puede tener docstrings, feed(), triggers "
238
+ "y columnas calculadas que la base de datos no contiene y que pull no "
239
+ f"sabe reconstruir. Usa '--force' para sobrescribirlo de todas formas, "
240
+ f"'-s <otro nombre>' para escribir uno nuevo, o '--stdout' para verlo sin "
241
+ f"tocar el disco."
242
+ ),
243
+ location=str(fichero),
244
+ code=ErrorCode.DUPLICATE_NAME,
245
+ )
246
+
247
+ progress.step(f"Leyendo el catálogo de '{destino_db}'")
248
+ estado, _ = build_remote_state(
249
+ provider.engine, driver, destino_db or None, target=nombre
250
+ )
251
+
252
+ if estado.views:
253
+ progress.step(f'Leyendo {len(estado.views)} vista(s)')
254
+ columnas_vista = read_view_columns(
255
+ provider.engine, driver, destino_db or None, list(estado.views)
256
+ )
257
+
258
+ progress.step(f'Deduciendo el schema ({len(estado.tables)} tablas)')
259
+ blueprint = build(
260
+ estado,
261
+ driver,
262
+ name=nombre,
263
+ db_schema=destino_db,
264
+ env_var=env_var or _env_var_for(source),
265
+ syntax=syntax,
266
+ view_columns=columnas_vista,
267
+ exclude=exclude,
268
+ )
269
+
270
+ contenido = render(blueprint)
271
+
272
+ progress.step('Comprobando que el schema carga')
273
+ errores = self_check(blueprint, provider)
274
+
275
+ if to_stdout:
276
+ return PullResult(blueprint=blueprint, content=contenido, load_errors=errores)
277
+
278
+ if errores:
279
+ detalle = '\n'.join(f' · {e}' for e in errores)
280
+ raise TaiSqlError(
281
+ message=(
282
+ f"El schema que ha deducido pull no carga, así que no se ha escrito:\n"
283
+ f"{detalle}"
284
+ ),
285
+ solution=(
286
+ "Es un fallo de tai-sql pull, no de tu base de datos: repórtalo con este "
287
+ "mensaje. Mientras tanto, 'tai-sql pull --stdout' imprime el fichero para "
288
+ "que lo completes a mano."
289
+ ),
290
+ code=ErrorCode.SCHEMA_IMPORT_FAILED,
291
+ )
292
+
293
+ progress.step('Escribiendo el schema')
294
+ escritas = _write(pm, blueprint, contenido, fichero)
295
+
296
+ return PullResult(
297
+ blueprint=blueprint,
298
+ content=contenido,
299
+ schema_file=fichero,
300
+ view_files=escritas,
301
+ )
302
+
303
+
304
+ def self_check(blueprint: SchemaBlueprint, provider: Provider) -> tuple[str, ...]:
305
+ """
306
+ Carga y analiza el schema deducido, en un proyecto temporal, antes de escribirlo.
307
+
308
+ `pull` tiene que escribir un schema **que cargue**, y hasta ahora nada lo comprobaba.
309
+ La primera base de datos ajena sobre la que se ejecutó dio un fichero que no se podía
310
+ ni importar —las vistas estaban donde `query()` no las buscaba, y tres tablas tenían
311
+ dos claves foráneas hacia la misma tabla, que el análisis rechazaba—, y el desarrollador
312
+ se enteraba al ejecutar el comando siguiente.
313
+
314
+ Es el mismo camino que seguirá `generate`: el import del fichero con sus `query()` y
315
+ el análisis completo. Se hace en un directorio temporal, con los generadores apuntando
316
+ dentro de él —instanciar uno crea su directorio—, y restaurando el schema y el
317
+ registro activos: comprobar no deja rastro.
318
+
319
+ Returns:
320
+ Los errores, uno por línea; vacío si el schema carga.
321
+ """
322
+ from tai_sql.errors import TaiSqlError as _Error
323
+ from tai_sql.orm.analysis import ErrorCollector
324
+ from tai_sql.orm.registry import use_registry
325
+ from tai_sql.project.context import use_schema
326
+ from tai_sql.schema import SchemaManager
327
+
328
+ with tempfile.TemporaryDirectory(prefix='tai-sql-pull-') as tmp:
329
+ raiz = Path(tmp)
330
+ contenido = render(
331
+ blueprint,
332
+ client_dir=str(raiz / 'cliente'),
333
+ diagrams_dir=str(raiz / 'diagramas'),
334
+ )
335
+ fichero = raiz / 'schemas' / f'{blueprint.name}.py'
336
+ fichero.parent.mkdir(parents=True)
337
+ fichero.write_text(contenido, encoding='utf-8')
338
+ _write_views(raiz / 'views', blueprint)
339
+
340
+ # El `env(...)` del fichero se resuelve al importarlo. Si la variable no está
341
+ # definida —se pasó una cadena de conexión con `-p`—, se le da la del origen
342
+ # solo mientras dura la comprobación.
343
+ faltaba = blueprint.env_var not in os.environ
344
+ if faltaba:
345
+ os.environ[blueprint.env_var] = provider.url.render_as_string(
346
+ hide_password=False
347
+ )
348
+
349
+ try:
350
+ with use_registry(), use_schema(None):
351
+ manager = SchemaManager(blueprint.name, fichero)
352
+ with use_schema(manager):
353
+ manager.load()
354
+ manager.validations()
355
+ collector = ErrorCollector(
356
+ context=f"Schema '{blueprint.name}'", enable_colors=False
357
+ )
358
+ manager.analyze(collector)
359
+ return tuple(e.message for e in collector.get_errors())
360
+ except _Error as exc:
361
+ return (exc.message,)
362
+ except Exception as exc: # el import de un fichero puede lanzar cualquier cosa
363
+ return (f'{type(exc).__name__}: {exc}',)
364
+ finally:
365
+ if faltaba:
366
+ os.environ.pop(blueprint.env_var, None)
367
+
368
+
369
+ def _views_dir(views_root: Path, blueprint: SchemaBlueprint) -> Path:
370
+ """
371
+ El directorio de las vistas: el mismo que resolverá `query()`.
372
+
373
+ `query()` busca en `views/<schema del datasource>/`, y el schema del datasource es el
374
+ que `pull` escribe (`blueprint.db_schema`). Con `''` el datasource cae en `'public'`,
375
+ y por eso también aquí.
376
+ """
377
+ return views_root / (blueprint.db_schema or 'public')
378
+
379
+
380
+ def _write_views(views_root: Path, blueprint: SchemaBlueprint) -> tuple[Path, ...]:
381
+ """Escribe el `.sql` de cada vista donde `query()` lo va a buscar."""
382
+ if not blueprint.views:
383
+ return ()
384
+
385
+ destino = _views_dir(views_root, blueprint)
386
+ destino.mkdir(parents=True, exist_ok=True)
387
+
388
+ escritas = []
389
+ for vista in blueprint.views:
390
+ ruta = destino / vista.sql_file
391
+ ruta.write_text(vista.sql + '\n', encoding='utf-8')
392
+ escritas.append(ruta)
393
+
394
+ return tuple(escritas)
395
+
396
+
397
+ def _env_var_for(source: str) -> str:
398
+ """
399
+ La variable de entorno que se escribirá en el fichero.
400
+
401
+ Nunca la cadena de conexión, aunque sea lo que se pasó: el schema se commitea, y una
402
+ contraseña dentro de él no se retira del historial. Con una cadena literal se usa el
403
+ nombre convencional, que el desarrollador cambia con `--env-var` si el suyo es otro.
404
+ """
405
+ return 'MAIN_DATABASE_URL' if '://' in source else source
406
+
407
+
408
+ def _write(pm, blueprint: SchemaBlueprint, contenido: str, fichero: Path) -> tuple[Path, ...]:
409
+ """Escribe el schema y los `.sql` de sus vistas."""
410
+ fichero.parent.mkdir(parents=True, exist_ok=True)
411
+ fichero.write_text(contenido, encoding='utf-8')
412
+ return _write_views(pm.paths.views_dir, blueprint)
413
+
414
+
415
+ __all__ = ['PullProgress', 'PullResult', 'run_pull', 'resolve_provider', 'resolve_db_schema', 'self_check']
@@ -16,6 +16,8 @@ Este módulo es la capa de presentación; el flujo está en `funcs.py`.
16
16
  from __future__ import annotations
17
17
 
18
18
  import sys
19
+ from contextlib import contextmanager
20
+ from typing import Iterator, Optional
19
21
 
20
22
  import click
21
23
 
@@ -61,22 +63,31 @@ from .funcs import run_pull
61
63
  def pull(provider, db_schema, schema_name, env_var, syntax, exclude, to_stdout, force):
62
64
  """Construye el schema a partir de una base de datos existente"""
63
65
 
64
- resultado = run_pull(
65
- source=provider,
66
- db_schema=db_schema,
67
- schema_name=schema_name,
68
- env_var=env_var,
69
- syntax=syntax,
70
- exclude=exclude,
71
- force=force,
72
- to_stdout=to_stdout,
73
- )
66
+ with _progress() as progreso:
67
+ resultado = run_pull(
68
+ source=provider,
69
+ db_schema=db_schema,
70
+ schema_name=schema_name,
71
+ env_var=env_var,
72
+ syntax=syntax,
73
+ exclude=exclude,
74
+ force=force,
75
+ to_stdout=to_stdout,
76
+ progress=progreso,
77
+ )
74
78
 
75
79
  if to_stdout:
76
80
  # El schema por stdout y el informe por stderr: así se puede canalizar el fichero
77
81
  # a un `diff` sin que los avisos lo ensucien.
78
82
  click.echo(resultado.content, nl=False)
79
83
  _report(resultado.blueprint, stream=sys.stderr)
84
+ if resultado.load_errors:
85
+ render_warning(
86
+ "Este schema NO carga tal cual, y sin --stdout no se habría escrito:\n"
87
+ + '\n'.join(f" · {e}" for e in resultado.load_errors),
88
+ hint="Es un fallo de tai-sql pull: repórtalo con este mensaje.",
89
+ stream=sys.stderr,
90
+ )
80
91
  return
81
92
 
82
93
  click.echo(f"✅ Schema escrito en {resultado.schema_file}")
@@ -92,6 +103,60 @@ def pull(provider, db_schema, schema_name, env_var, syntax, exclude, to_stdout,
92
103
  )
93
104
 
94
105
 
106
+ class _Spinner:
107
+ """
108
+ El progreso de `pull` en una línea que se reescribe: fase, consultas y tiempo.
109
+
110
+ El contador de consultas es la señal de vida. La reflexión es una sola llamada que no
111
+ dice por qué tabla va; que el número suba es lo que distingue un `pull` que trabaja de
112
+ uno colgado. Va por stderr, igual que el informe, para no mezclarse con `--stdout`.
113
+ """
114
+
115
+ def __init__(self, progress) -> None:
116
+ self._progress = progress
117
+ self._task = progress.add_task('Preparando…', total=None, queries=0)
118
+ self._queries = 0
119
+
120
+ def step(self, message: str) -> None:
121
+ self._progress.update(self._task, description=f'{message}…')
122
+
123
+ def query(self) -> None:
124
+ self._queries += 1
125
+ self._progress.update(self._task, queries=self._queries)
126
+
127
+
128
+ @contextmanager
129
+ def _progress() -> Iterator[Optional[_Spinner]]:
130
+ """
131
+ El spinner, solo si stderr es un terminal.
132
+
133
+ En un script o en CI no hay nadie mirando, y una línea que se reescribe acaba en el log
134
+ como basura. Sin terminal, `run_pull` recibe `None` y no cuenta nada. `transient` borra
135
+ la línea al terminar: lo que queda en pantalla es el resultado, no el andamiaje.
136
+
137
+ Se pregunta a `isatty()` y no a `Console.is_terminal`, que con `FORCE_COLOR` dice que sí
138
+ a cualquier salida: poder pintar colores no significa que haya alguien mirando.
139
+ """
140
+ if not sys.stderr.isatty():
141
+ yield None
142
+ return
143
+
144
+ from rich.console import Console
145
+ from rich.progress import Progress, SpinnerColumn, TextColumn, TimeElapsedColumn
146
+
147
+ consola = Console(file=sys.stderr)
148
+
149
+ with Progress(
150
+ SpinnerColumn(),
151
+ TextColumn('{task.description}'),
152
+ TextColumn('[dim]{task.fields[queries]} consultas[/dim]'),
153
+ TimeElapsedColumn(),
154
+ console=consola,
155
+ transient=True,
156
+ ) as progress:
157
+ yield _Spinner(progress)
158
+
159
+
95
160
  def _resumen(blueprint: SchemaBlueprint) -> None:
96
161
  """Qué se ha reconstruido, en una línea por tipo de objeto."""
97
162
  click.echo(
@@ -19,7 +19,14 @@ from typing import Optional, Sequence
19
19
 
20
20
  import click
21
21
 
22
- from tai_sql.rules import RuleState, current_version, installer_for, schema_context
22
+ from tai_sql.rules import (
23
+ RuleState,
24
+ current_version,
25
+ installer_for,
26
+ legacy_rules_dir,
27
+ retire_directory,
28
+ schema_context,
29
+ )
23
30
 
24
31
  from ...preamble import available_schemas, load_schema, resolve_schema_name
25
32
 
@@ -53,7 +60,9 @@ def _contexts(names: Sequence[str]) -> list:
53
60
  contextos = []
54
61
  for nombre in names:
55
62
  manager = load_schema(nombre)
56
- contextos.append(schema_context(nombre, manager, pm.paths.root))
63
+ contextos.append(
64
+ schema_context(nombre, manager, pm.paths.root, pm.paths.workspace_root)
65
+ )
57
66
  return contextos
58
67
 
59
68
 
@@ -67,6 +76,44 @@ def _target_dir():
67
76
  return pm.paths.platform_rules_dir
68
77
 
69
78
 
79
+ def _retire_legacy() -> None:
80
+ """
81
+ Retira las reglas de la ubicación de antes de la 0.7.12, dentro del proyecto.
82
+
83
+ Allí el asistente solo las cargaba al leer un fichero de esa carpeta, y dejarlas
84
+ tendría dos copias de versiones distintas diciendo cosas distintas.
85
+ """
86
+ from tai_sql import pm
87
+
88
+ antigua = legacy_rules_dir(pm.paths)
89
+ if antigua is None:
90
+ return
91
+ retirada = retire_directory(antigua, stop=pm.paths.root)
92
+ for nombre in retirada.removed:
93
+ click.echo(f" 🗑️ {nombre} (de la ubicación antigua)")
94
+ if retirada.kept:
95
+ click.echo()
96
+ click.echo(
97
+ f"⚠️ En {antigua} quedan {', '.join(retirada.kept)}, que alguien editó: no se "
98
+ f"han borrado. Las reglas están ahora en {pm.paths.platform_rules_dir}; "
99
+ f"borra esa carpeta cuando hayas recuperado lo que necesites."
100
+ )
101
+ click.echo()
102
+
103
+
104
+ def _legacy_reason() -> str:
105
+ """Por qué la ubicación antigua desalinea, o cadena vacía si no hay nada allí."""
106
+ from tai_sql import pm
107
+
108
+ antigua = legacy_rules_dir(pm.paths)
109
+ if antigua is None:
110
+ return ''
111
+ return (
112
+ f"hay reglas en {antigua}, la ubicación de antes de la 0.7.12, que el asistente "
113
+ f"no carga al arrancar"
114
+ )
115
+
116
+
70
117
  @click.group()
71
118
  def rules():
72
119
  """Gestiona las reglas de contexto para asistentes de IA"""
@@ -96,6 +143,7 @@ def rules_install(schema: Optional[str], force: bool):
96
143
  parcial = bool(schema)
97
144
  instalador = installer_for(destino, _contexts(nombres), track_orphans=not parcial)
98
145
  resultado = instalador.install(force=force)
146
+ _retire_legacy()
99
147
 
100
148
  for nombre in resultado.written:
101
149
  click.echo(f" ✅ {nombre}")
@@ -159,11 +207,13 @@ def rules_check(schema: Optional[str]):
159
207
  destino, _contexts(nombres), track_orphans=not parcial
160
208
  ).inspect()
161
209
 
162
- if informe.aligned:
210
+ antigua = _legacy_reason()
211
+ if informe.aligned and not antigua:
163
212
  click.echo(f"✅ Las reglas están al día (tai-sql {informe.version})")
164
213
  return
165
214
 
166
- click.echo(f"❌ Las reglas no están al día: {informe.reason()}")
215
+ motivo = '; '.join(m for m in (informe.reason(), antigua) if m)
216
+ click.echo(f"❌ Las reglas no están al día: {motivo}")
167
217
  click.echo()
168
218
 
169
219
  if informe.installed and informe.installed_version:
@@ -208,6 +258,11 @@ def rules_diff(schema: Optional[str]):
208
258
  destino, _contexts(nombres), track_orphans=not parcial
209
259
  ).inspect()
210
260
 
261
+ antigua = _legacy_reason()
262
+ if antigua:
263
+ click.echo(f"ℹ️ Además, {antigua}: 'tai-sql rules install' las retira.")
264
+ click.echo()
265
+
211
266
  if informe.aligned:
212
267
  click.echo(f"✅ No hay diferencias (tai-sql {informe.version})")
213
268
  return