tai-sql 0.7.11__tar.gz → 0.7.13__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.13}/PKG-INFO +1 -1
  2. {tai_sql-0.7.11 → tai_sql-0.7.13}/pyproject.toml +1 -1
  3. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/cli/commands/cmd_pull/funcs.py +79 -3
  4. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/cli/commands/cmd_pull/main.py +68 -10
  5. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/cli/commands/cmd_rules/main.py +59 -4
  6. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/generators/python_client/context.py +6 -0
  7. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/generators/python_client/templates/base.py.jinja2 +20 -0
  8. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/generators/python_client/templates/domain/dao.py.jinja2 +3 -0
  9. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/generators/python_client/templates/domain/dtos.py.jinja2 +12 -2
  10. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/generators/python_client/templates/domain/model.py.jinja2 +8 -1
  11. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/generators/python_client/templates/facade.py.jinja2 +2 -0
  12. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/generators/python_client/triggers/transformer.py +8 -5
  13. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/generators/rules/__init__.py +6 -0
  14. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/generators/rules/templates/schema.md.jinja2 +6 -0
  15. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/introspect/README.md +3 -0
  16. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/introspect/build.py +19 -2
  17. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/project/paths.py +37 -6
  18. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/rules/README.md +19 -1
  19. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/rules/__init__.py +12 -0
  20. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/rules/catalog.py +26 -9
  21. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/rules/installer.py +79 -5
  22. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/rules/service.py +41 -1
  23. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/rules/staleness.py +7 -3
  24. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/rules/static/comandos.md +11 -2
  25. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/rules/static/modelo.md +8 -0
  26. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/sync/README.md +1 -1
  27. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/sync/diff/__init__.py +3 -1
  28. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/sync/diff/tables.py +6 -3
  29. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/sync/ownership.py +25 -1
  30. {tai_sql-0.7.11 → tai_sql-0.7.13}/LICENSE +0 -0
  31. {tai_sql-0.7.11 → tai_sql-0.7.13}/README.md +0 -0
  32. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/__init__.py +0 -0
  33. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/cli/commands/__init__.py +0 -0
  34. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/cli/commands/cmd_backup/__init__.py +0 -0
  35. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/cli/commands/cmd_backup/main.py +0 -0
  36. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/cli/commands/cmd_deploy/__init__.py +0 -0
  37. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/cli/commands/cmd_deploy/config.py +0 -0
  38. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/cli/commands/cmd_deploy/local.py +0 -0
  39. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/cli/commands/cmd_deploy/main.py +0 -0
  40. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/cli/commands/cmd_deploy/workflow.py +0 -0
  41. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/cli/commands/cmd_feed/__init__.py +0 -0
  42. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/cli/commands/cmd_feed/main.py +0 -0
  43. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/cli/commands/cmd_generate/__init__.py +0 -0
  44. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/cli/commands/cmd_generate/funcs.py +0 -0
  45. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/cli/commands/cmd_generate/main.py +0 -0
  46. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/cli/commands/cmd_hash_migrate/__init__.py +0 -0
  47. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/cli/commands/cmd_hash_migrate/main.py +0 -0
  48. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/cli/commands/cmd_info/__init__.py +0 -0
  49. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/cli/commands/cmd_info/main.py +0 -0
  50. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/cli/commands/cmd_info/status.py +0 -0
  51. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/cli/commands/cmd_init/__init__.py +0 -0
  52. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/cli/commands/cmd_init/main.py +0 -0
  53. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/cli/commands/cmd_init/model.py +0 -0
  54. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/cli/commands/cmd_install/__init__.py +0 -0
  55. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/cli/commands/cmd_install/main.py +0 -0
  56. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/cli/commands/cmd_ping/__init__.py +0 -0
  57. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/cli/commands/cmd_ping/main.py +0 -0
  58. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/cli/commands/cmd_pull/__init__.py +0 -0
  59. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/cli/commands/cmd_restore/__init__.py +0 -0
  60. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/cli/commands/cmd_restore/main.py +0 -0
  61. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/cli/commands/cmd_rules/__init__.py +0 -0
  62. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/cli/commands/cmd_schema/__init__.py +0 -0
  63. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/cli/commands/cmd_schema/model.py +0 -0
  64. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/cli/commands/cmd_schema/new.py +0 -0
  65. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/cli/commands/cmd_schema/schemas/__init__.py +0 -0
  66. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/cli/commands/cmd_schema/schemas/auth.py +0 -0
  67. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/cli/commands/cmd_schema/schemas/base.py +0 -0
  68. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/cli/commands/cmd_schema/schemas/chatbot.py +0 -0
  69. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/cli/commands/cmd_schema/schemas/default.py +0 -0
  70. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/cli/commands/cmd_schema/set_default.py +0 -0
  71. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/cli/commands/prompts.py +0 -0
  72. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/cli/commands/utils/__init__.py +0 -0
  73. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/cli/commands/utils/connectivity.py +0 -0
  74. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/cli/commands/utils/create.py +0 -0
  75. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/cli/commands/utils/github/__init__.py +0 -0
  76. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/cli/commands/utils/github/auth.py +0 -0
  77. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/cli/commands/utils/github/client.py +0 -0
  78. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/cli/commands/utils/github/models.py +0 -0
  79. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/cli/errors.py +0 -0
  80. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/cli/main.py +0 -0
  81. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/cli/preamble.py +0 -0
  82. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/cli/services/__init__.py +0 -0
  83. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/cli/services/backup.py +0 -0
  84. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/connection/README.md +0 -0
  85. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/connection/__init__.py +0 -0
  86. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/connection/provider.py +0 -0
  87. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/connection/recipe.py +0 -0
  88. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/connection/session.py +0 -0
  89. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/connection/sources.py +0 -0
  90. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/crypto.py +0 -0
  91. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/drivers/__init__.py +0 -0
  92. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/drivers/base.py +0 -0
  93. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/drivers/bigquery.py +0 -0
  94. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/drivers/capabilities.py +0 -0
  95. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/drivers/mysql.py +0 -0
  96. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/drivers/postgresql.py +0 -0
  97. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/drivers/sqlserver.py +0 -0
  98. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/errors/README.md +0 -0
  99. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/errors/__init__.py +0 -0
  100. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/errors/collector.py +0 -0
  101. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/errors/render.py +0 -0
  102. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/errors/types.py +0 -0
  103. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/feed/README.md +0 -0
  104. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/feed/__init__.py +0 -0
  105. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/feed/collect.py +0 -0
  106. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/feed/executor.py +0 -0
  107. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/feed/feeder.py +0 -0
  108. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/feed/plan.py +0 -0
  109. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/feed/presenter.py +0 -0
  110. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/feed/records.py +0 -0
  111. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/feed/transform.py +0 -0
  112. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/generators/__init__.py +0 -0
  113. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/generators/base.py +0 -0
  114. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/generators/diagram/README.md +0 -0
  115. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/generators/diagram/__init__.py +0 -0
  116. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/generators/diagram/assets/README.md +0 -0
  117. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/generators/diagram/assets/cytoscape.min.js +0 -0
  118. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/generators/diagram/builder.py +0 -0
  119. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/generators/diagram/generator.py +0 -0
  120. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/generators/diagram/graphviz_render.py +0 -0
  121. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/generators/diagram/html_render.py +0 -0
  122. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/generators/diagram/layout.py +0 -0
  123. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/generators/diagram/model.py +0 -0
  124. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/generators/diagram/templates/er.html.jinja2 +0 -0
  125. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/generators/diagram/themes.py +0 -0
  126. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/generators/python_client/__init__.py +0 -0
  127. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/generators/python_client/calculated.py +0 -0
  128. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/generators/python_client/templates/domain/__init__.py.jinja2 +0 -0
  129. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/generators/python_client/templates/macros/aggregation.jinja2 +0 -0
  130. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/generators/python_client/templates/macros/dataframe.jinja2 +0 -0
  131. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/generators/python_client/templates/macros/filters.jinja2 +0 -0
  132. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/generators/python_client/templates/macros/hierarchy.jinja2 +0 -0
  133. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/generators/python_client/templates/macros/read.jinja2 +0 -0
  134. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/generators/python_client/templates/macros/triggers.jinja2 +0 -0
  135. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/generators/python_client/templates/macros/upsert.jinja2 +0 -0
  136. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/generators/python_client/templates/macros/vector.jinja2 +0 -0
  137. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/generators/python_client/templates/macros/write.jinja2 +0 -0
  138. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/generators/python_client/templates/session.py.jinja2 +0 -0
  139. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/generators/python_client/templates/shared/__init__.py.jinja2 +0 -0
  140. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/generators/python_client/templates/shared/aggregation.py.jinja2 +0 -0
  141. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/generators/python_client/templates/shared/context.py.jinja2 +0 -0
  142. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/generators/python_client/templates/shared/utils/__init__.py.jinja2 +0 -0
  143. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/generators/python_client/templates/shared/utils/encoders.py.jinja2 +0 -0
  144. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/generators/python_client/templates/shared/utils/funcs.py.jinja2 +0 -0
  145. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/generators/python_client/templates/shared/utils/hierarchy.py.jinja2 +0 -0
  146. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/generators/python_client/templates/shared/utils/rls.py.jinja2 +0 -0
  147. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/generators/python_client/templates/shared/vector.py.jinja2 +0 -0
  148. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/generators/python_client/triggers/__init__.py +0 -0
  149. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/generators/python_client/triggers/analyzer.py +0 -0
  150. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/generators/python_client/triggers/renderer.py +0 -0
  151. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/generators/python_client/triggers/steps.py +0 -0
  152. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/introspect/__init__.py +0 -0
  153. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/introspect/blueprint.py +0 -0
  154. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/introspect/defaults.py +0 -0
  155. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/introspect/naming.py +0 -0
  156. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/introspect/reader.py +0 -0
  157. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/introspect/templates/schema.py.jinja2 +0 -0
  158. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/introspect/types.py +0 -0
  159. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/introspect/writer.py +0 -0
  160. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/orm/README.md +0 -0
  161. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/orm/__init__.py +0 -0
  162. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/orm/analysis/README.md +0 -0
  163. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/orm/analysis/__init__.py +0 -0
  164. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/orm/analysis/calculated.py +0 -0
  165. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/orm/analysis/collector.py +0 -0
  166. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/orm/analysis/column_analyzer.py +0 -0
  167. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/orm/analysis/constraints.py +0 -0
  168. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/orm/analysis/hints.py +0 -0
  169. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/orm/analysis/relation_analyzer.py +0 -0
  170. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/orm/analysis/schema_analyzer.py +0 -0
  171. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/orm/declarative/README.md +0 -0
  172. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/orm/declarative/__init__.py +0 -0
  173. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/orm/declarative/annotations.py +0 -0
  174. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/orm/declarative/base.py +0 -0
  175. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/orm/declarative/calculated.py +0 -0
  176. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/orm/declarative/columns.py +0 -0
  177. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/orm/declarative/constraints.py +0 -0
  178. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/orm/declarative/datasource.py +0 -0
  179. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/orm/declarative/descriptors.py +0 -0
  180. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/orm/declarative/encoders.py +0 -0
  181. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/orm/declarative/enumerations.py +0 -0
  182. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/orm/declarative/relations.py +0 -0
  183. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/orm/declarative/table.py +0 -0
  184. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/orm/declarative/trigger_api.py +0 -0
  185. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/orm/declarative/triggers.py +0 -0
  186. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/orm/declarative/view.py +0 -0
  187. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/orm/mapping/README.md +0 -0
  188. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/orm/mapping/__init__.py +0 -0
  189. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/orm/mapping/sqlalchemy_mapper.py +0 -0
  190. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/orm/model/README.md +0 -0
  191. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/orm/model/__init__.py +0 -0
  192. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/orm/model/column.py +0 -0
  193. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/orm/model/constraints.py +0 -0
  194. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/orm/model/entity.py +0 -0
  195. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/orm/model/enumeration.py +0 -0
  196. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/orm/model/relation.py +0 -0
  197. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/orm/model/schema.py +0 -0
  198. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/orm/model/trigger.py +0 -0
  199. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/orm/naming.py +0 -0
  200. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/orm/ordering.py +0 -0
  201. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/orm/registry.py +0 -0
  202. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/orm/types.py +0 -0
  203. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/project/README.md +0 -0
  204. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/project/__init__.py +0 -0
  205. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/project/config.py +0 -0
  206. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/project/context.py +0 -0
  207. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/project/manager.py +0 -0
  208. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/rules/meta.py +0 -0
  209. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/rules/static/errores.md +0 -0
  210. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/schema.py +0 -0
  211. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/sync/__init__.py +0 -0
  212. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/sync/diff/constraints.py +0 -0
  213. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/sync/diff/indexes.py +0 -0
  214. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/sync/diff/views.py +0 -0
  215. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/sync/executor.py +0 -0
  216. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/sync/plan/__init__.py +0 -0
  217. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/sync/plan/planner.py +0 -0
  218. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/sync/plan/statements.py +0 -0
  219. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/sync/presenter.py +0 -0
  220. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/sync/report.py +0 -0
  221. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/sync/safety.py +0 -0
  222. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/sync/state/__init__.py +0 -0
  223. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/sync/state/local.py +0 -0
  224. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/sync/state/model.py +0 -0
  225. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/sync/state/normalize.py +0 -0
  226. {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/sync/state/remote.py +0 -0
  227. {tai_sql-0.7.11 → tai_sql-0.7.13}/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.13
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.13"
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
@@ -225,6 +225,12 @@ class TemplateContextBuilder:
225
225
  'is_operative': column.is_operative,
226
226
  'is_calculated': column.is_calculated,
227
227
  'self_reference': column.self_reference,
228
+ # La rellena la base de datos (NOW()): no es obligatoria al crear. Faltaba, y las
229
+ # plantillas que la miraban la leían siempre como falsa.
230
+ 'server_now': column.server_now,
231
+ # El tamaño máximo de un texto (`column(length=n)`), para quien valide antes de
232
+ # escribir —los esquemas de formulario de tai-api—. `None` si no se declaró.
233
+ 'max_length': column.length if column.type == 'str' else None,
228
234
  }
