tai-sql 0.7.11__tar.gz → 0.7.13__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {tai_sql-0.7.11 → tai_sql-0.7.13}/PKG-INFO +1 -1
- {tai_sql-0.7.11 → tai_sql-0.7.13}/pyproject.toml +1 -1
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/cli/commands/cmd_pull/funcs.py +79 -3
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/cli/commands/cmd_pull/main.py +68 -10
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/cli/commands/cmd_rules/main.py +59 -4
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/generators/python_client/context.py +6 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/generators/python_client/templates/base.py.jinja2 +20 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/generators/python_client/templates/domain/dao.py.jinja2 +3 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/generators/python_client/templates/domain/dtos.py.jinja2 +12 -2
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/generators/python_client/templates/domain/model.py.jinja2 +8 -1
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/generators/python_client/templates/facade.py.jinja2 +2 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/generators/python_client/triggers/transformer.py +8 -5
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/generators/rules/__init__.py +6 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/generators/rules/templates/schema.md.jinja2 +6 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/introspect/README.md +3 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/introspect/build.py +19 -2
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/project/paths.py +37 -6
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/rules/README.md +19 -1
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/rules/__init__.py +12 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/rules/catalog.py +26 -9
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/rules/installer.py +79 -5
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/rules/service.py +41 -1
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/rules/staleness.py +7 -3
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/rules/static/comandos.md +11 -2
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/rules/static/modelo.md +8 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/sync/README.md +1 -1
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/sync/diff/__init__.py +3 -1
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/sync/diff/tables.py +6 -3
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/sync/ownership.py +25 -1
- {tai_sql-0.7.11 → tai_sql-0.7.13}/LICENSE +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/README.md +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/__init__.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/cli/commands/__init__.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/cli/commands/cmd_backup/__init__.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/cli/commands/cmd_backup/main.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/cli/commands/cmd_deploy/__init__.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/cli/commands/cmd_deploy/config.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/cli/commands/cmd_deploy/local.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/cli/commands/cmd_deploy/main.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/cli/commands/cmd_deploy/workflow.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/cli/commands/cmd_feed/__init__.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/cli/commands/cmd_feed/main.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/cli/commands/cmd_generate/__init__.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/cli/commands/cmd_generate/funcs.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/cli/commands/cmd_generate/main.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/cli/commands/cmd_hash_migrate/__init__.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/cli/commands/cmd_hash_migrate/main.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/cli/commands/cmd_info/__init__.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/cli/commands/cmd_info/main.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/cli/commands/cmd_info/status.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/cli/commands/cmd_init/__init__.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/cli/commands/cmd_init/main.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/cli/commands/cmd_init/model.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/cli/commands/cmd_install/__init__.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/cli/commands/cmd_install/main.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/cli/commands/cmd_ping/__init__.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/cli/commands/cmd_ping/main.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/cli/commands/cmd_pull/__init__.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/cli/commands/cmd_restore/__init__.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/cli/commands/cmd_restore/main.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/cli/commands/cmd_rules/__init__.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/cli/commands/cmd_schema/__init__.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/cli/commands/cmd_schema/model.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/cli/commands/cmd_schema/new.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/cli/commands/cmd_schema/schemas/__init__.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/cli/commands/cmd_schema/schemas/auth.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/cli/commands/cmd_schema/schemas/base.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/cli/commands/cmd_schema/schemas/chatbot.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/cli/commands/cmd_schema/schemas/default.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/cli/commands/cmd_schema/set_default.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/cli/commands/prompts.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/cli/commands/utils/__init__.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/cli/commands/utils/connectivity.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/cli/commands/utils/create.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/cli/commands/utils/github/__init__.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/cli/commands/utils/github/auth.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/cli/commands/utils/github/client.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/cli/commands/utils/github/models.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/cli/errors.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/cli/main.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/cli/preamble.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/cli/services/__init__.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/cli/services/backup.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/connection/README.md +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/connection/__init__.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/connection/provider.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/connection/recipe.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/connection/session.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/connection/sources.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/crypto.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/drivers/__init__.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/drivers/base.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/drivers/bigquery.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/drivers/capabilities.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/drivers/mysql.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/drivers/postgresql.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/drivers/sqlserver.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/errors/README.md +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/errors/__init__.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/errors/collector.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/errors/render.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/errors/types.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/feed/README.md +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/feed/__init__.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/feed/collect.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/feed/executor.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/feed/feeder.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/feed/plan.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/feed/presenter.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/feed/records.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/feed/transform.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/generators/__init__.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/generators/base.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/generators/diagram/README.md +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/generators/diagram/__init__.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/generators/diagram/assets/README.md +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/generators/diagram/assets/cytoscape.min.js +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/generators/diagram/builder.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/generators/diagram/generator.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/generators/diagram/graphviz_render.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/generators/diagram/html_render.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/generators/diagram/layout.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/generators/diagram/model.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/generators/diagram/templates/er.html.jinja2 +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/generators/diagram/themes.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/generators/python_client/__init__.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/generators/python_client/calculated.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/generators/python_client/templates/domain/__init__.py.jinja2 +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/generators/python_client/templates/macros/aggregation.jinja2 +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/generators/python_client/templates/macros/dataframe.jinja2 +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/generators/python_client/templates/macros/filters.jinja2 +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/generators/python_client/templates/macros/hierarchy.jinja2 +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/generators/python_client/templates/macros/read.jinja2 +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/generators/python_client/templates/macros/triggers.jinja2 +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/generators/python_client/templates/macros/upsert.jinja2 +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/generators/python_client/templates/macros/vector.jinja2 +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/generators/python_client/templates/macros/write.jinja2 +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/generators/python_client/templates/session.py.jinja2 +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/generators/python_client/templates/shared/__init__.py.jinja2 +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/generators/python_client/templates/shared/aggregation.py.jinja2 +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/generators/python_client/templates/shared/context.py.jinja2 +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/generators/python_client/templates/shared/utils/__init__.py.jinja2 +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/generators/python_client/templates/shared/utils/encoders.py.jinja2 +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/generators/python_client/templates/shared/utils/funcs.py.jinja2 +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/generators/python_client/templates/shared/utils/hierarchy.py.jinja2 +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/generators/python_client/templates/shared/utils/rls.py.jinja2 +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/generators/python_client/templates/shared/vector.py.jinja2 +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/generators/python_client/triggers/__init__.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/generators/python_client/triggers/analyzer.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/generators/python_client/triggers/renderer.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/generators/python_client/triggers/steps.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/introspect/__init__.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/introspect/blueprint.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/introspect/defaults.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/introspect/naming.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/introspect/reader.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/introspect/templates/schema.py.jinja2 +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/introspect/types.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/introspect/writer.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/orm/README.md +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/orm/__init__.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/orm/analysis/README.md +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/orm/analysis/__init__.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/orm/analysis/calculated.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/orm/analysis/collector.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/orm/analysis/column_analyzer.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/orm/analysis/constraints.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/orm/analysis/hints.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/orm/analysis/relation_analyzer.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/orm/analysis/schema_analyzer.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/orm/declarative/README.md +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/orm/declarative/__init__.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/orm/declarative/annotations.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/orm/declarative/base.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/orm/declarative/calculated.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/orm/declarative/columns.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/orm/declarative/constraints.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/orm/declarative/datasource.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/orm/declarative/descriptors.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/orm/declarative/encoders.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/orm/declarative/enumerations.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/orm/declarative/relations.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/orm/declarative/table.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/orm/declarative/trigger_api.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/orm/declarative/triggers.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/orm/declarative/view.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/orm/mapping/README.md +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/orm/mapping/__init__.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/orm/mapping/sqlalchemy_mapper.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/orm/model/README.md +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/orm/model/__init__.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/orm/model/column.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/orm/model/constraints.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/orm/model/entity.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/orm/model/enumeration.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/orm/model/relation.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/orm/model/schema.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/orm/model/trigger.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/orm/naming.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/orm/ordering.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/orm/registry.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/orm/types.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/project/README.md +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/project/__init__.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/project/config.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/project/context.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/project/manager.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/rules/meta.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/rules/static/errores.md +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/schema.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/sync/__init__.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/sync/diff/constraints.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/sync/diff/indexes.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/sync/diff/views.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/sync/executor.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/sync/plan/__init__.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/sync/plan/planner.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/sync/plan/statements.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/sync/presenter.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/sync/report.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/sync/safety.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/sync/state/__init__.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/sync/state/local.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/sync/state/model.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/sync/state/normalize.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/sync/state/remote.py +0 -0
- {tai_sql-0.7.11 → tai_sql-0.7.13}/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
|
|
@@ -225,6 +225,12 @@ class TemplateContextBuilder:
|
|
|
225
225
|
'is_operative': column.is_operative,
|
|
226
226
|
'is_calculated': column.is_calculated,
|
|
227
227
|
'self_reference': column.self_reference,
|
|
228
|
+
# La rellena la base de datos (NOW()): no es obligatoria al crear. Faltaba, y las
|
|
229
|
+
# plantillas que la miraban la leían siempre como falsa.
|
|
230
|
+
'server_now': column.server_now,
|
|
231
|
+
# El tamaño máximo de un texto (`column(length=n)`), para quien valide antes de
|
|
232
|
+
# escribir —los esquemas de formulario de tai-api—. `None` si no se declaró.
|
|
233
|
+
'max_length': column.length if column.type == 'str' else None,
|
|
228
234
|
}
|
|
229
235
|
|
|
230
236
|
if column.is_vector:
|
|
@@ -18,6 +18,26 @@ import bcrypt
|
|
|
18
18
|
class Base(DeclarativeBase):
|
|
19
19
|
pass
|
|
20
20
|
|
|
21
|
+
|
|
22
|
+
class TriggerAborted(ValueError):
|
|
23
|
+
"""
|
|
24
|
+
Un trigger del schema ha detenido la operación con `t.abort(mensaje)`.
|
|
25
|
+
|
|
26
|
+
Es una regla de negocio que no se cumple, no un fallo: quien la reciba tiene que poder
|
|
27
|
+
decírselo al usuario tal cual. `message` es el texto que escribió el trigger, sin prefijo.
|
|
28
|
+
|
|
29
|
+
Hereda de `ValueError` porque es lo que se lanzaba antes, y el código que ya la captura
|
|
30
|
+
así sigue funcionando.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
#: Para reconocerla sin importar la clase: tai-api atiende a los clientes de varios schemas
|
|
34
|
+
#: con un solo manejador, y cada uno tiene la suya.
|
|
35
|
+
tai_trigger_abort = True
|
|
36
|
+
|
|
37
|
+
def __init__(self, message: str):
|
|
38
|
+
self.message = message
|
|
39
|
+
super().__init__(f"Trigger abortado: {message}")
|
|
40
|
+
|
|
21
41
|
{% if has_encrypted_columns %}
|
|
22
42
|
|
|
23
43
|
# Configuración de encriptación
|
{tai_sql-0.7.11 → tai_sql-0.7.13}/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.13}/tai_sql/generators/python_client/templates/domain/dtos.py.jinja2
RENAMED
|
@@ -247,7 +247,7 @@ class {{ model.name }}Read(PrettyModel):
|
|
|
247
247
|
class {{ model.name }}Create(PrettyModel):
|
|
248
248
|
"""DTO de escritura para {{ model.name }}."""
|
|
249
249
|
{% for column in model.columns -%}
|
|
250
|
-
{% if not column.constraints.get('autoincrement', False) and column.default is none and not column.nullable and not column.is_foreign_key and not column.is_operative and not column.has_encoder|default(false) and not column.is_calculated %}
|
|
250
|
+
{% if not column.constraints.get('autoincrement', False) and column.default is none and not column.nullable and not column.is_foreign_key and not column.is_operative and not column.has_encoder|default(false) and not column.is_calculated and not column.server_now %}
|
|
251
251
|
{{ column.name }}: {{ column.pydantic_type }}
|
|
252
252
|
{% endif %}
|
|
253
253
|
{% endfor %}
|
|
@@ -257,6 +257,9 @@ class {{ model.name }}Create(PrettyModel):
|
|
|
257
257
|
{{ column.name }}: Optional[{{ column.pydantic_type }}] = None
|
|
258
258
|
{% elif column.is_foreign_key %}
|
|
259
259
|
{{ column.name }}: Optional[{{ column.pydantic_type }}] = None
|
|
260
|
+
{% elif column.server_now %}
|
|
261
|
+
{# La rellena la base de datos con NOW() si no llega: no es obligatoria. #}
|
|
262
|
+
{{ column.name }}: Optional[{{ column.pydantic_type }}] = None
|
|
260
263
|
{% elif column.default is none and column.nullable %}
|
|
261
264
|
{{ column.name }}: Optional[{{ column.pydantic_type }}] = None
|
|
262
265
|
{% elif column.default is not none %}
|
|
@@ -295,11 +298,18 @@ class {{ model.name }}Create(PrettyModel):
|
|
|
295
298
|
def to_instance(self) -> {{ model.name }}:
|
|
296
299
|
model = {{ model.name }}(
|
|
297
300
|
{% for column in model.columns %}
|
|
298
|
-
{% if not column.constraints.get('autoincrement', False) and not column.is_operative and not column.is_calculated %}
|
|
301
|
+
{% if not column.constraints.get('autoincrement', False) and not column.is_operative and not column.is_calculated and not column.server_now %}
|
|
299
302
|
{{ column.name }}=self.{{ column.name }},
|
|
300
303
|
{% endif %}
|
|
301
304
|
{% endfor %}
|
|
302
305
|
)
|
|
306
|
+
{% for column in model.columns %}
|
|
307
|
+
{% if column.server_now and not column.is_operative and not column.is_calculated %}
|
|
308
|
+
# Solo si llega: asignarle None mandaría un NULL, y no dejaría a la base de datos poner NOW().
|
|
309
|
+
if self.{{ column.name }} is not None:
|
|
310
|
+
model.{{ column.name }} = self.{{ column.name }}
|
|
311
|
+
{% endif %}
|
|
312
|
+
{% endfor %}
|
|
303
313
|
{% for col in operative_str_cols %}
|
|
304
314
|
model.{{ col.name }} = get_username()
|
|
305
315
|
{% endfor %}
|
{tai_sql-0.7.11 → tai_sql-0.7.13}/tai_sql/generators/python_client/templates/domain/model.py.jinja2
RENAMED
|
@@ -83,6 +83,9 @@ from pgvector.sqlalchemy import Vector
|
|
|
83
83
|
{% if column.sa_type and column.sa_type not in sa_imports %}
|
|
84
84
|
{% set _ = sa_imports.append(column.sa_type) %}
|
|
85
85
|
{% endif %}
|
|
86
|
+
{% if column.server_now and 'func' not in sa_imports %}
|
|
87
|
+
{% set _ = sa_imports.append('func') %}
|
|
88
|
+
{% endif %}
|
|
86
89
|
{% endfor %}
|
|
87
90
|
{% if sa_imports %}
|
|
88
91
|
from sqlalchemy import {{ sa_imports | sort | join(', ') }}
|
|
@@ -165,7 +168,7 @@ class {{ model.name }}(Base):
|
|
|
165
168
|
{% set mapped_type = 'Optional[' + py_type + ']' if column.nullable else py_type %}
|
|
166
169
|
{# Determine if mapped_column() call is needed #}
|
|
167
170
|
{% set has_constraints = column.constraints.values() | select | list | length > 0 %}
|
|
168
|
-
{% set needs_mapped_column = has_constraints or column.encrypt or column.hash or column.is_calculated or column.sa_type is not none or column.type == 'vector' %}
|
|
171
|
+
{% set needs_mapped_column = has_constraints or column.encrypt or column.hash or column.is_calculated or column.sa_type is not none or column.type == 'vector' or column.server_now %}
|
|
169
172
|
{% if needs_mapped_column %}
|
|
170
173
|
{% set mc_args = [] %}
|
|
171
174
|
{% if column.type == 'vector' %}{% set _ = mc_args.append('Vector(' + column.dimensions|string + ')') %}
|
|
@@ -185,6 +188,10 @@ class {{ model.name }}(Base):
|
|
|
185
188
|
{% endfor %}
|
|
186
189
|
{% endif %}
|
|
187
190
|
{% if column.encrypt or column.hash %}{% set _ = mc_args.append('name="' + column.name + '"') %}{% endif %}
|
|
191
|
+
{# La pone la base de datos. Sin declararlo aquí, SQLAlchemy la enviaba como NULL —rellena
|
|
192
|
+
con None lo que no tiene valor para poder agrupar el INSERT— y la base de datos no llegaba
|
|
193
|
+
a poner NOW(). #}
|
|
194
|
+
{% if column.server_now %}{% set _ = mc_args.append('server_default=func.now()') %}{% endif %}
|
|
188
195
|
{% if column.is_calculated %}{% set _ = mc_args.append('name="' + column.name + '"') %}{% endif %}
|
|
189
196
|
{{ col_attr }}: Mapped[{{ mapped_type }}] = mapped_column({{ mc_args | join(', ') }})
|
|
190
197
|
{% else %}
|
{tai_sql-0.7.11 → tai_sql-0.7.13}/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,
|