tai-sql 0.7.18__tar.gz → 0.7.20__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (229) hide show
  1. {tai_sql-0.7.18 → tai_sql-0.7.20}/PKG-INFO +5 -4
  2. {tai_sql-0.7.18 → tai_sql-0.7.20}/README.md +4 -3
  3. {tai_sql-0.7.18 → tai_sql-0.7.20}/pyproject.toml +1 -1
  4. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/cli/commands/cmd_generate/funcs.py +16 -1
  5. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/cli/commands/cmd_schema/schemas/chatbot.py +5 -0
  6. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/errors/render.py +21 -8
  7. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/generators/python_client/__init__.py +17 -0
  8. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/generators/python_client/templates/domain/dao.py.jinja2 +5 -0
  9. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/generators/python_client/templates/domain/dtos.py.jinja2 +5 -26
  10. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/generators/python_client/templates/macros/hierarchy.jinja2 +7 -33
  11. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/generators/python_client/templates/macros/read.jinja2 +10 -1
  12. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/generators/python_client/templates/macros/upsert.jinja2 +5 -0
  13. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/generators/python_client/templates/macros/vector.jinja2 +2 -2
  14. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/generators/python_client/templates/macros/write.jinja2 +24 -21
  15. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/generators/python_client/templates/shared/utils/encoders.py.jinja2 +66 -6
  16. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/generators/python_client/triggers/__init__.py +3 -0
  17. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/generators/python_client/triggers/analyzer.py +82 -22
  18. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/generators/python_client/triggers/renderer.py +67 -10
  19. tai_sql-0.7.20/tai_sql/generators/python_client/triggers/scopes.py +164 -0
  20. tai_sql-0.7.20/tai_sql/generators/python_client/triggers/transformer.py +724 -0
  21. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/orm/declarative/trigger_api.py +4 -2
  22. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/orm/declarative/triggers.py +32 -9
  23. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/orm/mapping/sqlalchemy_mapper.py +15 -5
  24. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/orm/types.py +9 -4
  25. tai_sql-0.7.18/tai_sql/generators/python_client/triggers/transformer.py +0 -691
  26. {tai_sql-0.7.18 → tai_sql-0.7.20}/LICENSE +0 -0
  27. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/__init__.py +0 -0
  28. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/cli/commands/__init__.py +0 -0
  29. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/cli/commands/cmd_backup/__init__.py +0 -0
  30. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/cli/commands/cmd_backup/main.py +0 -0
  31. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/cli/commands/cmd_deploy/__init__.py +0 -0
  32. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/cli/commands/cmd_deploy/config.py +0 -0
  33. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/cli/commands/cmd_deploy/local.py +0 -0
  34. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/cli/commands/cmd_deploy/main.py +0 -0
  35. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/cli/commands/cmd_deploy/workflow.py +0 -0
  36. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/cli/commands/cmd_feed/__init__.py +0 -0
  37. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/cli/commands/cmd_feed/main.py +0 -0
  38. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/cli/commands/cmd_generate/__init__.py +0 -0
  39. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/cli/commands/cmd_generate/main.py +0 -0
  40. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/cli/commands/cmd_hash_migrate/__init__.py +0 -0
  41. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/cli/commands/cmd_hash_migrate/main.py +0 -0
  42. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/cli/commands/cmd_info/__init__.py +0 -0
  43. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/cli/commands/cmd_info/main.py +0 -0
  44. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/cli/commands/cmd_info/status.py +0 -0
  45. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/cli/commands/cmd_init/__init__.py +0 -0
  46. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/cli/commands/cmd_init/main.py +0 -0
  47. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/cli/commands/cmd_init/model.py +0 -0
  48. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/cli/commands/cmd_install/__init__.py +0 -0
  49. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/cli/commands/cmd_install/main.py +0 -0
  50. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/cli/commands/cmd_ping/__init__.py +0 -0
  51. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/cli/commands/cmd_ping/main.py +0 -0
  52. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/cli/commands/cmd_pull/__init__.py +0 -0
  53. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/cli/commands/cmd_pull/funcs.py +0 -0
  54. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/cli/commands/cmd_pull/main.py +0 -0
  55. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/cli/commands/cmd_restore/__init__.py +0 -0
  56. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/cli/commands/cmd_restore/main.py +0 -0
  57. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/cli/commands/cmd_rules/__init__.py +0 -0
  58. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/cli/commands/cmd_rules/main.py +0 -0
  59. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/cli/commands/cmd_schema/__init__.py +0 -0
  60. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/cli/commands/cmd_schema/model.py +0 -0
  61. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/cli/commands/cmd_schema/new.py +0 -0
  62. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/cli/commands/cmd_schema/schemas/__init__.py +0 -0
  63. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/cli/commands/cmd_schema/schemas/auth.py +0 -0
  64. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/cli/commands/cmd_schema/schemas/base.py +0 -0
  65. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/cli/commands/cmd_schema/schemas/default.py +0 -0
  66. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/cli/commands/cmd_schema/set_default.py +0 -0
  67. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/cli/commands/prompts.py +0 -0
  68. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/cli/commands/utils/__init__.py +0 -0
  69. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/cli/commands/utils/connectivity.py +0 -0
  70. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/cli/commands/utils/create.py +0 -0
  71. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/cli/commands/utils/github/__init__.py +0 -0
  72. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/cli/commands/utils/github/auth.py +0 -0
  73. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/cli/commands/utils/github/client.py +0 -0
  74. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/cli/commands/utils/github/models.py +0 -0
  75. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/cli/errors.py +0 -0
  76. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/cli/main.py +0 -0
  77. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/cli/preamble.py +0 -0
  78. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/cli/services/__init__.py +0 -0
  79. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/cli/services/backup.py +0 -0
  80. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/connection/README.md +0 -0
  81. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/connection/__init__.py +0 -0
  82. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/connection/provider.py +0 -0
  83. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/connection/recipe.py +0 -0
  84. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/connection/session.py +0 -0
  85. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/connection/sources.py +0 -0
  86. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/crypto.py +0 -0
  87. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/drivers/__init__.py +0 -0
  88. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/drivers/base.py +0 -0
  89. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/drivers/bigquery.py +0 -0
  90. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/drivers/capabilities.py +0 -0
  91. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/drivers/mysql.py +0 -0
  92. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/drivers/postgresql.py +0 -0
  93. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/drivers/sqlserver.py +0 -0
  94. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/errors/README.md +0 -0
  95. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/errors/__init__.py +0 -0
  96. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/errors/collector.py +0 -0
  97. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/errors/types.py +0 -0
  98. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/feed/README.md +0 -0
  99. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/feed/__init__.py +0 -0
  100. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/feed/collect.py +0 -0
  101. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/feed/executor.py +0 -0
  102. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/feed/feeder.py +0 -0
  103. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/feed/plan.py +0 -0
  104. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/feed/presenter.py +0 -0
  105. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/feed/records.py +0 -0
  106. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/feed/transform.py +0 -0
  107. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/generators/__init__.py +0 -0
  108. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/generators/base.py +0 -0
  109. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/generators/diagram/README.md +0 -0
  110. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/generators/diagram/__init__.py +0 -0
  111. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/generators/diagram/assets/README.md +0 -0
  112. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/generators/diagram/assets/cytoscape.min.js +0 -0
  113. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/generators/diagram/builder.py +0 -0
  114. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/generators/diagram/generator.py +0 -0
  115. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/generators/diagram/graphviz_render.py +0 -0
  116. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/generators/diagram/html_render.py +0 -0
  117. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/generators/diagram/layout.py +0 -0
  118. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/generators/diagram/model.py +0 -0
  119. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/generators/diagram/templates/er.html.jinja2 +0 -0
  120. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/generators/diagram/themes.py +0 -0
  121. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/generators/python_client/calculated.py +0 -0
  122. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/generators/python_client/context.py +0 -0
  123. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/generators/python_client/templates/base.py.jinja2 +0 -0
  124. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/generators/python_client/templates/domain/__init__.py.jinja2 +0 -0
  125. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/generators/python_client/templates/domain/model.py.jinja2 +0 -0
  126. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/generators/python_client/templates/facade.py.jinja2 +0 -0
  127. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/generators/python_client/templates/macros/aggregation.jinja2 +0 -0
  128. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/generators/python_client/templates/macros/dataframe.jinja2 +0 -0
  129. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/generators/python_client/templates/macros/filters.jinja2 +0 -0
  130. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/generators/python_client/templates/macros/triggers.jinja2 +0 -0
  131. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/generators/python_client/templates/session.py.jinja2 +0 -0
  132. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/generators/python_client/templates/shared/__init__.py.jinja2 +0 -0
  133. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/generators/python_client/templates/shared/aggregation.py.jinja2 +0 -0
  134. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/generators/python_client/templates/shared/context.py.jinja2 +0 -0
  135. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/generators/python_client/templates/shared/utils/__init__.py.jinja2 +0 -0
  136. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/generators/python_client/templates/shared/utils/funcs.py.jinja2 +0 -0
  137. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/generators/python_client/templates/shared/utils/hierarchy.py.jinja2 +0 -0
  138. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/generators/python_client/templates/shared/utils/rls.py.jinja2 +0 -0
  139. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/generators/python_client/templates/shared/vector.py.jinja2 +0 -0
  140. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/generators/python_client/triggers/steps.py +0 -0
  141. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/generators/rules/__init__.py +0 -0
  142. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/generators/rules/templates/schema.md.jinja2 +0 -0
  143. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/introspect/README.md +0 -0
  144. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/introspect/__init__.py +0 -0
  145. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/introspect/blueprint.py +0 -0
  146. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/introspect/build.py +0 -0
  147. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/introspect/defaults.py +0 -0
  148. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/introspect/naming.py +0 -0
  149. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/introspect/reader.py +0 -0
  150. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/introspect/templates/schema.py.jinja2 +0 -0
  151. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/introspect/types.py +0 -0
  152. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/introspect/writer.py +0 -0
  153. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/orm/README.md +0 -0
  154. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/orm/__init__.py +0 -0
  155. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/orm/analysis/README.md +0 -0
  156. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/orm/analysis/__init__.py +0 -0
  157. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/orm/analysis/calculated.py +0 -0
  158. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/orm/analysis/collector.py +0 -0
  159. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/orm/analysis/column_analyzer.py +0 -0
  160. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/orm/analysis/constraints.py +0 -0
  161. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/orm/analysis/hints.py +0 -0
  162. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/orm/analysis/relation_analyzer.py +0 -0
  163. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/orm/analysis/schema_analyzer.py +0 -0
  164. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/orm/declarative/README.md +0 -0
  165. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/orm/declarative/__init__.py +0 -0
  166. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/orm/declarative/annotations.py +0 -0
  167. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/orm/declarative/base.py +0 -0
  168. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/orm/declarative/calculated.py +0 -0
  169. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/orm/declarative/columns.py +0 -0
  170. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/orm/declarative/constraints.py +0 -0
  171. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/orm/declarative/datasource.py +0 -0
  172. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/orm/declarative/descriptors.py +0 -0
  173. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/orm/declarative/encoders.py +0 -0
  174. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/orm/declarative/enumerations.py +0 -0
  175. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/orm/declarative/relations.py +0 -0
  176. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/orm/declarative/table.py +0 -0
  177. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/orm/declarative/view.py +0 -0
  178. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/orm/mapping/README.md +0 -0
  179. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/orm/mapping/__init__.py +0 -0
  180. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/orm/model/README.md +0 -0
  181. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/orm/model/__init__.py +0 -0
  182. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/orm/model/column.py +0 -0
  183. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/orm/model/constraints.py +0 -0
  184. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/orm/model/entity.py +0 -0
  185. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/orm/model/enumeration.py +0 -0
  186. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/orm/model/relation.py +0 -0
  187. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/orm/model/schema.py +0 -0
  188. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/orm/model/trigger.py +0 -0
  189. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/orm/naming.py +0 -0
  190. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/orm/ordering.py +0 -0
  191. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/orm/registry.py +0 -0
  192. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/project/README.md +0 -0
  193. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/project/__init__.py +0 -0
  194. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/project/config.py +0 -0
  195. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/project/context.py +0 -0
  196. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/project/manager.py +0 -0
  197. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/project/paths.py +0 -0
  198. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/rules/README.md +0 -0
  199. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/rules/__init__.py +0 -0
  200. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/rules/catalog.py +0 -0
  201. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/rules/installer.py +0 -0
  202. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/rules/meta.py +0 -0
  203. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/rules/service.py +0 -0
  204. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/rules/staleness.py +0 -0
  205. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/rules/static/comandos.md +0 -0
  206. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/rules/static/errores.md +0 -0
  207. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/rules/static/modelo.md +0 -0
  208. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/schema.py +0 -0
  209. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/sync/README.md +0 -0
  210. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/sync/__init__.py +0 -0
  211. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/sync/diff/__init__.py +0 -0
  212. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/sync/diff/constraints.py +0 -0
  213. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/sync/diff/indexes.py +0 -0
  214. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/sync/diff/tables.py +0 -0
  215. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/sync/diff/views.py +0 -0
  216. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/sync/executor.py +0 -0
  217. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/sync/ownership.py +0 -0
  218. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/sync/plan/__init__.py +0 -0
  219. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/sync/plan/planner.py +0 -0
  220. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/sync/plan/statements.py +0 -0
  221. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/sync/presenter.py +0 -0
  222. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/sync/report.py +0 -0
  223. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/sync/safety.py +0 -0
  224. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/sync/state/__init__.py +0 -0
  225. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/sync/state/local.py +0 -0
  226. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/sync/state/model.py +0 -0
  227. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/sync/state/normalize.py +0 -0
  228. {tai_sql-0.7.18 → tai_sql-0.7.20}/tai_sql/sync/state/remote.py +0 -0
  229. {tai_sql-0.7.18 → tai_sql-0.7.20}/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.18
