tai-sql 0.7.11__tar.gz → 0.7.12__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {tai_sql-0.7.11 → tai_sql-0.7.12}/PKG-INFO +1 -1
- {tai_sql-0.7.11 → tai_sql-0.7.12}/pyproject.toml +1 -1
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_pull/funcs.py +79 -3
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_pull/main.py +68 -10
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_rules/main.py +59 -4
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/generators/python_client/templates/base.py.jinja2 +20 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/generators/python_client/templates/domain/dao.py.jinja2 +3 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/generators/python_client/templates/facade.py.jinja2 +2 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/generators/python_client/triggers/transformer.py +8 -5
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/generators/rules/__init__.py +6 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/generators/rules/templates/schema.md.jinja2 +6 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/introspect/README.md +3 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/introspect/build.py +19 -2
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/project/paths.py +37 -6
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/rules/README.md +19 -1
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/rules/__init__.py +12 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/rules/catalog.py +26 -9
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/rules/installer.py +79 -5
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/rules/service.py +41 -1
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/rules/staleness.py +7 -3
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/rules/static/comandos.md +11 -2
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/rules/static/modelo.md +5 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/sync/README.md +1 -1
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/sync/diff/__init__.py +3 -1
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/sync/diff/tables.py +6 -3
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/sync/ownership.py +25 -1
- {tai_sql-0.7.11 → tai_sql-0.7.12}/LICENSE +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/README.md +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/__init__.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/cli/commands/__init__.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_backup/__init__.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_backup/main.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_deploy/__init__.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_deploy/config.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_deploy/local.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_deploy/main.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_deploy/workflow.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_feed/__init__.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_feed/main.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_generate/__init__.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_generate/funcs.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_generate/main.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_hash_migrate/__init__.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_hash_migrate/main.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_info/__init__.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_info/main.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_info/status.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_init/__init__.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_init/main.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_init/model.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_install/__init__.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_install/main.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_ping/__init__.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_ping/main.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_pull/__init__.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_restore/__init__.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_restore/main.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_rules/__init__.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_schema/__init__.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_schema/model.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_schema/new.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_schema/schemas/__init__.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_schema/schemas/auth.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_schema/schemas/base.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_schema/schemas/chatbot.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_schema/schemas/default.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_schema/set_default.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/cli/commands/prompts.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/cli/commands/utils/__init__.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/cli/commands/utils/connectivity.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/cli/commands/utils/create.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/cli/commands/utils/github/__init__.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/cli/commands/utils/github/auth.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/cli/commands/utils/github/client.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/cli/commands/utils/github/models.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/cli/errors.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/cli/main.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/cli/preamble.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/cli/services/__init__.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/cli/services/backup.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/connection/README.md +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/connection/__init__.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/connection/provider.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/connection/recipe.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/connection/session.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/connection/sources.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/crypto.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/drivers/__init__.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/drivers/base.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/drivers/bigquery.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/drivers/capabilities.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/drivers/mysql.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/drivers/postgresql.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/drivers/sqlserver.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/errors/README.md +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/errors/__init__.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/errors/collector.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/errors/render.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/errors/types.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/feed/README.md +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/feed/__init__.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/feed/collect.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/feed/executor.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/feed/feeder.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/feed/plan.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/feed/presenter.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/feed/records.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/feed/transform.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/generators/__init__.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/generators/base.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/generators/diagram/README.md +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/generators/diagram/__init__.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/generators/diagram/assets/README.md +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/generators/diagram/assets/cytoscape.min.js +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/generators/diagram/builder.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/generators/diagram/generator.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/generators/diagram/graphviz_render.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/generators/diagram/html_render.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/generators/diagram/layout.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/generators/diagram/model.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/generators/diagram/templates/er.html.jinja2 +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/generators/diagram/themes.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/generators/python_client/__init__.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/generators/python_client/calculated.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/generators/python_client/context.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/generators/python_client/templates/domain/__init__.py.jinja2 +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/generators/python_client/templates/domain/dtos.py.jinja2 +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/generators/python_client/templates/domain/model.py.jinja2 +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/generators/python_client/templates/macros/aggregation.jinja2 +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/generators/python_client/templates/macros/dataframe.jinja2 +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/generators/python_client/templates/macros/filters.jinja2 +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/generators/python_client/templates/macros/hierarchy.jinja2 +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/generators/python_client/templates/macros/read.jinja2 +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/generators/python_client/templates/macros/triggers.jinja2 +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/generators/python_client/templates/macros/upsert.jinja2 +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/generators/python_client/templates/macros/vector.jinja2 +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/generators/python_client/templates/macros/write.jinja2 +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/generators/python_client/templates/session.py.jinja2 +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/generators/python_client/templates/shared/__init__.py.jinja2 +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/generators/python_client/templates/shared/aggregation.py.jinja2 +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/generators/python_client/templates/shared/context.py.jinja2 +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/generators/python_client/templates/shared/utils/__init__.py.jinja2 +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/generators/python_client/templates/shared/utils/encoders.py.jinja2 +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/generators/python_client/templates/shared/utils/funcs.py.jinja2 +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/generators/python_client/templates/shared/utils/hierarchy.py.jinja2 +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/generators/python_client/templates/shared/utils/rls.py.jinja2 +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/generators/python_client/templates/shared/vector.py.jinja2 +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/generators/python_client/triggers/__init__.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/generators/python_client/triggers/analyzer.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/generators/python_client/triggers/renderer.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/generators/python_client/triggers/steps.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/introspect/__init__.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/introspect/blueprint.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/introspect/defaults.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/introspect/naming.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/introspect/reader.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/introspect/templates/schema.py.jinja2 +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/introspect/types.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/introspect/writer.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/orm/README.md +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/orm/__init__.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/orm/analysis/README.md +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/orm/analysis/__init__.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/orm/analysis/calculated.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/orm/analysis/collector.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/orm/analysis/column_analyzer.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/orm/analysis/constraints.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/orm/analysis/hints.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/orm/analysis/relation_analyzer.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/orm/analysis/schema_analyzer.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/orm/declarative/README.md +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/orm/declarative/__init__.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/orm/declarative/annotations.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/orm/declarative/base.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/orm/declarative/calculated.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/orm/declarative/columns.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/orm/declarative/constraints.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/orm/declarative/datasource.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/orm/declarative/descriptors.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/orm/declarative/encoders.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/orm/declarative/enumerations.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/orm/declarative/relations.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/orm/declarative/table.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/orm/declarative/trigger_api.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/orm/declarative/triggers.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/orm/declarative/view.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/orm/mapping/README.md +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/orm/mapping/__init__.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/orm/mapping/sqlalchemy_mapper.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/orm/model/README.md +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/orm/model/__init__.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/orm/model/column.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/orm/model/constraints.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/orm/model/entity.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/orm/model/enumeration.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/orm/model/relation.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/orm/model/schema.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/orm/model/trigger.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/orm/naming.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/orm/ordering.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/orm/registry.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/orm/types.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/project/README.md +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/project/__init__.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/project/config.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/project/context.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/project/manager.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/rules/meta.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/rules/static/errores.md +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/schema.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/sync/__init__.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/sync/diff/constraints.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/sync/diff/indexes.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/sync/diff/views.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/sync/executor.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/sync/plan/__init__.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/sync/plan/planner.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/sync/plan/statements.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/sync/presenter.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/sync/report.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/sync/safety.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/sync/state/__init__.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/sync/state/local.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/sync/state/model.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/sync/state/normalize.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/sync/state/remote.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/sync/synchronizer.py +0 -0
|
@@ -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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
215
|
+
motivo = '; '.join(m for m in (informe.reason(), antigua) if m)
|
|
216
|
+
click.echo(f"❌ Las reglas no están al día: {motivo}")
|
|
167
217
|
click.echo()
|
|
168
218
|
|
|
169
219
|
if informe.installed and informe.installed_version:
|
|
@@ -208,6 +258,11 @@ def rules_diff(schema: Optional[str]):
|
|
|
208
258
|
destino, _contexts(nombres), track_orphans=not parcial
|
|
209
259
|
).inspect()
|
|
210
260
|
|
|
261
|
+
antigua = _legacy_reason()
|
|
262
|
+
if antigua:
|
|
263
|
+
click.echo(f"ℹ️ Además, {antigua}: 'tai-sql rules install' las retira.")
|
|
264
|
+
click.echo()
|
|
265
|
+
|
|
211
266
|
if informe.aligned:
|
|
212
267
|
click.echo(f"✅ No hay diferencias (tai-sql {informe.version})")
|
|
213
268
|
return
|
|
@@ -18,6 +18,26 @@ import bcrypt
|
|
|
18
18
|
class Base(DeclarativeBase):
|
|
19
19
|
pass
|
|
20
20
|
|
|
21
|
+
|
|
22
|
+
class TriggerAborted(ValueError):
|
|
23
|
+
"""
|
|
24
|
+
Un trigger del schema ha detenido la operación con `t.abort(mensaje)`.
|
|
25
|
+
|
|
26
|
+
Es una regla de negocio que no se cumple, no un fallo: quien la reciba tiene que poder
|
|
27
|
+
decírselo al usuario tal cual. `message` es el texto que escribió el trigger, sin prefijo.
|
|
28
|
+
|
|
29
|
+
Hereda de `ValueError` porque es lo que se lanzaba antes, y el código que ya la captura
|
|
30
|
+
así sigue funcionando.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
#: Para reconocerla sin importar la clase: tai-api atiende a los clientes de varios schemas
|
|
34
|
+
#: con un solo manejador, y cada uno tiene la suya.
|
|
35
|
+
tai_trigger_abort = True
|
|
36
|
+
|
|
37
|
+
def __init__(self, message: str):
|
|
38
|
+
self.message = message
|
|
39
|
+
super().__init__(f"Trigger abortado: {message}")
|
|
40
|
+
|
|
21
41
|
{% if has_encrypted_columns %}
|
|
22
42
|
|
|
23
43
|
# Configuración de encriptación
|
{tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/generators/python_client/templates/domain/dao.py.jinja2
RENAMED
|
@@ -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([]) %}
|
{tai_sql-0.7.11 → tai_sql-0.7.12}/tai_sql/generators/python_client/templates/facade.py.jinja2
RENAMED
|
@@ -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
|
|
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
|
|
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='
|
|
275
|
-
args=[
|
|
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
|
-
|
|
126
|
+
# Por nombre, no por clave: las claves de `state.tables` llevan el schema delante.
|
|
127
|
+
existentes = {s.name for s in state.tables.values()}
|
|
128
|
+
for tabla in sorted(excluidas - existentes):
|
|
112
129
|
descartes.append(Discarded(
|
|
113
130
|
kind='tabla', location=tabla, detail=f'tabla {tabla}',
|
|
114
131
|
impact=Impact.IGNORED,
|
|
@@ -58,23 +58,54 @@ class ProjectPaths:
|
|
|
58
58
|
"""Ruta del fichero de un schema, exista o no."""
|
|
59
59
|
return self.schemas_dir / f'{schema_name}.py'
|
|
60
60
|
|
|
61
|
+
@property
|
|
62
|
+
def workspace_root(self) -> Path:
|
|
63
|
+
"""
|
|
64
|
+
La raíz del repositorio que contiene el proyecto: el primer directorio, desde
|
|
65
|
+
`root` hacia arriba, que tiene un `.git`. Sin repositorio, el propio proyecto.
|
|
66
|
+
|
|
67
|
+
Es donde se arranca el asistente, y por eso es donde tienen que estar sus reglas.
|
|
68
|
+
El proyecto de tai-sql suele ser una carpeta del repositorio (`database/`, junto a
|
|
69
|
+
`api/` y al frontend), y un asistente **no carga al arrancar** el `.claude/rules/`
|
|
70
|
+
de una subcarpeta: solo cuando lee un fichero de dentro. Las reglas instaladas en
|
|
71
|
+
`database/.claude/rules/` no llegaban a quien trabajaba en la API o en el
|
|
72
|
+
frontend, que es justo quien más las necesita.
|
|
73
|
+
"""
|
|
74
|
+
inicio = self.root.resolve()
|
|
75
|
+
for directorio in (inicio, *inicio.parents):
|
|
76
|
+
# `exists()` y no `is_dir()`: en un worktree o un submódulo `.git` es un fichero.
|
|
77
|
+
if (directorio / '.git').exists():
|
|
78
|
+
return directorio
|
|
79
|
+
return inicio
|
|
80
|
+
|
|
61
81
|
@property
|
|
62
82
|
def rules_dir(self) -> Path:
|
|
63
|
-
"""Directorio de reglas de contexto del
|
|
64
|
-
return self.
|
|
83
|
+
"""Directorio de reglas de contexto del repositorio (`<raíz>/.claude/rules`)."""
|
|
84
|
+
return self.workspace_root / self.RULES_DIR
|
|
65
85
|
|
|
66
86
|
@property
|
|
67
87
|
def platform_rules_dir(self) -> Path:
|
|
68
88
|
"""
|
|
69
89
|
Directorio donde `tai-sql rules install` escribe las reglas de la herramienta.
|
|
70
90
|
|
|
71
|
-
Es
|
|
72
|
-
frontera entre lo que instala tai-sql —borrable y
|
|
73
|
-
el equipo. Sin ella se reproduce el problema del
|
|
74
|
-
la siguiente instalación lo pisa, y nadie se
|
|
91
|
+
Es `<raíz del repositorio>/.claude/rules/_platform/tai-sql`, y el `_platform/` no es
|
|
92
|
+
organización: es la frontera entre lo que instala tai-sql —borrable y
|
|
93
|
+
reinstalable— y lo que escribe el equipo. Sin ella se reproduce el problema del
|
|
94
|
+
código generado: alguien edita, la siguiente instalación lo pisa, y nadie se
|
|
95
|
+
entera. Ver `workspace_root` para por qué la raíz del repositorio y no el proyecto.
|
|
75
96
|
"""
|
|
76
97
|
return self.rules_dir / self.PLATFORM_RULES_DIR
|
|
77
98
|
|
|
99
|
+
@property
|
|
100
|
+
def legacy_platform_rules_dir(self) -> Optional[Path]:
|
|
101
|
+
"""
|
|
102
|
+
Donde las instalaban las versiones anteriores a la 0.7.12: dentro del proyecto de
|
|
103
|
+
tai-sql. `None` si coincide con la ubicación actual —el proyecto es la raíz del
|
|
104
|
+
repositorio— y no hay nada que retirar.
|
|
105
|
+
"""
|
|
106
|
+
antigua = self.root.resolve() / self.RULES_DIR / self.PLATFORM_RULES_DIR
|
|
107
|
+
return None if antigua == self.platform_rules_dir else antigua
|
|
108
|
+
|
|
78
109
|
# ── Descubrimiento ───────────────────────────────────────────────────
|
|
79
110
|
|
|
80
111
|
def discover_schemas(self) -> List[str]:
|
|
@@ -37,7 +37,7 @@ texto, que es la forma preferente de añadir cobertura aquí.
|
|
|
37
37
|
## Dónde se instalan, y por qué ahí
|
|
38
38
|
|
|
39
39
|
```
|
|
40
|
-
|
|
40
|
+
<raíz del repositorio>/.claude/rules/
|
|
41
41
|
├── _platform/ instalado — NO se edita, es borrable y reinstalable
|
|
42
42
|
│ └── tai-sql/
|
|
43
43
|
│ ├── _meta.json
|
|
@@ -52,6 +52,24 @@ La separación física de `_platform/` es un requisito y no organización: sin e
|
|
|
52
52
|
reproduce el problema que ya tiene el código generado — alguien edita, la siguiente
|
|
53
53
|
instalación lo pisa, y nadie se entera.
|
|
54
54
|
|
|
55
|
+
**En la raíz del repositorio, no en el proyecto de tai-sql** (`ProjectPaths.workspace_root`:
|
|
56
|
+
el primer directorio hacia arriba con `.git`; sin repositorio, el proyecto). El proyecto suele
|
|
57
|
+
ser una carpeta —`database/`, junto a `api/` y al frontend—, y el asistente carga al arrancar el
|
|
58
|
+
`.claude/rules/` de la raíz pero el de una subcarpeta **solo cuando lee un fichero de dentro**
|
|
59
|
+
(comprobado con Claude Code 2.1). Hasta la 0.7.11 se instalaban en el proyecto, y quien
|
|
60
|
+
trabajaba en la API no las veía. `rules install` retira lo que quede allí con
|
|
61
|
+
`retire_directory()`, que borra lo intacto y deja lo editado; `check` y el aviso de `push` lo
|
|
62
|
+
señalan mientras quede algo. El documento derivado dice dónde está el proyecto dentro del
|
|
63
|
+
repositorio, porque sus rutas son relativas a él.
|
|
64
|
+
|
|
65
|
+
## Reutilizarlo desde otra librería
|
|
66
|
+
|
|
67
|
+
`RulesInstaller` no sabe de schemas, y tampoco de quién es: `tool='tai-api'` hace que la
|
|
68
|
+
cabecera diga `<!-- tai-api <versión> · … · regenerar: tai-api rules install -->` y que la
|
|
69
|
+
lectura reconozca esa cabecera y no la de tai-sql. Es lo que usa tai-api para instalar las suyas
|
|
70
|
+
en `_platform/tai-api/`, junto a estas. Lo que le toca a la otra librería: sus documentos, su
|
|
71
|
+
directorio, su versión y su aviso.
|
|
72
|
+
|
|
55
73
|
**Un fichero derivado por schema**, y no uno por proyecto. Lo que describe un schema no
|
|
56
74
|
tiene que ver con lo que describe otro, y separarlos permite dos cosas: que `push` avise de
|
|
57
75
|
que se quedó obsoleto **el del schema que acaba de tocar** sin cargar los demás, y que
|