tai-sql 0.7.11__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 (227) hide show
  1. {tai_sql-0.7.11 → tai_sql-0.7.12}/PKG-INFO +1 -1
  2. {tai_sql-0.7.11 → tai_sql-0.7.12}/pyproject.toml +1 -1
  3. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_pull/funcs.py +79 -3
  4. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_pull/main.py +68 -10
  5. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_rules/main.py +59 -4
  6. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/generators/python_client/templates/base.py.jinja2 +20 -0
  7. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/generators/python_client/templates/domain/dao.py.jinja2 +3 -0
  8. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/generators/python_client/templates/facade.py.jinja2 +2 -0
  9. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/generators/python_client/triggers/transformer.py +8 -5
  10. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/generators/rules/__init__.py +6 -0
  11. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/generators/rules/templates/schema.md.jinja2 +6 -0
  12. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/introspect/README.md +3 -0
  13. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/introspect/build.py +19 -2
  14. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/project/paths.py +37 -6
  15. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/rules/README.md +19 -1
  16. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/rules/__init__.py +12 -0
  17. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/rules/catalog.py +26 -9
  18. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/rules/installer.py +79 -5
  19. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/rules/service.py +41 -1
  20. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/rules/staleness.py +7 -3
  21. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/rules/static/comandos.md +11 -2
  22. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/rules/static/modelo.md +5 -0
  23. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/sync/README.md +1 -1
  24. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/sync/diff/__init__.py +3 -1
  25. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/sync/diff/tables.py +6 -3
  26. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/sync/ownership.py +25 -1
  27. {tai_sql-0.7.11 → tai_sql-0.7.12}/LICENSE +0 -0
  28. {tai_sql-0.7.11 → tai_sql-0.7.12}/README.md +0 -0
  29. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/__init__.py +0 -0
  30. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/cli/commands/__init__.py +0 -0
  31. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_backup/__init__.py +0 -0
  32. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_backup/main.py +0 -0
  33. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_deploy/__init__.py +0 -0
  34. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_deploy/config.py +0 -0
  35. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_deploy/local.py +0 -0
  36. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_deploy/main.py +0 -0
  37. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_deploy/workflow.py +0 -0
  38. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_feed/__init__.py +0 -0
  39. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_feed/main.py +0 -0
  40. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_generate/__init__.py +0 -0
  41. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_generate/funcs.py +0 -0
  42. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_generate/main.py +0 -0
  43. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_hash_migrate/__init__.py +0 -0
  44. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_hash_migrate/main.py +0 -0
  45. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_info/__init__.py +0 -0
  46. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_info/main.py +0 -0
  47. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_info/status.py +0 -0
  48. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_init/__init__.py +0 -0
  49. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_init/main.py +0 -0
  50. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_init/model.py +0 -0
  51. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_install/__init__.py +0 -0
  52. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_install/main.py +0 -0
  53. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_ping/__init__.py +0 -0
  54. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_ping/main.py +0 -0
  55. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_pull/__init__.py +0 -0
  56. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_restore/__init__.py +0 -0
  57. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_restore/main.py +0 -0
  58. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_rules/__init__.py +0 -0
  59. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_schema/__init__.py +0 -0
  60. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_schema/model.py +0 -0
  61. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_schema/new.py +0 -0
  62. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_schema/schemas/__init__.py +0 -0
  63. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_schema/schemas/auth.py +0 -0
  64. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_schema/schemas/base.py +0 -0
  65. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_schema/schemas/chatbot.py +0 -0
  66. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_schema/schemas/default.py +0 -0
  67. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_schema/set_default.py +0 -0
  68. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/cli/commands/prompts.py +0 -0
  69. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/cli/commands/utils/__init__.py +0 -0
  70. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/cli/commands/utils/connectivity.py +0 -0
  71. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/cli/commands/utils/create.py +0 -0
  72. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/cli/commands/utils/github/__init__.py +0 -0
  73. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/cli/commands/utils/github/auth.py +0 -0
  74. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/cli/commands/utils/github/client.py +0 -0
  75. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/cli/commands/utils/github/models.py +0 -0
  76. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/cli/errors.py +0 -0
  77. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/cli/main.py +0 -0
  78. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/cli/preamble.py +0 -0
  79. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/cli/services/__init__.py +0 -0
  80. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/cli/services/backup.py +0 -0
  81. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/connection/README.md +0 -0
  82. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/connection/__init__.py +0 -0
  83. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/connection/provider.py +0 -0
  84. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/connection/recipe.py +0 -0
  85. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/connection/session.py +0 -0
  86. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/connection/sources.py +0 -0
  87. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/crypto.py +0 -0
  88. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/drivers/__init__.py +0 -0
  89. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/drivers/base.py +0 -0
  90. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/drivers/bigquery.py +0 -0
  91. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/drivers/capabilities.py +0 -0
  92. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/drivers/mysql.py +0 -0
  93. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/drivers/postgresql.py +0 -0
  94. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/drivers/sqlserver.py +0 -0
  95. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/errors/README.md +0 -0
  96. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/errors/__init__.py +0 -0
  97. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/errors/collector.py +0 -0
  98. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/errors/render.py +0 -0
  99. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/errors/types.py +0 -0
  100. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/feed/README.md +0 -0
  101. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/feed/__init__.py +0 -0
  102. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/feed/collect.py +0 -0
  103. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/feed/executor.py +0 -0
  104. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/feed/feeder.py +0 -0
  105. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/feed/plan.py +0 -0
  106. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/feed/presenter.py +0 -0
  107. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/feed/records.py +0 -0
  108. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/feed/transform.py +0 -0
  109. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/generators/__init__.py +0 -0
  110. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/generators/base.py +0 -0
  111. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/generators/diagram/README.md +0 -0
  112. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/generators/diagram/__init__.py +0 -0
  113. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/generators/diagram/assets/README.md +0 -0
  114. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/generators/diagram/assets/cytoscape.min.js +0 -0
  115. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/generators/diagram/builder.py +0 -0
  116. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/generators/diagram/generator.py +0 -0
  117. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/generators/diagram/graphviz_render.py +0 -0
  118. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/generators/diagram/html_render.py +0 -0
  119. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/generators/diagram/layout.py +0 -0
  120. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/generators/diagram/model.py +0 -0
  121. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/generators/diagram/templates/er.html.jinja2 +0 -0
  122. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/generators/diagram/themes.py +0 -0
  123. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/generators/python_client/__init__.py +0 -0
  124. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/generators/python_client/calculated.py +0 -0
  125. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/generators/python_client/context.py +0 -0
  126. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/generators/python_client/templates/domain/__init__.py.jinja2 +0 -0
  127. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/generators/python_client/templates/domain/dtos.py.jinja2 +0 -0
  128. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/generators/python_client/templates/domain/model.py.jinja2 +0 -0
  129. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/generators/python_client/templates/macros/aggregation.jinja2 +0 -0
  130. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/generators/python_client/templates/macros/dataframe.jinja2 +0 -0
  131. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/generators/python_client/templates/macros/filters.jinja2 +0 -0
  132. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/generators/python_client/templates/macros/hierarchy.jinja2 +0 -0
  133. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/generators/python_client/templates/macros/read.jinja2 +0 -0
  134. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/generators/python_client/templates/macros/triggers.jinja2 +0 -0
  135. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/generators/python_client/templates/macros/upsert.jinja2 +0 -0
  136. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/generators/python_client/templates/macros/vector.jinja2 +0 -0
  137. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/generators/python_client/templates/macros/write.jinja2 +0 -0
  138. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/generators/python_client/templates/session.py.jinja2 +0 -0
  139. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/generators/python_client/templates/shared/__init__.py.jinja2 +0 -0
  140. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/generators/python_client/templates/shared/aggregation.py.jinja2 +0 -0
  141. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/generators/python_client/templates/shared/context.py.jinja2 +0 -0
  142. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/generators/python_client/templates/shared/utils/__init__.py.jinja2 +0 -0
  143. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/generators/python_client/templates/shared/utils/encoders.py.jinja2 +0 -0
  144. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/generators/python_client/templates/shared/utils/funcs.py.jinja2 +0 -0
  145. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/generators/python_client/templates/shared/utils/hierarchy.py.jinja2 +0 -0
  146. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/generators/python_client/templates/shared/utils/rls.py.jinja2 +0 -0
  147. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/generators/python_client/templates/shared/vector.py.jinja2 +0 -0
  148. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/generators/python_client/triggers/__init__.py +0 -0
  149. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/generators/python_client/triggers/analyzer.py +0 -0
  150. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/generators/python_client/triggers/renderer.py +0 -0
  151. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/generators/python_client/triggers/steps.py +0 -0
  152. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/introspect/__init__.py +0 -0
  153. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/introspect/blueprint.py +0 -0
  154. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/introspect/defaults.py +0 -0
  155. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/introspect/naming.py +0 -0
  156. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/introspect/reader.py +0 -0
  157. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/introspect/templates/schema.py.jinja2 +0 -0
  158. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/introspect/types.py +0 -0
  159. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/introspect/writer.py +0 -0
  160. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/orm/README.md +0 -0
  161. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/orm/__init__.py +0 -0
  162. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/orm/analysis/README.md +0 -0
  163. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/orm/analysis/__init__.py +0 -0
  164. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/orm/analysis/calculated.py +0 -0
  165. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/orm/analysis/collector.py +0 -0
  166. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/orm/analysis/column_analyzer.py +0 -0
  167. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/orm/analysis/constraints.py +0 -0
  168. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/orm/analysis/hints.py +0 -0
  169. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/orm/analysis/relation_analyzer.py +0 -0
  170. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/orm/analysis/schema_analyzer.py +0 -0
  171. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/orm/declarative/README.md +0 -0
  172. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/orm/declarative/__init__.py +0 -0
  173. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/orm/declarative/annotations.py +0 -0
  174. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/orm/declarative/base.py +0 -0
  175. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/orm/declarative/calculated.py +0 -0
  176. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/orm/declarative/columns.py +0 -0
  177. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/orm/declarative/constraints.py +0 -0
  178. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/orm/declarative/datasource.py +0 -0
  179. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/orm/declarative/descriptors.py +0 -0
  180. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/orm/declarative/encoders.py +0 -0
  181. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/orm/declarative/enumerations.py +0 -0
  182. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/orm/declarative/relations.py +0 -0
  183. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/orm/declarative/table.py +0 -0
  184. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/orm/declarative/trigger_api.py +0 -0
  185. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/orm/declarative/triggers.py +0 -0
  186. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/orm/declarative/view.py +0 -0
  187. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/orm/mapping/README.md +0 -0
  188. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/orm/mapping/__init__.py +0 -0
  189. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/orm/mapping/sqlalchemy_mapper.py +0 -0
  190. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/orm/model/README.md +0 -0
  191. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/orm/model/__init__.py +0 -0
  192. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/orm/model/column.py +0 -0
  193. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/orm/model/constraints.py +0 -0
  194. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/orm/model/entity.py +0 -0
  195. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/orm/model/enumeration.py +0 -0
  196. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/orm/model/relation.py +0 -0
  197. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/orm/model/schema.py +0 -0
  198. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/orm/model/trigger.py +0 -0
  199. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/orm/naming.py +0 -0
  200. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/orm/ordering.py +0 -0
  201. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/orm/registry.py +0 -0
  202. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/orm/types.py +0 -0
  203. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/project/README.md +0 -0
  204. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/project/__init__.py +0 -0
  205. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/project/config.py +0 -0
  206. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/project/context.py +0 -0
  207. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/project/manager.py +0 -0
  208. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/rules/meta.py +0 -0
  209. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/rules/static/errores.md +0 -0
  210. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/schema.py +0 -0
  211. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/sync/__init__.py +0 -0
  212. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/sync/diff/constraints.py +0 -0
  213. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/sync/diff/indexes.py +0 -0
  214. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/sync/diff/views.py +0 -0
  215. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/sync/executor.py +0 -0
  216. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/sync/plan/__init__.py +0 -0
  217. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/sync/plan/planner.py +0 -0
  218. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/sync/plan/statements.py +0 -0
  219. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/sync/presenter.py +0 -0
  220. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/sync/report.py +0 -0
  221. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/sync/safety.py +0 -0
  222. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/sync/state/__init__.py +0 -0
  223. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/sync/state/local.py +0 -0
  224. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/sync/state/model.py +0 -0
  225. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/sync/state/normalize.py +0 -0
  226. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/sync/state/remote.py +0 -0
  227. {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/sync/synchronizer.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tai-sql
3
- Version: 0.7.11
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.11"
3
+ version = "0.7.12"
4
4
  description = "SQL database management and code generation tool"
5
5
  license = "MIT"
6
6
  license-files = ["LICENSE"]
@@ -14,11 +14,12 @@ from __future__ import annotations
14
14
 
15
15
  import os
16
16
  import tempfile
17
+ from contextlib import contextmanager
17
18
  from dataclasses import dataclass
18
19
  from pathlib import Path
19
- from typing import Optional, Sequence
20
+ from typing import Iterator, Optional, Protocol, Sequence
20
21
 
21
- from sqlalchemy import inspect
22
+ from sqlalchemy import Engine, event, inspect
22
23
 
23
24
  from tai_sql.connection import Provider
24
25
  from tai_sql.errors import ErrorCode, ProjectError, TaiSqlError
@@ -47,6 +48,52 @@ class PullResult:
47
48
  load_errors: tuple[str, ...] = ()
48
49
 
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
+
50
97
  def resolve_provider(source: str) -> Provider:
51
98
  """
52
99
  El proveedor de conexión, a partir de lo que se pasó en `-p/--provider`.
@@ -114,6 +161,7 @@ def run_pull(
114
161
  exclude: Sequence[str] = (),
115
162
  force: bool = False,
116
163
  to_stdout: bool = False,
164
+ progress: Optional[PullProgress] = None,
117
165
  ) -> PullResult:
118
166
  """
119
167
  Construye el fichero de schema que describe una base de datos.
@@ -128,6 +176,7 @@ def run_pull(
128
176
  exclude: Tablas que no se declaran.
129
177
  force: Sobrescribir un fichero de schema existente.
130
178
  to_stdout: No escribir nada; devolver el contenido.
179
+ progress: A quién contarle en qué fase va. Ver `PullProgress`.
131
180
 
132
181
  Returns:
133
182
  El resultado, con el blueprint para poder informar de los descartes.
@@ -158,6 +207,25 @@ def run_pull(
158
207
  code=ErrorCode.INVALID_PROVIDER,
159
208
  )
160
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}')
161
229
  destino_db = resolve_db_schema(provider, db_schema)
162
230
  nombre = schema_name or destino_db or 'main'
163
231
 
@@ -176,13 +244,18 @@ def run_pull(
176
244
  code=ErrorCode.DUPLICATE_NAME,
177
245
  )
178
246
 
247
+ progress.step(f"Leyendo el catálogo de '{destino_db}'")
179
248
  estado, _ = build_remote_state(
180
249
  provider.engine, driver, destino_db or None, target=nombre
181
250
  )
251
+
252
+ if estado.views:
253
+ progress.step(f'Leyendo {len(estado.views)} vista(s)')
182
254
  columnas_vista = read_view_columns(
183
255
  provider.engine, driver, destino_db or None, list(estado.views)
184
256
  )
185
257
 
258
+ progress.step(f'Deduciendo el schema ({len(estado.tables)} tablas)')
186
259
  blueprint = build(
187
260
  estado,
188
261
  driver,
@@ -195,6 +268,8 @@ def run_pull(
195
268
  )
196
269
 
197
270
  contenido = render(blueprint)
271
+
272
+ progress.step('Comprobando que el schema carga')
198
273
  errores = self_check(blueprint, provider)
199
274
 
200
275
  if to_stdout:
@@ -215,6 +290,7 @@ def run_pull(
215
290
  code=ErrorCode.SCHEMA_IMPORT_FAILED,
216
291
  )
217
292
 
293
+ progress.step('Escribiendo el schema')
218
294
  escritas = _write(pm, blueprint, contenido, fichero)
219
295
 
220
296
  return PullResult(
@@ -336,4 +412,4 @@ def _write(pm, blueprint: SchemaBlueprint, contenido: str, fichero: Path) -> tup
336
412
  return _write_views(pm.paths.views_dir, blueprint)
337
413
 
338
414
 
339
- __all__ = ['PullResult', 'run_pull', 'resolve_provider', 'resolve_db_schema', 'self_check']
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,16 +63,18 @@ 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
@@ -99,6 +103,60 @@ def pull(provider, db_schema, schema_name, env_var, syntax, exclude, to_stdout,
99
103
  )
100
104
 
101
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
+
102
160
  def _resumen(blueprint: SchemaBlueprint) -> None:
103
161
  """Qué se ha reconstruido, en una línea por tipo de objeto."""
104
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
@@ -18,6 +18,26 @@ import bcrypt
18
18
  class Base(DeclarativeBase):
19
19
  pass
20
20
 
21
+
22
+ class TriggerAborted(ValueError):
23
+ """
24
+ Un trigger del schema ha detenido la operación con `t.abort(mensaje)`.
25
+
26
+ Es una regla de negocio que no se cumple, no un fallo: quien la reciba tiene que poder
27
+ decírselo al usuario tal cual. `message` es el texto que escribió el trigger, sin prefijo.
28
+
29
+ Hereda de `ValueError` porque es lo que se lanzaba antes, y el código que ya la captura
30
+ así sigue funcionando.
31
+ """
32
+
33
+ #: Para reconocerla sin importar la clase: tai-api atiende a los clientes de varios schemas
34
+ #: con un solo manejador, y cada uno tiene la suya.
35
+ tai_trigger_abort = True
36
+
37
+ def __init__(self, message: str):
38
+ self.message = message
39
+ super().__init__(f"Trigger abortado: {message}")
40
+
21
41
  {% if has_encrypted_columns %}
22
42
 
23
43
  # Configuración de encriptación
@@ -88,6 +88,9 @@ from {{ import_prefix }}_session import {{ session_manager_var }}
88
88
  {% if model.columns | selectattr('hash') | list %}
89
89
  from {{ import_prefix }}_base import verify_hash, HASH_DE_RELLENO
90
90
  {% endif %}
91
+ {% if model.triggers is defined and (model.triggers['create'] or model.triggers['update'] or model.triggers['delete']) %}
92
+ from {{ import_prefix }}_base import TriggerAborted
93
+ {% endif %}
91
94
  {# ── Collect cross-DAO targets (relations + trigger cross-table ops) ── #}
92
95
  {% set _cross_dao_targets = [] %}
93
96
  {% for rel in model.relations | default([]) %}
@@ -24,6 +24,7 @@ from .{{ model.tablename }}{{ cross_domain_infix }}.{{ dao_sync_module }} import
24
24
  from .{{ model.tablename }}{{ cross_domain_infix }}.{{ dao_async_module }} import {{ model.name }}AsyncDAO
25
25
  {% endif %}
26
26
  {% endfor %}
27
+ from ._base import TriggerAborted
27
28
  from ._shared import (
28
29
  AggregationResult,
29
30
  AggRow,
@@ -159,6 +160,7 @@ __all__ = [
159
160
  'set_username',
160
161
  'get_username',
161
162
  'username_context',
163
+ 'TriggerAborted',
162
164
  'AggregationResult',
163
165
  'AggRow',
164
166
  'AggRequest',
@@ -11,7 +11,7 @@ Reescribe el AST de la función del trigger sustituyendo la API declarativa
11
11
  t.old → old_<tablename>
12
12
  self.campo → new_<tablename>.campo (create/update)
13
13
  t.log(msg) → logger.info("🔔 Trigger: ...")
14
- t.abort(msg) → raise ValueError("Trigger abortado: ...")
14
+ t.abort(msg) → raise TriggerAborted(msg) (un ValueError; ver _base.py)
15
15
  t.update(Model, pk, **v) → {Model}SyncDAO(...).update(...)
16
16
 
17
17
  Vive aquí y no en el ORM porque conoce los nombres de los artefactos generados
@@ -44,7 +44,7 @@ class TriggerCodeTransformer(ast.NodeTransformer):
44
44
 
45
45
  Mapeo de métodos:
46
46
  t.log(msg) → logger.info("🔔 Trigger: ...")
47
- t.abort(msg) → raise ValueError("Trigger abortado: ...")
47
+ t.abort(msg) → raise TriggerAborted(msg)
48
48
 
49
49
  Operaciones cross-table:
50
50
  t.update(Model, pk, **values) → {Model}{Mode}DAO(self.session_manager).update(pk, updated_values={Model}UpdateValues(**values), session=session)
@@ -266,13 +266,16 @@ class TriggerCodeTransformer(ast.NodeTransformer):
266
266
  return ast.copy_location(ast.Expr(value=new_call), node)
267
267
 
268
268
  elif method == 'abort':
269
+ # Una excepción propia y no un `ValueError` con prefijo: quien la recibe —tai-api,
270
+ # que la convierte en un 422 BUSINESS_RULE_VIOLATION— tiene que poder distinguir una
271
+ # regla de negocio de un fallo, y darle al usuario el mensaje que escribió el trigger.
272
+ # El prefijo lo pone la propia excepción al convertirse en texto.
269
273
  msg = call.args[0] if call.args else ast.Constant("")
270
- prefixed = self._prepend_to_msg("Trigger abortado: ", msg)
271
274
  return ast.copy_location(
272
275
  ast.Raise(
273
276
  exc=ast.Call(
274
- func=ast.Name(id='ValueError', ctx=ast.Load()),
275
- args=[prefixed], keywords=[]
277
+ func=ast.Name(id='TriggerAborted', ctx=ast.Load()),
278
+ args=[msg], keywords=[]
276
279
  ),
277
280
  cause=None
278
281
  ), node
@@ -71,6 +71,10 @@ class SchemaRulesContext:
71
71
  máquina y rompería la comparación de `rules check`.
72
72
  root: Raíz del proyecto, para expresar en relativo las rutas de salida de
73
73
  los generadores. No se renderiza.
74
+ project_dir: Dónde está el proyecto de tai-sql **dentro del repositorio**
75
+ (`database`), o `.` si es la raíz. Las reglas se instalan en la raíz
76
+ del repositorio y las rutas de este documento son relativas al
77
+ proyecto, así que el asistente necesita saber desde dónde leerlas.
74
78
  """
75
79
 
76
80
  schema: SchemaModel
@@ -79,6 +83,7 @@ class SchemaRulesContext:
79
83
  generators: tuple[Any, ...] = ()
80
84
  file: Optional[str] = None
81
85
  root: Optional[str] = None
86
+ project_dir: Optional[str] = None
82
87
 
83
88
 
84
89
  # ════════════════════════════════════════════════
@@ -302,6 +307,7 @@ def _schema_context(context: SchemaRulesContext) -> dict:
302
307
  'name': schema.name,
303
308
  'key': context.key or schema.name,
304
309
  'file': context.file or '',
310
+ 'project_dir': context.project_dir or '',
305
311
  'syntax': schema.syntax,
306
312
  'operative_fields': schema.operative_fields,
307
313
  'secret_key_name': schema.secret_key_name,
@@ -20,7 +20,13 @@ otros ficheros de este directorio.
20
20
  {% for schema in schemas %}
21
21
  ## Schema `{{ schema.key }}`
22
22
 
23
+ {% if schema.project_dir and schema.project_dir != '.' %}
24
+ - **Proyecto de tai-sql**: `{{ schema.project_dir }}/` (el `.taisqlproject`). Las rutas de
25
+ este apartado son relativas a él, y los comandos `tai-sql` se ejecutan desde ahí
23
26
  - **Fichero**: `{{ schema.file }}` — es el nombre que toma `--schema`
27
+ {% else %}
28
+ - **Fichero**: `{{ schema.file }}` — es el nombre que toma `--schema`
29
+ {% endif %}
24
30
  {% if schema.name != schema.key %}
25
31
  - **Nombre en la base de datos**: `{{ schema.name }}` (lo declara `datasource(schema=...)`)
26
32
  {% else %}
@@ -80,6 +80,9 @@ en MySQL, donde vive dentro de la definición de la columna.
80
80
  `datasource(schema='mi_bd')`, que es lo que declara cualquier schema de MySQL y el prefijo
81
81
  que usa el DDL. Antes se escribía `''`, que acababa en `'public'`, y nada cargaba. El SQL
82
82
  de las vistas se escribe **sin** ese prefijo, que el catálogo añade.
83
+ - **Una tabla sin clave primaria no se declara, pero se cuenta.** El análisis la
84
+ rechaza, así que declararla daba un schema que no cargaba. Tampoco es de tai-sql, y el
85
+ drift no la elimina (`ownership.is_keyless`).
83
86
  - **Lo que es de otra herramienta no se declara, pero se cuenta.** La lista está en
84
87
  `sync/ownership.py` (sistemas de migraciones, catálogo de PostGIS), y el drift la usa para
85
88
  no eliminarlas nunca. Antes se ocultaban sin avisar y el push las borraba.
@@ -20,7 +20,7 @@ from typing import Mapping, Optional, Sequence
20
20
  from ..drivers import DatabaseDriver
21
21
  from ..orm.analysis.column_analyzer import OPERATIVE_FIELDS
22
22
  from ..orm.ordering import topological_order
23
- from ..sync.ownership import FOREIGN_PREFIXES, FOREIGN_TABLES, is_foreign_table
23
+ from ..sync.ownership import FOREIGN_PREFIXES, FOREIGN_TABLES, is_foreign_table, is_keyless
24
24
  from ..sync.state import ColumnState, IndexState, SchemaState, TableState
25
25
  from ..sync.state.normalize import is_numeric_type
26
26
  from . import defaults as dfl
@@ -100,6 +100,21 @@ def build(
100
100
  reason='es de otra herramienta (migraciones, extensión): no se declara y push no la toca',
101
101
  ))
102
102
 
103
+ # Sin clave primaria no hay tabla que declarar: el análisis la rechaza, y un schema
104
+ # con ella no cargaría. Tampoco es de tai-sql —no ha podido crearla—, así que el
105
+ # drift no la elimina: se deja fuera, se cuenta y sobrevive intacta.
106
+ for fisico in sorted(n for n, s in tablas_estado.items() if is_keyless(s)):
107
+ descartes.append(Discarded(
108
+ kind='tabla', location=fisico, detail=f'tabla {fisico}',
109
+ impact=Impact.IGNORED,
110
+ reason=(
111
+ 'no tiene clave primaria, que el DSL exige: no se declara y push no la '
112
+ 'toca. Para gestionarla con tai-sql, dale una clave primaria en la base '
113
+ 'de datos y vuelve a ejecutar pull'
114
+ ),
115
+ ))
116
+ del tablas_estado[fisico]
117
+
103
118
  for tabla in sorted(excluidas & set(tablas_estado)):
104
119
  descartes.append(Discarded(
105
120
  kind='tabla', location=tabla, detail=f'tabla {tabla}',
@@ -108,7 +123,9 @@ def build(
108
123
  ))
109
124
  del tablas_estado[tabla]
110
125
 
111
- for tabla in sorted(excluidas - set(tablas_estado) - set(state.tables)):
126
+ # Por nombre, no por clave: las claves de `state.tables` llevan el schema delante.
127
+ existentes = {s.name for s in state.tables.values()}
128
+ for tabla in sorted(excluidas - existentes):
112
129
  descartes.append(Discarded(
113
130
  kind='tabla', location=tabla, detail=f'tabla {tabla}',
114
131
  impact=Impact.IGNORED,
@@ -58,23 +58,54 @@ class ProjectPaths:
58
58
  """Ruta del fichero de un schema, exista o no."""
59
59
  return self.schemas_dir / f'{schema_name}.py'
60
60
 
61
+ @property
62
+ def workspace_root(self) -> Path:
63
+ """
64
+ La raíz del repositorio que contiene el proyecto: el primer directorio, desde
65
+ `root` hacia arriba, que tiene un `.git`. Sin repositorio, el propio proyecto.
66
+
67
+ Es donde se arranca el asistente, y por eso es donde tienen que estar sus reglas.
68
+ El proyecto de tai-sql suele ser una carpeta del repositorio (`database/`, junto a
69
+ `api/` y al frontend), y un asistente **no carga al arrancar** el `.claude/rules/`
70
+ de una subcarpeta: solo cuando lee un fichero de dentro. Las reglas instaladas en
71
+ `database/.claude/rules/` no llegaban a quien trabajaba en la API o en el
72
+ frontend, que es justo quien más las necesita.
73
+ """
74
+ inicio = self.root.resolve()
75
+ for directorio in (inicio, *inicio.parents):
76
+ # `exists()` y no `is_dir()`: en un worktree o un submódulo `.git` es un fichero.
77
+ if (directorio / '.git').exists():
78
+ return directorio
79
+ return inicio
80
+
61
81
  @property
62
82
  def rules_dir(self) -> Path:
63
- """Directorio de reglas de contexto del proyecto (`.claude/rules`)."""
64
- return self.root / self.RULES_DIR
83
+ """Directorio de reglas de contexto del repositorio (`<raíz>/.claude/rules`)."""
84
+ return self.workspace_root / self.RULES_DIR
65
85
 
66
86
  @property
67
87
  def platform_rules_dir(self) -> Path:
68
88
  """
69
89
  Directorio donde `tai-sql rules install` escribe las reglas de la herramienta.
70
90
 
71
- Es `.claude/rules/_platform/tai-sql`, y el `_platform/` no es organización: es la
72
- frontera entre lo que instala tai-sql —borrable y reinstalable— y lo que escribe
73
- el equipo. Sin ella se reproduce el problema del código generado: alguien edita,
74
- la siguiente instalación lo pisa, y nadie se entera.
91
+ Es `<raíz del repositorio>/.claude/rules/_platform/tai-sql`, y el `_platform/` no es
92
+ organización: es la frontera entre lo que instala tai-sql —borrable y
93
+ reinstalable— y lo que escribe el equipo. Sin ella se reproduce el problema del
94
+ código generado: alguien edita, la siguiente instalación lo pisa, y nadie se
95
+ entera. Ver `workspace_root` para por qué la raíz del repositorio y no el proyecto.
75
96
  """
76
97
  return self.rules_dir / self.PLATFORM_RULES_DIR
77
98
 
99
+ @property
100
+ def legacy_platform_rules_dir(self) -> Optional[Path]:
101
+ """
102
+ Donde las instalaban las versiones anteriores a la 0.7.12: dentro del proyecto de
103
+ tai-sql. `None` si coincide con la ubicación actual —el proyecto es la raíz del
104
+ repositorio— y no hay nada que retirar.
105
+ """
106
+ antigua = self.root.resolve() / self.RULES_DIR / self.PLATFORM_RULES_DIR
107
+ return None if antigua == self.platform_rules_dir else antigua
108
+
78
109
  # ── Descubrimiento ───────────────────────────────────────────────────
79
110
 
80
111
  def discover_schemas(self) -> List[str]:
@@ -37,7 +37,7 @@ texto, que es la forma preferente de añadir cobertura aquí.
37
37
  ## Dónde se instalan, y por qué ahí
38
38
 
39
39
  ```
40
- .claude/rules/
40
+ <raíz del repositorio>/.claude/rules/
41
41
  ├── _platform/ instalado — NO se edita, es borrable y reinstalable
42
42
  │ └── tai-sql/
43
43
  │ ├── _meta.json
@@ -52,6 +52,24 @@ La separación física de `_platform/` es un requisito y no organización: sin e
52
52
  reproduce el problema que ya tiene el código generado — alguien edita, la siguiente
53
53
  instalación lo pisa, y nadie se entera.
54
54
 
55
+ **En la raíz del repositorio, no en el proyecto de tai-sql** (`ProjectPaths.workspace_root`:
56
+ el primer directorio hacia arriba con `.git`; sin repositorio, el proyecto). El proyecto suele
57
+ ser una carpeta —`database/`, junto a `api/` y al frontend—, y el asistente carga al arrancar el
58
+ `.claude/rules/` de la raíz pero el de una subcarpeta **solo cuando lee un fichero de dentro**
59
+ (comprobado con Claude Code 2.1). Hasta la 0.7.11 se instalaban en el proyecto, y quien
60
+ trabajaba en la API no las veía. `rules install` retira lo que quede allí con
61
+ `retire_directory()`, que borra lo intacto y deja lo editado; `check` y el aviso de `push` lo
62
+ señalan mientras quede algo. El documento derivado dice dónde está el proyecto dentro del
63
+ repositorio, porque sus rutas son relativas a él.
64
+
65
+ ## Reutilizarlo desde otra librería
66
+
67
+ `RulesInstaller` no sabe de schemas, y tampoco de quién es: `tool='tai-api'` hace que la
68
+ cabecera diga `<!-- tai-api <versión> · … · regenerar: tai-api rules install -->` y que la
69
+ lectura reconozca esa cabecera y no la de tai-sql. Es lo que usa tai-api para instalar las suyas
70
+ en `_platform/tai-api/`, junto a estas. Lo que le toca a la otra librería: sus documentos, su
71
+ directorio, su versión y su aviso.
72
+
55
73
  **Un fichero derivado por schema**, y no uno por proyecto. Lo que describe un schema no
56
74
  tiene que ver con lo que describe otro, y separarlos permite dos cosas: que `push` avise de
57
75
  que se quedó obsoleto **el del schema que acaba de tocar** sin cargar los demás, y que