3
+ Version: 0.7.20
4
4
  Summary: SQL database management and code generation tool
5
5
  License-Expression: LicenseRef-Proprietary
6
6
  License-File: LICENSE
@@ -211,9 +211,10 @@ poetry run pytest -m mysql # el ciclo completo contra MySQL
211
211
  Los tests marcados `db` necesitan un PostgreSQL alcanzable y se **saltan** si no lo hay; los
212
212
  marcados `mysql`, un MySQL en `TAI_SQL_MYSQL_URL`. Sus schemas salen de `tests/fixtures/project/`.
213
213
 
214
- El CI tiene tres workflows: `tests.yaml` ejecuta la suite en cada push y PR a `main` y `dev` —un
215
- trabajo con PostgreSQL y otro con MySQL—, `docs.yaml` publica el manual y `publish.yaml` publica
216
- a PyPI en push a `main`. La rama de trabajo habitual es `dev`, y la versión se bumpea a mano en
214
+ El CI tiene tres workflows: `tests.yaml` ejecuta la suite en cada PR a `main` y `dev` —un trabajo
215
+ con PostgreSQL y otro con MySQL—, `docs.yaml` publica el manual y `publish.yaml` publica a PyPI en
216
+ push a `main`, después de volver a pasar la suite. En los push no se ejecuta aparte, ni en una PR
217
+ que solo cambia documentación (`docs/`, `specs/`, `.claude/`). La rama de trabajo habitual es `dev`, y la versión se bumpea a mano en
217
218
  `pyproject.toml`.
