tai-sql 0.7.10__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.10 → tai_sql-0.7.12}/PKG-INFO +1 -1
- {tai_sql-0.7.10 → tai_sql-0.7.12}/pyproject.toml +1 -1
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/__init__.py +4 -1
- tai_sql-0.7.12/tai_sql/cli/commands/cmd_pull/funcs.py +415 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_pull/main.py +75 -10
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_rules/main.py +59 -4
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/drivers/base.py +111 -4
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/drivers/bigquery.py +12 -2
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/drivers/mysql.py +155 -4
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/drivers/postgresql.py +3 -2
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/drivers/sqlserver.py +3 -2
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/generators/python_client/context.py +25 -1
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/generators/python_client/templates/base.py.jinja2 +20 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/generators/python_client/templates/domain/dao.py.jinja2 +4 -1
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/generators/python_client/templates/domain/dtos.py.jinja2 +1 -1
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/generators/python_client/templates/domain/model.py.jinja2 +2 -2
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/generators/python_client/templates/facade.py.jinja2 +2 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/generators/python_client/triggers/transformer.py +8 -5
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/generators/rules/__init__.py +6 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/generators/rules/templates/schema.md.jinja2 +6 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/introspect/README.md +35 -14
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/introspect/blueprint.py +6 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/introspect/build.py +50 -32
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/introspect/defaults.py +60 -4
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/introspect/naming.py +35 -9
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/introspect/templates/schema.py.jinja2 +7 -3
- tai_sql-0.7.12/tai_sql/introspect/types.py +228 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/introspect/writer.py +12 -3
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/orm/__init__.py +2 -1
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/orm/analysis/column_analyzer.py +81 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/orm/analysis/relation_analyzer.py +19 -1
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/orm/analysis/schema_analyzer.py +6 -6
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/orm/declarative/columns.py +21 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/orm/mapping/sqlalchemy_mapper.py +7 -1
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/orm/model/column.py +11 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/orm/model/relation.py +23 -8
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/orm/types.py +159 -1
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/project/paths.py +37 -6
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/rules/README.md +19 -1
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/rules/__init__.py +12 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/rules/catalog.py +26 -9
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/rules/installer.py +79 -5
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/rules/service.py +41 -1
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/rules/staleness.py +7 -3
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/rules/static/comandos.md +52 -7
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/rules/static/errores.md +16 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/rules/static/modelo.md +56 -3
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/schema.py +29 -16
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/sync/README.md +1 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/sync/diff/__init__.py +3 -1
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/sync/diff/tables.py +17 -23
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/sync/diff/views.py +3 -0
- tai_sql-0.7.12/tai_sql/sync/ownership.py +74 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/sync/plan/planner.py +1 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/sync/state/normalize.py +68 -1
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/sync/state/remote.py +3 -2
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/sync/synchronizer.py +3 -1
- tai_sql-0.7.10/tai_sql/cli/commands/cmd_pull/funcs.py +0 -232
- tai_sql-0.7.10/tai_sql/introspect/types.py +0 -143
- {tai_sql-0.7.10 → tai_sql-0.7.12}/LICENSE +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/README.md +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/cli/commands/__init__.py +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_backup/__init__.py +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_backup/main.py +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_deploy/__init__.py +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_deploy/config.py +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_deploy/local.py +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_deploy/main.py +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_deploy/workflow.py +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_feed/__init__.py +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_feed/main.py +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_generate/__init__.py +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_generate/funcs.py +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_generate/main.py +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_hash_migrate/__init__.py +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_hash_migrate/main.py +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_info/__init__.py +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_info/main.py +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_info/status.py +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_init/__init__.py +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_init/main.py +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_init/model.py +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_install/__init__.py +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_install/main.py +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_ping/__init__.py +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_ping/main.py +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_pull/__init__.py +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_restore/__init__.py +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_restore/main.py +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_rules/__init__.py +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_schema/__init__.py +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_schema/model.py +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_schema/new.py +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_schema/schemas/__init__.py +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_schema/schemas/auth.py +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_schema/schemas/base.py +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_schema/schemas/chatbot.py +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_schema/schemas/default.py +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/cli/commands/cmd_schema/set_default.py +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/cli/commands/prompts.py +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/cli/commands/utils/__init__.py +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/cli/commands/utils/connectivity.py +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/cli/commands/utils/create.py +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/cli/commands/utils/github/__init__.py +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/cli/commands/utils/github/auth.py +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/cli/commands/utils/github/client.py +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/cli/commands/utils/github/models.py +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/cli/errors.py +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/cli/main.py +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/cli/preamble.py +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/cli/services/__init__.py +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/cli/services/backup.py +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/connection/README.md +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/connection/__init__.py +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/connection/provider.py +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/connection/recipe.py +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/connection/session.py +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/connection/sources.py +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/crypto.py +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/drivers/__init__.py +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/drivers/capabilities.py +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/errors/README.md +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/errors/__init__.py +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/errors/collector.py +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/errors/render.py +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/errors/types.py +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/feed/README.md +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/feed/__init__.py +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/feed/collect.py +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/feed/executor.py +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/feed/feeder.py +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/feed/plan.py +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/feed/presenter.py +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/feed/records.py +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/feed/transform.py +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/generators/__init__.py +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/generators/base.py +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/generators/diagram/README.md +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/generators/diagram/__init__.py +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/generators/diagram/assets/README.md +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/generators/diagram/assets/cytoscape.min.js +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/generators/diagram/builder.py +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/generators/diagram/generator.py +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/generators/diagram/graphviz_render.py +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/generators/diagram/html_render.py +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/generators/diagram/layout.py +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/generators/diagram/model.py +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/generators/diagram/templates/er.html.jinja2 +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/generators/diagram/themes.py +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/generators/python_client/__init__.py +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/generators/python_client/calculated.py +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/generators/python_client/templates/domain/__init__.py.jinja2 +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/generators/python_client/templates/macros/aggregation.jinja2 +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/generators/python_client/templates/macros/dataframe.jinja2 +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/generators/python_client/templates/macros/filters.jinja2 +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/generators/python_client/templates/macros/hierarchy.jinja2 +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/generators/python_client/templates/macros/read.jinja2 +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/generators/python_client/templates/macros/triggers.jinja2 +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/generators/python_client/templates/macros/upsert.jinja2 +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/generators/python_client/templates/macros/vector.jinja2 +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/generators/python_client/templates/macros/write.jinja2 +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/generators/python_client/templates/session.py.jinja2 +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/generators/python_client/templates/shared/__init__.py.jinja2 +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/generators/python_client/templates/shared/aggregation.py.jinja2 +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/generators/python_client/templates/shared/context.py.jinja2 +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/generators/python_client/templates/shared/utils/__init__.py.jinja2 +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/generators/python_client/templates/shared/utils/encoders.py.jinja2 +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/generators/python_client/templates/shared/utils/funcs.py.jinja2 +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/generators/python_client/templates/shared/utils/hierarchy.py.jinja2 +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/generators/python_client/templates/shared/utils/rls.py.jinja2 +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/generators/python_client/templates/shared/vector.py.jinja2 +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/generators/python_client/triggers/__init__.py +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/generators/python_client/triggers/analyzer.py +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/generators/python_client/triggers/renderer.py +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/generators/python_client/triggers/steps.py +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/introspect/__init__.py +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/introspect/reader.py +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/orm/README.md +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/orm/analysis/README.md +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/orm/analysis/__init__.py +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/orm/analysis/calculated.py +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/orm/analysis/collector.py +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/orm/analysis/constraints.py +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/orm/analysis/hints.py +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/orm/declarative/README.md +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/orm/declarative/__init__.py +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/orm/declarative/annotations.py +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/orm/declarative/base.py +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/orm/declarative/calculated.py +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/orm/declarative/constraints.py +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/orm/declarative/datasource.py +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/orm/declarative/descriptors.py +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/orm/declarative/encoders.py +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/orm/declarative/enumerations.py +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/orm/declarative/relations.py +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/orm/declarative/table.py +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/orm/declarative/trigger_api.py +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/orm/declarative/triggers.py +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/orm/declarative/view.py +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/orm/mapping/README.md +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/orm/mapping/__init__.py +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/orm/model/README.md +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/orm/model/__init__.py +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/orm/model/constraints.py +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/orm/model/entity.py +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/orm/model/enumeration.py +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/orm/model/schema.py +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/orm/model/trigger.py +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/orm/naming.py +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/orm/ordering.py +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/orm/registry.py +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/project/README.md +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/project/__init__.py +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/project/config.py +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/project/context.py +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/project/manager.py +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/rules/meta.py +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/sync/__init__.py +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/sync/diff/constraints.py +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/sync/diff/indexes.py +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/sync/executor.py +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/sync/plan/__init__.py +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/sync/plan/statements.py +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/sync/presenter.py +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/sync/report.py +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/sync/safety.py +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/sync/state/__init__.py +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/sync/state/local.py +0 -0
- {tai_sql-0.7.10 → tai_sql-0.7.12}/tai_sql/sync/state/model.py +0 -0
|
@@ -6,6 +6,7 @@ models using SQLAlchemy's declarative system.
|
|
|
6
6
|
from __future__ import annotations
|
|
7
7
|
from sqlalchemy.types import (
|
|
8
8
|
BigInteger as bigint,
|
|
9
|
+
SmallInteger as smallint,
|
|
9
10
|
Text as text,
|
|
10
11
|
Numeric as numeric,
|
|
11
12
|
LargeBinary as largebinary,
|
|
@@ -25,7 +26,7 @@ from .orm import (
|
|
|
25
26
|
# DSL de cabecera del schema
|
|
26
27
|
datasource, generate, query,
|
|
27
28
|
# Core ORM components
|
|
28
|
-
Table, View, Enum, column, vector, vector_column, VectorColumn,
|
|
29
|
+
Table, View, Enum, column, vector, vector_column, VectorColumn, timestamptz,
|
|
29
30
|
relation, unique_constraint, index_constraint,
|
|
30
31
|
TriggerAPI, on_create, on_update, on_delete,
|
|
31
32
|
trigger_api, col, onetomany, manytoone, onetoone, get_type_registry,
|
|
@@ -57,6 +58,8 @@ __all__ = [
|
|
|
57
58
|
'date',
|
|
58
59
|
'time',
|
|
59
60
|
'bigint',
|
|
61
|
+
'smallint',
|
|
62
|
+
'timestamptz',
|
|
60
63
|
'text',
|
|
61
64
|
'numeric',
|
|
62
65
|
'largebinary',
|
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
La orquestación de `tai-sql pull`: conectar, leer, decidir, escribir.
|
|
4
|
+
|
|
5
|
+
El comando de arriba solo traduce banderas y presenta; aquí está el flujo, que se puede
|
|
6
|
+
ejecutar sin Click.
|
|
7
|
+
|
|
8
|
+
**Es el único comando que invierte el patrón universal** (`00-filosofia.md`): no resuelve
|
|
9
|
+
un schema para actuar sobre su mapeo, porque es el que fabrica el schema. La excepción está
|
|
10
|
+
acotada a este módulo: en cuanto el fichero existe, `generate` y `push` vuelven al camino
|
|
11
|
+
de siempre.
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import os
|
|
16
|
+
import tempfile
|
|
17
|
+
from contextlib import contextmanager
|
|
18
|
+
from dataclasses import dataclass
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from typing import Iterator, Optional, Protocol, Sequence
|
|
21
|
+
|
|
22
|
+
from sqlalchemy import Engine, event, inspect
|
|
23
|
+
|
|
24
|
+
from tai_sql.connection import Provider
|
|
25
|
+
from tai_sql.errors import ErrorCode, ProjectError, TaiSqlError
|
|
26
|
+
from tai_sql.introspect import SchemaBlueprint, build, read_view_columns, render
|
|
27
|
+
from tai_sql.introspect import list_schemas as _list_schemas
|
|
28
|
+
from tai_sql.sync.state import build_remote_state
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass(frozen=True)
|
|
32
|
+
class PullResult:
|
|
33
|
+
"""
|
|
34
|
+
Lo que produjo un `pull`.
|
|
35
|
+
|
|
36
|
+
Attributes:
|
|
37
|
+
blueprint: Lo que se dedujo, con sus descartes.
|
|
38
|
+
content: El texto del fichero de schema.
|
|
39
|
+
schema_file: Dónde se escribió, o `None` con `--stdout`.
|
|
40
|
+
view_files: Los `.sql` escritos.
|
|
41
|
+
load_errors: Lo que falló al cargar el fichero antes de escribirlo. Solo puede
|
|
42
|
+
no estar vacío con `--stdout`: sin él, un fichero que no carga no se escribe.
|
|
43
|
+
"""
|
|
44
|
+
blueprint: SchemaBlueprint
|
|
45
|
+
content: str
|
|
46
|
+
schema_file: Optional[Path] = None
|
|
47
|
+
view_files: tuple[Path, ...] = ()
|
|
48
|
+
load_errors: tuple[str, ...] = ()
|
|
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
|
+
|
|
97
|
+
def resolve_provider(source: str) -> Provider:
|
|
98
|
+
"""
|
|
99
|
+
El proveedor de conexión, a partir de lo que se pasó en `-p/--provider`.
|
|
100
|
+
|
|
101
|
+
Las dos formas se distinguen por el `://`: sin él es el **nombre de una variable de
|
|
102
|
+
entorno**, que es el caso normal y el único que acaba escrito en el fichero.
|
|
103
|
+
"""
|
|
104
|
+
if '://' in source:
|
|
105
|
+
return Provider.from_connection_string(source)
|
|
106
|
+
return Provider.from_environment(source)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def resolve_db_schema(provider: Provider, requested: Optional[str]) -> str:
|
|
110
|
+
"""
|
|
111
|
+
El schema de la base de datos que se va a leer.
|
|
112
|
+
|
|
113
|
+
Sin `--db-schema`, se usa el del motor (`public`, `dbo`) si existe, y si no el único
|
|
114
|
+
que haya. Con varios y ninguno por defecto, **se corta y se listan**: elegir uno por
|
|
115
|
+
orden alfabético sería adivinar, y leer el equivocado produce un schema completo y
|
|
116
|
+
equivocado, que es peor que un error.
|
|
117
|
+
"""
|
|
118
|
+
driver = provider.driver
|
|
119
|
+
if driver is not None and not driver.supports_schemas():
|
|
120
|
+
# En MySQL el «schema» del datasource es la base de datos: es lo que declara
|
|
121
|
+
# cualquier schema de MySQL (`schema='mi_bd'`) y lo que el DDL usa de prefijo.
|
|
122
|
+
# Antes se devolvía `''`, que `datasource()` convierte en `'public'`: el push
|
|
123
|
+
# apuntaba a una base de datos `public` y `query()` buscaba las vistas en
|
|
124
|
+
# `views/public/`, así que el schema recién escrito ni siquiera cargaba.
|
|
125
|
+
return (
|
|
126
|
+
requested
|
|
127
|
+
or inspect(provider.engine).default_schema_name
|
|
128
|
+
or provider.database
|
|
129
|
+
or ''
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
if requested:
|
|
133
|
+
return requested
|
|
134
|
+
|
|
135
|
+
disponibles = _list_schemas(provider.engine)
|
|
136
|
+
por_defecto = inspect(provider.engine).default_schema_name
|
|
137
|
+
|
|
138
|
+
if por_defecto and por_defecto in disponibles:
|
|
139
|
+
return por_defecto
|
|
140
|
+
|
|
141
|
+
if len(disponibles) == 1:
|
|
142
|
+
return disponibles[0]
|
|
143
|
+
|
|
144
|
+
raise ProjectError(
|
|
145
|
+
message='La base de datos tiene varios schemas y ninguno es el del motor',
|
|
146
|
+
solution=(
|
|
147
|
+
f"Indica cuál leer con '--db-schema <nombre>'. Disponibles: "
|
|
148
|
+
f"{', '.join(disponibles) or '(ninguno)'}"
|
|
149
|
+
),
|
|
150
|
+
code=ErrorCode.SCHEMA_NOT_FOUND,
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def run_pull(
|
|
155
|
+
*,
|
|
156
|
+
source: str = 'MAIN_DATABASE_URL',
|
|
157
|
+
db_schema: Optional[str] = None,
|
|
158
|
+
schema_name: Optional[str] = None,
|
|
159
|
+
env_var: Optional[str] = None,
|
|
160
|
+
syntax: str = 'v2',
|
|
161
|
+
exclude: Sequence[str] = (),
|
|
162
|
+
force: bool = False,
|
|
163
|
+
to_stdout: bool = False,
|
|
164
|
+
progress: Optional[PullProgress] = None,
|
|
165
|
+
) -> PullResult:
|
|
166
|
+
"""
|
|
167
|
+
Construye el fichero de schema que describe una base de datos.
|
|
168
|
+
|
|
169
|
+
Args:
|
|
170
|
+
source: Nombre de variable de entorno o cadena de conexión.
|
|
171
|
+
db_schema: Schema de la base de datos a leer.
|
|
172
|
+
schema_name: Nombre del fichero de schema. Por defecto, el del schema de la BD.
|
|
173
|
+
env_var: Variable que se escribirá en `env(...)`. Por defecto, la de `source`
|
|
174
|
+
cuando se pasó una variable; `MAIN_DATABASE_URL` cuando se pasó una cadena.
|
|
175
|
+
syntax: 'v1' o 'v2'.
|
|
176
|
+
exclude: Tablas que no se declaran.
|
|
177
|
+
force: Sobrescribir un fichero de schema existente.
|
|
178
|
+
to_stdout: No escribir nada; devolver el contenido.
|
|
179
|
+
progress: A quién contarle en qué fase va. Ver `PullProgress`.
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
El resultado, con el blueprint para poder informar de los descartes.
|
|
183
|
+
"""
|
|
184
|
+
from tai_sql import pm
|
|
185
|
+
|
|
186
|
+
if pm.find_project_root() is None:
|
|
187
|
+
raise ProjectError(
|
|
188
|
+
message='No estás dentro de un proyecto tai-sql',
|
|
189
|
+
solution=(
|
|
190
|
+
"pull escribe en 'schemas/' y en 'views/', así que necesita un proyecto. "
|
|
191
|
+
"Ejecútalo desde el directorio del proyecto o crea uno con "
|
|
192
|
+
"'tai-sql init <nombre>'."
|
|
193
|
+
),
|
|
194
|
+
code=ErrorCode.PROJECT_NOT_FOUND,
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
provider = resolve_provider(source)
|
|
198
|
+
driver = provider.driver
|
|
199
|
+
|
|
200
|
+
if driver is None:
|
|
201
|
+
raise TaiSqlError(
|
|
202
|
+
message=f"No hay driver para '{provider.drivername}'",
|
|
203
|
+
solution=(
|
|
204
|
+
'Los motores soportados son postgresql, mysql, mssql y bigquery. '
|
|
205
|
+
'Comprueba el prefijo de la URL de conexión.'
|
|
206
|
+
),
|
|
207
|
+
code=ErrorCode.INVALID_PROVIDER,
|
|
208
|
+
)
|
|
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}')
|
|
229
|
+
destino_db = resolve_db_schema(provider, db_schema)
|
|
230
|
+
nombre = schema_name or destino_db or 'main'
|
|
231
|
+
|
|
232
|
+
fichero = pm.paths.schema_file(nombre)
|
|
233
|
+
if fichero.exists() and not (force or to_stdout):
|
|
234
|
+
raise ProjectError(
|
|
235
|
+
message=f"El schema '{nombre}' ya existe en este proyecto",
|
|
236
|
+
solution=(
|
|
237
|
+
"pull no sobrescribe: un schema puede tener docstrings, feed(), triggers "
|
|
238
|
+
"y columnas calculadas que la base de datos no contiene y que pull no "
|
|
239
|
+
f"sabe reconstruir. Usa '--force' para sobrescribirlo de todas formas, "
|
|
240
|
+
f"'-s <otro nombre>' para escribir uno nuevo, o '--stdout' para verlo sin "
|
|
241
|
+
f"tocar el disco."
|
|
242
|
+
),
|
|
243
|
+
location=str(fichero),
|
|
244
|
+
code=ErrorCode.DUPLICATE_NAME,
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
progress.step(f"Leyendo el catálogo de '{destino_db}'")
|
|
248
|
+
estado, _ = build_remote_state(
|
|
249
|
+
provider.engine, driver, destino_db or None, target=nombre
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
if estado.views:
|
|
253
|
+
progress.step(f'Leyendo {len(estado.views)} vista(s)')
|
|
254
|
+
columnas_vista = read_view_columns(
|
|
255
|
+
provider.engine, driver, destino_db or None, list(estado.views)
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
progress.step(f'Deduciendo el schema ({len(estado.tables)} tablas)')
|
|
259
|
+
blueprint = build(
|
|
260
|
+
estado,
|
|
261
|
+
driver,
|
|
262
|
+
name=nombre,
|
|
263
|
+
db_schema=destino_db,
|
|
264
|
+
env_var=env_var or _env_var_for(source),
|
|
265
|
+
syntax=syntax,
|
|
266
|
+
view_columns=columnas_vista,
|
|
267
|
+
exclude=exclude,
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
contenido = render(blueprint)
|
|
271
|
+
|
|
272
|
+
progress.step('Comprobando que el schema carga')
|
|
273
|
+
errores = self_check(blueprint, provider)
|
|
274
|
+
|
|
275
|
+
if to_stdout:
|
|
276
|
+
return PullResult(blueprint=blueprint, content=contenido, load_errors=errores)
|
|
277
|
+
|
|
278
|
+
if errores:
|
|
279
|
+
detalle = '\n'.join(f' · {e}' for e in errores)
|
|
280
|
+
raise TaiSqlError(
|
|
281
|
+
message=(
|
|
282
|
+
f"El schema que ha deducido pull no carga, así que no se ha escrito:\n"
|
|
283
|
+
f"{detalle}"
|
|
284
|
+
),
|
|
285
|
+
solution=(
|
|
286
|
+
"Es un fallo de tai-sql pull, no de tu base de datos: repórtalo con este "
|
|
287
|
+
"mensaje. Mientras tanto, 'tai-sql pull --stdout' imprime el fichero para "
|
|
288
|
+
"que lo completes a mano."
|
|
289
|
+
),
|
|
290
|
+
code=ErrorCode.SCHEMA_IMPORT_FAILED,
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
progress.step('Escribiendo el schema')
|
|
294
|
+
escritas = _write(pm, blueprint, contenido, fichero)
|
|
295
|
+
|
|
296
|
+
return PullResult(
|
|
297
|
+
blueprint=blueprint,
|
|
298
|
+
content=contenido,
|
|
299
|
+
schema_file=fichero,
|
|
300
|
+
view_files=escritas,
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def self_check(blueprint: SchemaBlueprint, provider: Provider) -> tuple[str, ...]:
|
|
305
|
+
"""
|
|
306
|
+
Carga y analiza el schema deducido, en un proyecto temporal, antes de escribirlo.
|
|
307
|
+
|
|
308
|
+
`pull` tiene que escribir un schema **que cargue**, y hasta ahora nada lo comprobaba.
|
|
309
|
+
La primera base de datos ajena sobre la que se ejecutó dio un fichero que no se podía
|
|
310
|
+
ni importar —las vistas estaban donde `query()` no las buscaba, y tres tablas tenían
|
|
311
|
+
dos claves foráneas hacia la misma tabla, que el análisis rechazaba—, y el desarrollador
|
|
312
|
+
se enteraba al ejecutar el comando siguiente.
|
|
313
|
+
|
|
314
|
+
Es el mismo camino que seguirá `generate`: el import del fichero con sus `query()` y
|
|
315
|
+
el análisis completo. Se hace en un directorio temporal, con los generadores apuntando
|
|
316
|
+
dentro de él —instanciar uno crea su directorio—, y restaurando el schema y el
|
|
317
|
+
registro activos: comprobar no deja rastro.
|
|
318
|
+
|
|
319
|
+
Returns:
|
|
320
|
+
Los errores, uno por línea; vacío si el schema carga.
|
|
321
|
+
"""
|
|
322
|
+
from tai_sql.errors import TaiSqlError as _Error
|
|
323
|
+
from tai_sql.orm.analysis import ErrorCollector
|
|
324
|
+
from tai_sql.orm.registry import use_registry
|
|
325
|
+
from tai_sql.project.context import use_schema
|
|
326
|
+
from tai_sql.schema import SchemaManager
|
|
327
|
+
|
|
328
|
+
with tempfile.TemporaryDirectory(prefix='tai-sql-pull-') as tmp:
|
|
329
|
+
raiz = Path(tmp)
|
|
330
|
+
contenido = render(
|
|
331
|
+
blueprint,
|
|
332
|
+
client_dir=str(raiz / 'cliente'),
|
|
333
|
+
diagrams_dir=str(raiz / 'diagramas'),
|
|
334
|
+
)
|
|
335
|
+
fichero = raiz / 'schemas' / f'{blueprint.name}.py'
|
|
336
|
+
fichero.parent.mkdir(parents=True)
|
|
337
|
+
fichero.write_text(contenido, encoding='utf-8')
|
|
338
|
+
_write_views(raiz / 'views', blueprint)
|
|
339
|
+
|
|
340
|
+
# El `env(...)` del fichero se resuelve al importarlo. Si la variable no está
|
|
341
|
+
# definida —se pasó una cadena de conexión con `-p`—, se le da la del origen
|
|
342
|
+
# solo mientras dura la comprobación.
|
|
343
|
+
faltaba = blueprint.env_var not in os.environ
|
|
344
|
+
if faltaba:
|
|
345
|
+
os.environ[blueprint.env_var] = provider.url.render_as_string(
|
|
346
|
+
hide_password=False
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
try:
|
|
350
|
+
with use_registry(), use_schema(None):
|
|
351
|
+
manager = SchemaManager(blueprint.name, fichero)
|
|
352
|
+
with use_schema(manager):
|
|
353
|
+
manager.load()
|
|
354
|
+
manager.validations()
|
|
355
|
+
collector = ErrorCollector(
|
|
356
|
+
context=f"Schema '{blueprint.name}'", enable_colors=False
|
|
357
|
+
)
|
|
358
|
+
manager.analyze(collector)
|
|
359
|
+
return tuple(e.message for e in collector.get_errors())
|
|
360
|
+
except _Error as exc:
|
|
361
|
+
return (exc.message,)
|
|
362
|
+
except Exception as exc: # el import de un fichero puede lanzar cualquier cosa
|
|
363
|
+
return (f'{type(exc).__name__}: {exc}',)
|
|
364
|
+
finally:
|
|
365
|
+
if faltaba:
|
|
366
|
+
os.environ.pop(blueprint.env_var, None)
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
def _views_dir(views_root: Path, blueprint: SchemaBlueprint) -> Path:
|
|
370
|
+
"""
|
|
371
|
+
El directorio de las vistas: el mismo que resolverá `query()`.
|
|
372
|
+
|
|
373
|
+
`query()` busca en `views/<schema del datasource>/`, y el schema del datasource es el
|
|
374
|
+
que `pull` escribe (`blueprint.db_schema`). Con `''` el datasource cae en `'public'`,
|
|
375
|
+
y por eso también aquí.
|
|
376
|
+
"""
|
|
377
|
+
return views_root / (blueprint.db_schema or 'public')
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def _write_views(views_root: Path, blueprint: SchemaBlueprint) -> tuple[Path, ...]:
|
|
381
|
+
"""Escribe el `.sql` de cada vista donde `query()` lo va a buscar."""
|
|
382
|
+
if not blueprint.views:
|
|
383
|
+
return ()
|
|
384
|
+
|
|
385
|
+
destino = _views_dir(views_root, blueprint)
|
|
386
|
+
destino.mkdir(parents=True, exist_ok=True)
|
|
387
|
+
|
|
388
|
+
escritas = []
|
|
389
|
+
for vista in blueprint.views:
|
|
390
|
+
ruta = destino / vista.sql_file
|
|
391
|
+
ruta.write_text(vista.sql + '\n', encoding='utf-8')
|
|
392
|
+
escritas.append(ruta)
|
|
393
|
+
|
|
394
|
+
return tuple(escritas)
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
def _env_var_for(source: str) -> str:
|
|
398
|
+
"""
|
|
399
|
+
La variable de entorno que se escribirá en el fichero.
|
|
400
|
+
|
|
401
|
+
Nunca la cadena de conexión, aunque sea lo que se pasó: el schema se commitea, y una
|
|
402
|
+
contraseña dentro de él no se retira del historial. Con una cadena literal se usa el
|
|
403
|
+
nombre convencional, que el desarrollador cambia con `--env-var` si el suyo es otro.
|
|
404
|
+
"""
|
|
405
|
+
return 'MAIN_DATABASE_URL' if '://' in source else source
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
def _write(pm, blueprint: SchemaBlueprint, contenido: str, fichero: Path) -> tuple[Path, ...]:
|
|
409
|
+
"""Escribe el schema y los `.sql` de sus vistas."""
|
|
410
|
+
fichero.parent.mkdir(parents=True, exist_ok=True)
|
|
411
|
+
fichero.write_text(contenido, encoding='utf-8')
|
|
412
|
+
return _write_views(pm.paths.views_dir, blueprint)
|
|
413
|
+
|
|
414
|
+
|
|
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,22 +63,31 @@ 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
|
|
77
81
|
# a un `diff` sin que los avisos lo ensucien.
|
|
78
82
|
click.echo(resultado.content, nl=False)
|
|
79
83
|
_report(resultado.blueprint, stream=sys.stderr)
|
|
84
|
+
if resultado.load_errors:
|
|
85
|
+
render_warning(
|
|
86
|
+
"Este schema NO carga tal cual, y sin --stdout no se habría escrito:\n"
|
|
87
|
+
+ '\n'.join(f" · {e}" for e in resultado.load_errors),
|
|
88
|
+
hint="Es un fallo de tai-sql pull: repórtalo con este mensaje.",
|
|
89
|
+
stream=sys.stderr,
|
|
90
|
+
)
|
|
80
91
|
return
|
|
81
92
|
|
|
82
93
|
click.echo(f"✅ Schema escrito en {resultado.schema_file}")
|
|
@@ -92,6 +103,60 @@ def pull(provider, db_schema, schema_name, env_var, syntax, exclude, to_stdout,
|
|
|
92
103
|
)
|
|
93
104
|
|
|
94
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
|
+
|
|
95
160
|
def _resumen(blueprint: SchemaBlueprint) -> None:
|
|
96
161
|
"""Qué se ha reconstruido, en una línea por tipo de objeto."""
|
|
97
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
|