229
235
 
230
236
  if column.is_vector:
@@ -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([]) %}
@@ -247,7 +247,7 @@ class {{ model.name }}Read(PrettyModel):
247
247
  class {{ model.name }}Create(PrettyModel):
248
248
  """DTO de escritura para {{ model.name }}."""
249
249
  {% for column in model.columns -%}
250
- {% if not column.constraints.get('autoincrement', False) and column.default is none and not column.nullable and not column.is_foreign_key and not column.is_operative and not column.has_encoder|default(false) and not column.is_calculated %}
250
+ {% if not column.constraints.get('autoincrement', False) and column.default is none and not column.nullable and not column.is_foreign_key and not column.is_operative and not column.has_encoder|default(false) and not column.is_calculated and not column.server_now %}
251
251
  {{ column.name }}: {{ column.pydantic_type }}
252
252
  {% endif %}
253
253
  {% endfor %}
@@ -257,6 +257,9 @@ class {{ model.name }}Create(PrettyModel):
257
257
  {{ column.name }}: Optional[{{ column.pydantic_type }}] = None
258
258
  {% elif column.is_foreign_key %}
259
259
  {{ column.name }}: Optional[{{ column.pydantic_type }}] = None
260
+ {% elif column.server_now %}
261
+ {# La rellena la base de datos con NOW() si no llega: no es obligatoria. #}
262
+ {{ column.name }}: Optional[{{ column.pydantic_type }}] = None
260
263
  {% elif column.default is none and column.nullable %}
261
264
  {{ column.name }}: Optional[{{ column.pydantic_type }}] = None
262
265
  {% elif column.default is not none %}
@@ -295,11 +298,18 @@ class {{ model.name }}Create(PrettyModel):
295
298
  def to_instance(self) -> {{ model.name }}:
296
299
  model = {{ model.name }}(
297
300
  {% for column in model.columns %}
298
- {% if not column.constraints.get('autoincrement', False) and not column.is_operative and not column.is_calculated %}
301
+ {% if not column.constraints.get('autoincrement', False) and not column.is_operative and not column.is_calculated and not column.server_now %}
299
302
  {{ column.name }}=self.{{ column.name }},
300
303
  {% endif %}
301
304
  {% endfor %}
302
305
  )
306
+ {% for column in model.columns %}
307
+ {% if column.server_now and not column.is_operative and not column.is_calculated %}
308
+ # Solo si llega: asignarle None mandaría un NULL, y no dejaría a la base de datos poner NOW().
309
+ if self.{{ column.name }} is not None:
310
+ model.{{ column.name }} = self.{{ column.name }}
311
+ {% endif %}
312
+ {% endfor %}
303
313
  {% for col in operative_str_cols %}
304
314
  model.{{ col.name }} = get_username()
305
315
  {% endfor %}
@@ -83,6 +83,9 @@ from pgvector.sqlalchemy import Vector
83
83
  {% if column.sa_type and column.sa_type not in sa_imports %}
84
84
  {% set _ = sa_imports.append(column.sa_type) %}
85
85
  {% endif %}
86
+ {% if column.server_now and 'func' not in sa_imports %}
87
+ {% set _ = sa_imports.append('func') %}
88
+ {% endif %}
86
89
  {% endfor %}
87
90
  {% if sa_imports %}
88
91
  from sqlalchemy import {{ sa_imports | sort | join(', ') }}
@@ -165,7 +168,7 @@ class {{ model.name }}(Base):
165
168
  {% set mapped_type = 'Optional[' + py_type + ']' if column.nullable else py_type %}
166
169
  {# Determine if mapped_column() call is needed #}
167
170
  {% set has_constraints = column.constraints.values() | select | list | length > 0 %}
168
- {% set needs_mapped_column = has_constraints or column.encrypt or column.hash or column.is_calculated or column.sa_type is not none or column.type == 'vector' %}
171
+ {% set needs_mapped_column = has_constraints or column.encrypt or column.hash or column.is_calculated or column.sa_type is not none or column.type == 'vector' or column.server_now %}
169
172
  {% if needs_mapped_column %}
170
173
  {% set mc_args = [] %}
171
174
  {% if column.type == 'vector' %}{% set _ = mc_args.append('Vector(' + column.dimensions|string + ')') %}
@@ -185,6 +188,10 @@ class {{ model.name }}(Base):
185
188
  {% endfor %}
186
189
  {% endif %}
187
190
  {% if column.encrypt or column.hash %}{% set _ = mc_args.append('name="' + column.name + '"') %}{% endif %}
191
+ {# La pone la base de datos. Sin declararlo aquí, SQLAlchemy la enviaba como NULL —rellena
192
+ con None lo que no tiene valor para poder agrupar el INSERT— y la base de datos no llegaba
193
+ a poner NOW(). #}
194
+ {% if column.server_now %}{% set _ = mc_args.append('server_default=func.now()') %}{% endif %}
188
195
  {% if column.is_calculated %}{% set _ = mc_args.append('name="' + column.name + '"') %}{% endif %}
189
196
  {{ col_attr }}: Mapped[{{ mapped_type }}] = mapped_column({{ mc_args | join(', ') }})
190
197
  {% else %}
@@ -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,