218
219
 
219
220
  ### El manual, en local
@@ -148,9 +148,10 @@ poetry run pytest -m mysql # el ciclo completo contra MySQL
148
148
  Los tests marcados `db` necesitan un PostgreSQL alcanzable y se **saltan** si no lo hay; los
149
149
  marcados `mysql`, un MySQL en `TAI_SQL_MYSQL_URL`. Sus schemas salen de `tests/fixtures/project/`.
150
150
 
151
- El CI tiene tres workflows: `tests.yaml` ejecuta la suite en cada push y PR a `main` y `dev` —un
152
- trabajo con PostgreSQL y otro con MySQL—, `docs.yaml` publica el manual y `publish.yaml` publica
153
- a PyPI en push a `main`. La rama de trabajo habitual es `dev`, y la versión se bumpea a mano en
151
+ El CI tiene tres workflows: `tests.yaml` ejecuta la suite en cada PR a `main` y `dev` —un trabajo
152
+ con PostgreSQL y otro con MySQL—, `docs.yaml` publica el manual y `publish.yaml` publica a PyPI en
153
+ push a `main`, después de volver a pasar la suite. En los push no se ejecuta aparte, ni en una PR
154
+ que solo cambia documentación (`docs/`, `specs/`, `.claude/`). La rama de trabajo habitual es `dev`, y la versión se bumpea a mano en
154
155
  `pyproject.toml`.
155
156
 
156
157
  ### El manual, en local
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "tai-sql"
3
- version = "0.7.18"
3
+ version = "0.7.20"
4
4
  description = "SQL database management and code generation tool"
5
5
  license = "LicenseRef-Proprietary"
6
6
  license-files = ["LICENSE"]
@@ -1,7 +1,7 @@
1
1
  import click
2
2
 
3
3
  from tai_sql import pm
4
- from tai_sql.errors import ErrorCode, GenerationError, render_warning
4
+ from tai_sql.errors import ErrorCode, GenerationError, TaiSqlError, render_warning
5
5
  from tai_sql.rules import warn_if_outdated
6
6
 
7
7
  from ...preamble import load_schema
@@ -49,6 +49,21 @@ def run_generate(schema_name: str = None):
49
49
  )
50
50
  generator.generate()
51
51
 
52
+ except TaiSqlError as e:
53
+ # El generador ya sabe qué ha fallado y cómo se arregla —un trigger que no se
54
+ # puede transpilar dice cuál y por qué—: su solución vale más que la genérica.
55
+ # Se conserva el tipo y se añade el schema y el generador delante del sitio
56
+ # que traía (el trigger), en vez de pisarlo.
57
+ raise type(e)(
58
+ message=f"{generator_name} falló al generar: {e.message}",
59
+ solution=e.solution,
60
+ location=' · '.join(filter(None, (
61
+ f"Schema '{schema_name}'", generator_name, e.diagnostic.location,
62
+ ))),
63
+ code=e.diagnostic.code,
64
+ **e.diagnostic.context,
65
+ ) from e
66
+
52
67
  except Exception as e:
53
68
  raise GenerationError(
54
69
  message=f"{generator_name} falló al generar: {e}",
@@ -62,6 +62,11 @@ class ChatbotSchema(SchemaTemplate):
62
62
 
63
63
  visible: col[bool] = column(default=True, index=True, description='Si no, la conversación no se lista')
64
64
  last_message_timestamp: col[datetime | None] = column(description='Timestamp del último mensaje del chat')
65
+ # Por alias y no como FK a modelo.id, a propósito: el alias es con lo que el catálogo y
66
+ # el front piden un modelo, y uno desactivado no tiene por qué romper la conversación
67
+ # (cae al de por defecto). Opcional y sin default: un push la añade a una base de
68
+ # datos existente sin tocar sus filas
69
+ modelo: col[str | None] = column(description='Alias del modelo con el que responde esta conversación; sin él, el de por defecto')
65
70
 
66
71
  messages: onetomany[Mensaje] # Relación one-to-many con la tabla Mensaje
67
72
  token_usage: onetomany[TokenUsage] # El consumo de la conversación, tenga mensaje o no
@@ -49,6 +49,19 @@ def _console(stream: Optional[TextIO] = None):
49
49
  return Console(file=stream or sys.stderr, soft_wrap=True)
50
50
 
51
51
 
52
+ def _escape(text) -> str:
53
+ """
54
+ El texto de un diagnóstico, a salvo del marcado de `rich`.
55
+
56
+ Lo que se interpola en `console.print()` se interpreta como marcado, y un corchete se
57
+ toma por una etiqueta de estilo: `pip install 'tai-sql[vectors]'` salía como
58
+ `pip install 'tai-sql'`, justo la solución que no funciona.
59
+ """
60
+ from rich.markup import escape
61
+
62
+ return escape(str(text))
63
+
64
+
52
65
  def shorten_path(path) -> str:
53
66
  """
54
67
  Deja una ruta relativa al directorio actual, si cuelga de él.
@@ -111,17 +124,17 @@ def render_diagnostics(
111
124
  def _render_one_rich(console, diagnostic: Diagnostic, context: Optional[str]) -> None:
112
125
  """Un problema: tres líneas, sin marco."""
113
126
  console.print()
114
- console.print(f"[bold red]❌ {diagnostic.message}[/bold red]")
127
+ console.print(f"[bold red]❌ {_escape(diagnostic.message)}[/bold red]")
115
128
 
116
129
  detail = _detail_line(diagnostic, context)
117
130
  if detail:
118
- console.print(f" [dim]📍 {detail}[/dim]")
131
+ console.print(f" [dim]📍 {_escape(detail)}[/dim]")
119
132
 
120
- console.print(f" [yellow]💡 {diagnostic.solution}[/yellow]")
133
+ console.print(f" [yellow]💡 {_escape(diagnostic.solution)}[/yellow]")
121
134
 
122
135
  if diagnostic.context:
123
136
  extra = ', '.join(f'{k}={v}' for k, v in diagnostic.context.items())
124
- console.print(f" [dim]ℹ️ {extra}[/dim]")
137
+ console.print(f" [dim]ℹ️ {_escape(extra)}[/dim]")
125
138
 
126
139
  console.print()
127
140
 
@@ -134,7 +147,7 @@ def _render_many_rich(
134
147
  from rich.text import Text
135
148
 
136
149
  total = len(diagnostics)
137
- where = f' en {context}' if context else ''
150
+ where = f' en {_escape(context)}' if context else ''
138
151
  console.print(f"\n[bold red]❌ Se encontraron {total} errores{where}[/bold red]\n")
139
152
 
140
153
  for i, diagnostic in enumerate(diagnostics, 1):
@@ -235,11 +248,11 @@ def render_warning(
235
248
  print(f" 💡 {hint}", file=out)
236
249
  return
237
250
 
238
- console.print(f"[bold yellow]⚠️ {message}[/bold yellow]")
251
+ console.print(f"[bold yellow]⚠️ {_escape(message)}[/bold yellow]")
239
252
  if location:
240
- console.print(f" [dim]📍 {location}[/dim]")
253
+ console.print(f" [dim]📍 {_escape(location)}[/dim]")
241
254
  if hint:
242
- console.print(f" [yellow]💡 {hint}[/yellow]")
255
+ console.print(f" [yellow]💡 {_escape(hint)}[/yellow]")
243
256
 
244
257
 
245
258
  __all__ = ['render_diagnostics', 'render_warning']
@@ -307,6 +307,22 @@ class PythonClientGenerator(BaseGenerator):
307
307
  def _all_models_data(self, mode: str = 'sync') -> List[Dict[str, Any]]:
308
308
  return self.context.entities(mode)
309
309
 
310
+ @staticmethod
311
+ def _writes_vectors(all_models: List[Dict[str, Any]]) -> bool:
312
+ """
313
+ Whether some table of the schema computes a vector on write (`vector_column` with
314
+ an encoder and a `source`).
315
+
316
+ It is schema-wide, not per table: a DAO also writes the nested rows of other
317
+ tables, and those can carry a vector.
318
+ """
319
+ return any(
320
+ col.get('type') == 'vector' and col.get('has_encoder')
321
+ and col.get('encoder_config') and col.get('source')
322
+ for m in all_models
323
+ for col in m.get('columns', [])
324
+ )
325
+
310
326
  # ── Generate ──────────────────────────────────────────────────────────
311
327
 
312
328
  def generate(self) -> str:
@@ -525,6 +541,7 @@ class PythonClientGenerator(BaseGenerator):
525
541
  schema_name=self.schema.name,
526
542
  max_depth=self.max_depth,
527
543
  logger_name=self.logger_name,
544
+ writes_vectors=self._writes_vectors(all_models),
528
545
  is_async=is_async,
529
546
  prefix=prefix,
530
547
  dao_module_name=dao_module_name,
@@ -153,6 +153,11 @@ from {{ import_prefix }}_shared.utils.encoders import {{ model.tablename }}_{{ v
153
153
  import asyncio
154
154
  {% endif %}
155
155
  {% endif %}
156
+ {% if writes_vectors and not model.is_view %}
157
+ {# Los vectores se calculan al escribir, sobre todo lo pendiente de la sesión: una fila
158
+ anidada de otra tabla también puede llevar uno, así que no depende de esta tabla. #}
159
+ from {{ import_prefix }}_shared.utils.encoders import {{ 'aencode_pending' if is_async else 'encode_pending' }} as _encode_pending
160
+ {% endif %}
156
161
 
157
162
  {# `natural_key` es la respuesta del modelo a «por qué columnas se reconoce una fila»:
158
163
  la clave primaria si quien escribe la trae, si no la primera columna única, y si no la
@@ -67,15 +67,6 @@ from {{ import_prefix }}_shared import (
67
67
  if TYPE_CHECKING:
68
68
  from pandas import DataFrame, Series # type: ignore[import-untyped]
69
69
 
70
- {% set _dto_encoder_cols = model.columns | selectattr('type', 'equalto', 'vector') | selectattr('has_encoder') | list %}
71
- {% if _dto_encoder_cols %}
72
- # ── Vector encoders ──────────────────────────────────────────────────────
73
- {% for vcol in _dto_encoder_cols %}
74
- {% if vcol.encoder_config %}
75
- from {{ import_prefix }}_shared.utils.encoders import {{ model.tablename }}_{{ vcol.name }}_encoder as _{{ vcol.name }}_encoder
76
- {% endif %}
77
- {% endfor %}
78
- {% endif %}
79
70
 
80
71
  {# ── Read DTO ─────────────────────────────────────────────────────────── #}
81
72
 
@@ -332,28 +323,16 @@ class {{ model.name }}Create(PrettyModel):
332
323
  return self.model_dump(exclude_none=True)
333
324
 
334
325
  {% if vector_cols_create_encoder %}
335
- def model_post_init(self, __context: Any) -> None:
336
- {% for vcol in vector_cols_create_encoder %}
337
- {% if vcol.source %}
338
- if self.{{ vcol.name }} is None and self.{{ vcol.source }} is not None:
339
- self.{{ vcol.name }} = _{{ vcol.name }}_encoder.encode(self.{{ vcol.source }})
340
- {% endif %}
341
- {% endfor %}
342
-
343
326
  @classmethod
344
327
  def deferred(cls, **kwargs) -> '{{ model.name }}Create':
345
328
  """
346
- Create a DTO with vector encoding deferred.
347
-
348
- Use when building lists for ``create_many()``: encoding will be done
349
- in a single ``encode_batch`` call inside the DAO instead of once per record.
350
-
351
- Example::
329
+ Lo mismo que el constructor. Se conserva por compatibilidad.
352
330
 
353
- records = [{{ model.name }}Create.deferred({% for vcol in vector_cols_create_encoder %}{% if vcol.source %}{{ vcol.source }}=text{% endif %}{% endfor %}) for text in texts]
354
- {{ model.name }}SyncDAO.create_many(records)
331
+ Construir un DTO no calcula ningún vector: lo calcula el DAO al escribir, en lote y,
332
+ en el DAO asíncrono, sin bloquear el bucle de eventos. Antes el constructor lo
333
+ calculaba en el acto, y `deferred()` existía para evitarlo.
355
334
  """
356
- return cls.model_construct(**kwargs)
335
+ return cls(**kwargs)
357
336
  {% endif %}
358
337
  {% endif %}
359
338
 
@@ -86,40 +86,11 @@ tree_depth: Optional[int] = None,
86
86
 
87
87
  {{ def_keyword }} execute_query(session: {{ session_type }}) -> List[{{ model.name }}Read]:
88
88
  if _hierarchy_mode == TreeMode.FLAT:
89
- # Standard flat query
90
- results = {{ await_keyword -}} session.execute(query)
91
89
  {% if vector_cols_with_encoder %}
92
-
93
- if _has_vector_search:
94
- rows = results.all()
95
- _log_commit(session, f"[{{ schema_name }}] ✅ Encontrados {len(rows)} registros {{ model.name }} (similitud)")
96
- return [
97
- {{ model.name }}Read.from_instance(
98
- row[0],
99
- {% if not model.is_view %}
100
- includes=includes,
101
- max_depth={{ max_depth }},
102
- {% endif %}
103
- include_vectors=include_vectors,
104
- vector_search=_build_vector_search_result(row, _active_vectors)
105
- )
106
- for row in rows
107
- ]
108
- else:
109
- instances = results.scalars().all()
110
- _log_commit(session, f"[{{ schema_name }}] ✅ Encontrados {len(instances)} registros {{ model.name }}")
111
- return [
112
- {{ model.name }}Read.from_instance(
113
- instance,
114
- {% if not model.is_view %}
115
- includes=includes,
116
- max_depth={{ max_depth }},
117
- {% endif %}
118
- include_vectors=include_vectors,
119
- )
120
- for instance in instances
121
- ]
90
+ # Sin recursión es la consulta normal, con o sin similitud
91
+ return {{ await_keyword -}} _execute_flat(session)
122
92
  {% else %}
93
+ results = {{ await_keyword -}} session.execute(query)
123
94
  instances = results.scalars().all()
124
95
 
125
96
  _log_commit(session, f"[{{ schema_name }}] ✅ Encontrados {len(instances)} registros {{ model.name }}")
@@ -129,7 +100,10 @@ tree_depth: Optional[int] = None,
129
100
  instance,
130
101
  {% if not model.is_view %}
131
102
  includes=includes,
132
- max_depth={{ max_depth }}
103
+ max_depth={{ max_depth }},
104
+ {% endif %}
105
+ {% if model.columns | selectattr('type', 'equalto', 'vector') | list %}
106
+ include_vectors=include_vectors,
133
107
  {% endif %}
134
108
  )
135
109
  for instance in instances
@@ -270,6 +270,11 @@
270
270
 
271
271
  # Ejecutar query
272
272
  {% if model.has_self_reference %}
273
+ {% if vector_cols_with_encoder %}
274
+ {# El modo `flat` de la jerarquía es la consulta vectorial de siempre: se emite con otro
275
+ nombre y el bloque jerárquico la llama, en vez de repetirla a medias. #}
276
+ {{ vector_macros.generate_vector_execute(model, max_depth, '_execute_flat') }}
277
+ {% endif %}
273
278
  {{ hierarchy_macros.generate_hierarchy_execute(model, max_depth) }}
274
279
  {% elif vector_cols_with_encoder %}
275
280
  {{ vector_macros.generate_vector_execute(model, max_depth) }}
@@ -285,7 +290,11 @@
285
290
  instance,
286
291
  {% if not model.is_view %}
287
292
  includes=includes,
288
- max_depth={{ max_depth }}
293
+ max_depth={{ max_depth }}{% if _find_many_vector_cols %},{% endif %}
294
+
295
+ {% endif %}
296
+ {% if _find_many_vector_cols %}
297
+ include_vectors=include_vectors,
289
298
  {% endif %}
290
299
  )
291
300
  for instance in instances
@@ -197,6 +197,11 @@
197
197
  if not match_fields:
198
198
  raise ValueError("match_fields no puede estar vacío. Debe contener al menos un campo para identificar registros.")
199
199
 
200
+ {% if model.columns | selectattr('type', 'equalto', 'vector') | selectattr('has_encoder') | selectattr('source') | list %}
201
+ # El camino masivo escribe diccionarios por Core, sin pasar por el modelo: los vectores
202
+ # se calculan antes, sobre los DTOs
203
+ {{ await_keyword -}} _encode_pending(records, '{{ model.name }}')
204
+ {% endif %}
200
205
  records_data = [record.to_dict() for record in records]
201
206
  {% if derived_columns %}
202
207
  records_data = cls._as_row_values(records_data)
@@ -110,7 +110,7 @@ vector_weights: Optional[Dict[str, float]] = None,
110
110
  {# Replaces the default scalars().all() block #}
111
111
  {# ============================================ #}
112
112
 
113
- {% macro generate_vector_execute(model, max_depth) -%}
113
+ {% macro generate_vector_execute(model, max_depth, fn_name='execute_query') -%}
114
114
  {% set vector_cols_with_encoder = [] -%}
115
115
  {% for vcol in model.columns | selectattr('type', 'equalto', 'vector') | list -%}
116
116
  {% if vcol.get('has_encoder') -%}
@@ -154,7 +154,7 @@ vector_weights: Optional[Dict[str, float]] = None,
154
154
  )
155
155
  {% endif %}
156
156
 
157
- {{ def_keyword }} execute_query(session: {{ session_type }}) -> List[{{ model.name }}Read]:
157
+ {{ def_keyword }} {{ fn_name }}(session: {{ session_type }}) -> List[{{ model.name }}Read]:
158
158
  results = {{ await_keyword -}} session.execute(query)
159
159
 
160
160
  if _has_vector_search:
@@ -153,6 +153,10 @@
153
153
  included = get_included_relations(new_instance, {{ model.tablename }})
154
154
  {{ await_keyword -}} cls._on_create_before(new_instance, included, session)
155
155
  session.add(new_instance)
156
+ {% if writes_vectors %}
157
+ # Después de los triggers `before`, que pueden cambiar el texto del que sale el vector
158
+ {{ await_keyword -}} _encode_pending(session.new)
159
+ {% endif %}
156
160
  {{ await_keyword -}} session.flush()
157
161
  {{ await_keyword -}} cls._on_create_after(new_instance, included, session)
158
162
  return {{ model.name }}Read.from_created_instance(new_instance, included)
@@ -196,22 +200,6 @@
196
200
  """
197
201
  _log_tx(session, f"[{{ schema_name }}] 🔢 Creando {len(records)} registros {{ model.name }}")
198
202
 
199
- {% set vcols_batch = model.columns | selectattr('type', 'equalto', 'vector') | selectattr('has_encoder') | selectattr('source') | list %}
200
- {% if vcols_batch %}
201
- # Batch encode any records with deferred vector fields (created via model_construct)
202
- {% for vcol in vcols_batch %}
203
- _batch_{{ vcol.name }}_idx = [i for i, r in enumerate(records) if r.{{ vcol.name }} is None and r.{{ vcol.source }} is not None]
204
- if _batch_{{ vcol.name }}_idx:
205
- _batch_{{ vcol.name }}_texts = [records[i].{{ vcol.source }} for i in _batch_{{ vcol.name }}_idx]
206
- {% if is_async %}
207
- _batch_{{ vcol.name }}_vecs = await _{{ vcol.name }}_encoder.aencode_batch(_batch_{{ vcol.name }}_texts)
208
- {% else %}
209
- _batch_{{ vcol.name }}_vecs = _{{ vcol.name }}_encoder.encode_batch(_batch_{{ vcol.name }}_texts)
210
- {% endif %}
211
- for _i, _vec in zip(_batch_{{ vcol.name }}_idx, _batch_{{ vcol.name }}_vecs):
212
- object.__setattr__(records[_i], '{{ vcol.name }}', _vec)
213
- {% endfor %}
214
- {% endif %}
215
203
  {% set operative_str_cols = model.columns | selectattr('is_operative') | selectattr('python_type', 'equalto', 'str') | list %}
216
204
  {{ def_keyword }} _execute_create_many(session: {{ session_type }}) -> int:
217
205
  instances = []
@@ -221,6 +209,10 @@
221
209
  {{ await_keyword -}} cls._on_create_before(instance, included, session)
222
210
  instances.append(instance)
223
211
  session.add_all(instances)
212
+ {% if writes_vectors %}
213
+ # Todos los vectores pendientes en una llamada por columna, anidados incluidos
214
+ {{ await_keyword -}} _encode_pending(session.new)
215
+ {% endif %}
224
216
  {{ await_keyword -}} session.flush()
225
217
  for instance in instances:
226
218
  included = get_included_relations(instance, record)
@@ -322,11 +314,10 @@
322
314
  {% for vcol in model.columns | selectattr('type', 'equalto', 'vector') | list %}
323
315
  {% if vcol.source and vcol.has_encoder %}
324
316
  if '{{ vcol.source }}' in update_data and '{{ vcol.name }}' not in update_data:
325
- {% if is_async %}
326
- update_data['{{ vcol.name }}'] = await _{{ vcol.name }}_encoder.aencode(update_data['{{ vcol.source }}'])
327
- {% else %}
328
- update_data['{{ vcol.name }}'] = _{{ vcol.name }}_encoder.encode(update_data['{{ vcol.source }}'])
329
- {% endif %}
317
+ update_data['{{ vcol.name }}'] = (
318
+ None if update_data['{{ vcol.source }}'] is None
319
+ else {{ await_keyword }}_{{ vcol.name }}_encoder.{{ 'aencode' if is_async else 'encode' }}(update_data['{{ vcol.source }}'])
320
+ )
330
321
  {% endif %}
331
322
  {% endfor %}
332
323
  for key, value in update_data.items():
@@ -520,6 +511,18 @@
520
511
 
521
512
  if not filters and not values: # Solo actualizar si hay filtros y valores
522
513
  return 0
514
+ {% for vcol in model.columns | selectattr('type', 'equalto', 'vector') | list %}
515
+ {% if vcol.source and vcol.has_encoder %}
516
+
517
+ # Un `UPDATE ... SET` no pasa por el modelo: si cambia el texto, el vector se calcula
518
+ # aquí. Es el mismo texto para todas las filas, así que es un solo vector.
519
+ if '{{ vcol.source }}' in values and '{{ vcol.name }}' not in values:
520
+ values['{{ vcol.name }}'] = (
521
+ None if values['{{ vcol.source }}'] is None
522
+ else {{ await_keyword }}_{{ vcol.name }}_encoder.{{ 'aencode' if is_async else 'encode' }}(values['{{ vcol.source }}'])
523
+ )
524
+ {% endif %}
525
+ {% endfor %}
523
526
 
524
527
  query = update({{ model.name }})
525
528
  query = payload.filter.apply_to_query(query)
@@ -2,14 +2,14 @@
2
2
  # No modifiques este archivo directamente
3
3
  #
4
4
  # _shared/utils/encoders.py
5
- # Encoder instances shared across all DAOs and DTOs in this schema.
5
+ # Encoder instances shared across all DAOs in this schema.
6
6
  # Each instance is named {tablename}_{colname}_encoder.
7
7
  #
8
8
  # All encoders use langchain as the unified embedding interface.
9
9
 
10
10
  from __future__ import annotations
11
11
  from abc import ABC, abstractmethod
12
- from typing import List
12
+ from typing import Any, Dict, Iterable, List, Optional, Tuple
13
13
  import asyncio
14
14
  import time
15
15
  import os
@@ -78,7 +78,7 @@ class HuggingFaceEncoder(BaseEncoder):
78
78
  except ImportError:
79
79
  raise ImportError(
80
80
  "langchain-huggingface is required for HuggingFaceEncoder. "
81
- "Install it with: pip install 'tai-sql[vectors]'"
81
+ "Install it in this project: pip install langchain-huggingface"
82
82
  )
83
83
  self._client = HuggingFaceEmbeddings(model_name=self._model_name)
84
84
  # Resolve dimensions from a test embedding if not provided
@@ -131,7 +131,7 @@ class OpenAIEncoder(BaseEncoder):
131
131
  except ImportError:
132
132
  raise ImportError(
133
133
  "langchain-openai is required for OpenAIEncoder. "
134
- "Install it with: pip install 'tai-sql[vectors]'"
134
+ "Install it in this project: pip install langchain-openai"
135
135
  )
136
136
  kwargs = {"model": self._model_name}
137
137
  if self._dimensions is not None:
@@ -205,7 +205,7 @@ class AzureOpenAIEncoder(BaseEncoder):
205
205
  except ImportError:
206
206
  raise ImportError(
207
207
  "langchain-openai is required for AzureOpenAIEncoder. "
208
- "Install it with: pip install 'tai-sql[vectors]'"
208
+ "Install it in this project: pip install langchain-openai"
209
209
  )
210
210
  kwargs = {"model": self._model_name}
211
211
  if self._dimensions is not None:
@@ -283,7 +283,7 @@ class GoogleEncoder(BaseEncoder):
283
283
  except ImportError:
284
284
  raise ImportError(
285
285
  "langchain-google-genai is required for GoogleEncoder. "
286
- "Install it with: pip install 'tai-sql[vectors]'"
286
+ "Install it in this project: pip install langchain-google-genai"
287
287
  )
288
288
  kwargs = {"model": self._model_name}
289
289
  if self._dimensions is not None:
@@ -340,3 +340,63 @@ class GoogleEncoder(BaseEncoder):
340
340
  {% endif %}
341
341
  {% endfor %}
342
342
  {% endfor %}
343
+
344
+
345
+ # ── Vectors computed on write ─────────────────────────────────────────────────
346
+ # Per model: (vector column, source column, encoder). Only the vector columns that
347
+ # declare a `source` are computed on write; the rest are written as given.
348
+ VECTOR_SOURCES: Dict[str, Tuple[Tuple[str, str, BaseEncoder], ...]] = {
349
+ {% for model in models %}
350
+ {% set _sources = [] %}
351
+ {% for col in model.columns %}
352
+ {% if col.type == 'vector' and col.has_encoder and col.encoder_config and col.source %}
353
+ {% set _ = _sources.append(col) %}
354
+ {% endif %}
355
+ {% endfor %}
356
+ {% if _sources %}
357
+ '{{ model.name }}': (
358
+ {% for col in _sources %}
359
+ ('{{ col.name }}', '{{ col.source }}', {{ model.tablename }}_{{ col.name }}_encoder),
360
+ {% endfor %}
361
+ ),
362
+ {% endif %}
363
+ {% endfor %}
364
+ }
365
+
366
+
367
+ def _pending(objects: Iterable[Any], model_name: Optional[str]) -> List[Tuple[BaseEncoder, str, str, List[Any]]]:
368
+ """
369
+ Groups the objects whose vector is missing but whose source has a value, by column.
370
+
371
+ `model_name` is for objects that are not instances of the model —the `Create`
372
+ DTOs that `upsert_many` writes as dicts—; without it, each object is looked up by
373
+ its class name, which is what `session.new` holds.
374
+ """
375
+ groups: Dict[Tuple[str, str], Tuple[BaseEncoder, str, str, List[Any]]] = {}
376
+ for obj in objects:
377
+ for column, source, encoder in VECTOR_SOURCES.get(model_name or type(obj).__name__, ()):
378
+ if getattr(obj, column, None) is None and getattr(obj, source, None) is not None:
379
+ key = (model_name or type(obj).__name__, column)
380
+ groups.setdefault(key, (encoder, column, source, []))[3].append(obj)
381
+ return list(groups.values())
382
+
383
+
384
+ def encode_pending(objects: Iterable[Any], model_name: Optional[str] = None) -> None:
385
+ """
386
+ Computes the vectors that are missing, one `encode_batch` per column.
387
+
388
+ It is what the DAOs call right before writing —on `session.new` before the flush,
389
+ so nested rows are included—: a DTO does not encode when it is built.
390
+ """
391
+ for encoder, column, source, objs in _pending(list(objects), model_name):
392
+ vectors = encoder.encode_batch([getattr(o, source) for o in objs])
393
+ for obj, vector in zip(objs, vectors):
394
+ setattr(obj, column, vector)
395
+
396
+
397
+ async def aencode_pending(objects: Iterable[Any], model_name: Optional[str] = None) -> None:
398
+ """`encode_pending` for the async DAOs: it awaits the encoder instead of blocking the loop."""
399
+ for encoder, column, source, objs in _pending(list(objects), model_name):
400
+ vectors = await encoder.aencode_batch([getattr(o, source) for o in objs])
401
+ for obj, vector in zip(objs, vectors):
402
+ setattr(obj, column, vector)
@@ -12,6 +12,9 @@ ninguna capa de despacho — el coste es cero.
12
12
  (metadato puro) → transformer → AST reescrito
13
13
  → renderer → dict de contexto para Jinja2
14
14
 
15
+ `scopes` lo usan los dos primeros: dice qué `t` es el parámetro y cuál es otra variable
16
+ con el mismo nombre en un ámbito interno.
17
+
15
18
  Este código vivía en `orm/triggers.py` (1.300 de sus 2.092 líneas). Está aquí porque
16
19
  conoce los nombres de los artefactos generados (`{Model}SyncDAO`, `{Model}Create`…),
17
20
  que son conocimiento del generador, no de la definición del schema.