pgdevkit 0.10.0__tar.gz → 0.11.0__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.
- {pgdevkit-0.10.0 → pgdevkit-0.11.0}/PKG-INFO +1 -1
- {pgdevkit-0.10.0 → pgdevkit-0.11.0}/pgdevkit/cli.py +7 -1
- {pgdevkit-0.10.0 → pgdevkit-0.11.0}/pgdevkit/stats.py +121 -1
- {pgdevkit-0.10.0 → pgdevkit-0.11.0}/pyproject.toml +1 -1
- {pgdevkit-0.10.0 → pgdevkit-0.11.0}/skills/pgdevkit/SKILL.md +2 -0
- {pgdevkit-0.10.0 → pgdevkit-0.11.0}/tests/test_stats_cli.py +1 -0
- pgdevkit-0.11.0/tests/test_stats_mssql_live.py +59 -0
- {pgdevkit-0.10.0 → pgdevkit-0.11.0}/uv.lock +1 -1
- {pgdevkit-0.10.0 → pgdevkit-0.11.0}/.github/workflows/auto-release.yml +0 -0
- {pgdevkit-0.10.0 → pgdevkit-0.11.0}/.github/workflows/python-publish.yml +0 -0
- {pgdevkit-0.10.0 → pgdevkit-0.11.0}/.github/workflows/python-test.yml +0 -0
- {pgdevkit-0.10.0 → pgdevkit-0.11.0}/.gitignore +0 -0
- {pgdevkit-0.10.0 → pgdevkit-0.11.0}/.python-version +0 -0
- {pgdevkit-0.10.0 → pgdevkit-0.11.0}/README.md +0 -0
- {pgdevkit-0.10.0 → pgdevkit-0.11.0}/docs/database-layout.md +0 -0
- {pgdevkit-0.10.0 → pgdevkit-0.11.0}/pgdevkit/__init__.py +0 -0
- {pgdevkit-0.10.0 → pgdevkit-0.11.0}/pgdevkit/areas.py +0 -0
- {pgdevkit-0.10.0 → pgdevkit-0.11.0}/pgdevkit/backends/__init__.py +0 -0
- {pgdevkit-0.10.0 → pgdevkit-0.11.0}/pgdevkit/backends/base.py +0 -0
- {pgdevkit-0.10.0 → pgdevkit-0.11.0}/pgdevkit/backends/mssql.py +0 -0
- {pgdevkit-0.10.0 → pgdevkit-0.11.0}/pgdevkit/backends/postgres.py +0 -0
- {pgdevkit-0.10.0 → pgdevkit-0.11.0}/pgdevkit/connection.py +0 -0
- {pgdevkit-0.10.0 → pgdevkit-0.11.0}/pgdevkit/db/__init__.py +0 -0
- {pgdevkit-0.10.0 → pgdevkit-0.11.0}/pgdevkit/db/complex_types.py +0 -0
- {pgdevkit-0.10.0 → pgdevkit-0.11.0}/pgdevkit/db/connection.py +0 -0
- {pgdevkit-0.10.0 → pgdevkit-0.11.0}/pgdevkit/db/crud.py +0 -0
- {pgdevkit-0.10.0 → pgdevkit-0.11.0}/pgdevkit/db/loader.py +0 -0
- {pgdevkit-0.10.0 → pgdevkit-0.11.0}/pgdevkit/db/model.py +0 -0
- {pgdevkit-0.10.0 → pgdevkit-0.11.0}/pgdevkit/db/mssql_crud.py +0 -0
- {pgdevkit-0.10.0 → pgdevkit-0.11.0}/pgdevkit/db/mssql_sql.py +0 -0
- {pgdevkit-0.10.0 → pgdevkit-0.11.0}/pgdevkit/dialect.py +0 -0
- {pgdevkit-0.10.0 → pgdevkit-0.11.0}/pgdevkit/diff.py +0 -0
- {pgdevkit-0.10.0 → pgdevkit-0.11.0}/pgdevkit/envtag.py +0 -0
- {pgdevkit-0.10.0 → pgdevkit-0.11.0}/pgdevkit/fetch_missing.py +0 -0
- {pgdevkit-0.10.0 → pgdevkit-0.11.0}/pgdevkit/introspect.py +0 -0
- {pgdevkit-0.10.0 → pgdevkit-0.11.0}/pgdevkit/lakebase.py +0 -0
- {pgdevkit-0.10.0 → pgdevkit-0.11.0}/pgdevkit/migrate.py +0 -0
- {pgdevkit-0.10.0 → pgdevkit-0.11.0}/pgdevkit/models.py +0 -0
- {pgdevkit-0.10.0 → pgdevkit-0.11.0}/pgdevkit/mssql_introspect.py +0 -0
- {pgdevkit-0.10.0 → pgdevkit-0.11.0}/pgdevkit/parser.py +0 -0
- {pgdevkit-0.10.0 → pgdevkit-0.11.0}/pgdevkit/schemas.py +0 -0
- {pgdevkit-0.10.0 → pgdevkit-0.11.0}/pgdevkit/sql_text.py +0 -0
- {pgdevkit-0.10.0 → pgdevkit-0.11.0}/pgdevkit/testdb/__init__.py +0 -0
- {pgdevkit-0.10.0 → pgdevkit-0.11.0}/pgdevkit/testdb/_docker.py +0 -0
- {pgdevkit-0.10.0 → pgdevkit-0.11.0}/pgdevkit/testdb/api.py +0 -0
- {pgdevkit-0.10.0 → pgdevkit-0.11.0}/pgdevkit/testdb/config.py +0 -0
- {pgdevkit-0.10.0 → pgdevkit-0.11.0}/pgdevkit/testdb/constants.py +0 -0
- {pgdevkit-0.10.0 → pgdevkit-0.11.0}/pgdevkit/testdb/container.py +0 -0
- {pgdevkit-0.10.0 → pgdevkit-0.11.0}/pgdevkit/testdb/mssql/__init__.py +0 -0
- {pgdevkit-0.10.0 → pgdevkit-0.11.0}/pgdevkit/testdb/mssql/api.py +0 -0
- {pgdevkit-0.10.0 → pgdevkit-0.11.0}/pgdevkit/testdb/mssql/constants.py +0 -0
- {pgdevkit-0.10.0 → pgdevkit-0.11.0}/pgdevkit/testdb/mssql/container.py +0 -0
- {pgdevkit-0.10.0 → pgdevkit-0.11.0}/pgdevkit/testdb/naming.py +0 -0
- {pgdevkit-0.10.0 → pgdevkit-0.11.0}/pgdevkit/testdb/query.py +0 -0
- {pgdevkit-0.10.0 → pgdevkit-0.11.0}/pgdevkit/testdb/schema.py +0 -0
- {pgdevkit-0.10.0 → pgdevkit-0.11.0}/skills/pgdevkit/references/dynamic-sql.md +0 -0
- {pgdevkit-0.10.0 → pgdevkit-0.11.0}/skills/pgdevkit/references/temporal-tables.md +0 -0
- {pgdevkit-0.10.0 → pgdevkit-0.11.0}/tests/__init__.py +0 -0
- {pgdevkit-0.10.0 → pgdevkit-0.11.0}/tests/conftest.py +0 -0
- {pgdevkit-0.10.0 → pgdevkit-0.11.0}/tests/db/__init__.py +0 -0
- {pgdevkit-0.10.0 → pgdevkit-0.11.0}/tests/db/test_complex_types.py +0 -0
- {pgdevkit-0.10.0 → pgdevkit-0.11.0}/tests/db/test_connection.py +0 -0
- {pgdevkit-0.10.0 → pgdevkit-0.11.0}/tests/db/test_crud.py +0 -0
- {pgdevkit-0.10.0 → pgdevkit-0.11.0}/tests/db/test_loader.py +0 -0
- {pgdevkit-0.10.0 → pgdevkit-0.11.0}/tests/db/test_mssql_crud_live.py +0 -0
- {pgdevkit-0.10.0 → pgdevkit-0.11.0}/tests/db/test_mssql_crud_sql.py +0 -0
- {pgdevkit-0.10.0 → pgdevkit-0.11.0}/tests/fixtures/01_schema.sql +0 -0
- {pgdevkit-0.10.0 → pgdevkit-0.11.0}/tests/fixtures/02_types.sql +0 -0
- {pgdevkit-0.10.0 → pgdevkit-0.11.0}/tests/fixtures/03_tables.sql +0 -0
- {pgdevkit-0.10.0 → pgdevkit-0.11.0}/tests/fixtures/04_views.sql +0 -0
- {pgdevkit-0.10.0 → pgdevkit-0.11.0}/tests/fixtures/05_functions.sql +0 -0
- {pgdevkit-0.10.0 → pgdevkit-0.11.0}/tests/fixtures/06_indexes.sql +0 -0
- {pgdevkit-0.10.0 → pgdevkit-0.11.0}/tests/test_areas.py +0 -0
- {pgdevkit-0.10.0 → pgdevkit-0.11.0}/tests/test_cli_compare.py +0 -0
- {pgdevkit-0.10.0 → pgdevkit-0.11.0}/tests/test_compare.py +0 -0
- {pgdevkit-0.10.0 → pgdevkit-0.11.0}/tests/test_compare_mssql_live.py +0 -0
- {pgdevkit-0.10.0 → pgdevkit-0.11.0}/tests/test_connection.py +0 -0
- {pgdevkit-0.10.0 → pgdevkit-0.11.0}/tests/test_dialect.py +0 -0
- {pgdevkit-0.10.0 → pgdevkit-0.11.0}/tests/test_diff_mssql.py +0 -0
- {pgdevkit-0.10.0 → pgdevkit-0.11.0}/tests/test_envtag.py +0 -0
- {pgdevkit-0.10.0 → pgdevkit-0.11.0}/tests/test_fetch_missing.py +0 -0
- {pgdevkit-0.10.0 → pgdevkit-0.11.0}/tests/test_fetch_missing_cli.py +0 -0
- {pgdevkit-0.10.0 → pgdevkit-0.11.0}/tests/test_lakebase.py +0 -0
- {pgdevkit-0.10.0 → pgdevkit-0.11.0}/tests/test_migrate.py +0 -0
- {pgdevkit-0.10.0 → pgdevkit-0.11.0}/tests/test_migrate_areas.py +0 -0
- {pgdevkit-0.10.0 → pgdevkit-0.11.0}/tests/test_migrate_env.py +0 -0
- {pgdevkit-0.10.0 → pgdevkit-0.11.0}/tests/test_migrate_grants.py +0 -0
- {pgdevkit-0.10.0 → pgdevkit-0.11.0}/tests/test_migrate_schemas.py +0 -0
- {pgdevkit-0.10.0 → pgdevkit-0.11.0}/tests/test_mssql_introspect.py +0 -0
- {pgdevkit-0.10.0 → pgdevkit-0.11.0}/tests/test_parser_areas.py +0 -0
- {pgdevkit-0.10.0 → pgdevkit-0.11.0}/tests/test_parser_mssql.py +0 -0
- {pgdevkit-0.10.0 → pgdevkit-0.11.0}/tests/test_parser_schemas.py +0 -0
- {pgdevkit-0.10.0 → pgdevkit-0.11.0}/tests/test_schemas.py +0 -0
- {pgdevkit-0.10.0 → pgdevkit-0.11.0}/tests/testdb/__init__.py +0 -0
- {pgdevkit-0.10.0 → pgdevkit-0.11.0}/tests/testdb/conftest.py +0 -0
- {pgdevkit-0.10.0 → pgdevkit-0.11.0}/tests/testdb/fixtures/database/app/migrations/001_add_gadget_note.sql +0 -0
- {pgdevkit-0.10.0 → pgdevkit-0.11.0}/tests/testdb/fixtures/database/app/tables/event.sql +0 -0
- {pgdevkit-0.10.0 → pgdevkit-0.11.0}/tests/testdb/fixtures/database/app/tables/event_kind.sql +0 -0
- {pgdevkit-0.10.0 → pgdevkit-0.11.0}/tests/testdb/fixtures/database/app/tables/gadget.sql +0 -0
- {pgdevkit-0.10.0 → pgdevkit-0.11.0}/tests/testdb/fixtures/database/app/tables/gadget.test_data.json +0 -0
- {pgdevkit-0.10.0 → pgdevkit-0.11.0}/tests/testdb/fixtures/database/app/tables/prod_only.prod.sql +0 -0
- {pgdevkit-0.10.0 → pgdevkit-0.11.0}/tests/testdb/fixtures/database/app/tables/widget.init.sql +0 -0
- {pgdevkit-0.10.0 → pgdevkit-0.11.0}/tests/testdb/fixtures/database/app/tables/widget.sql +0 -0
- {pgdevkit-0.10.0 → pgdevkit-0.11.0}/tests/testdb/fixtures/database/app/tables/widget.test_data.json +0 -0
- {pgdevkit-0.10.0 → pgdevkit-0.11.0}/tests/testdb/fixtures/database/app/tables/widget_part.sql +0 -0
- {pgdevkit-0.10.0 → pgdevkit-0.11.0}/tests/testdb/fixtures/database/app/tables/widget_part_detail.sql +0 -0
- {pgdevkit-0.10.0 → pgdevkit-0.11.0}/tests/testdb/fixtures/database/app/types/dimensions.sql +0 -0
- {pgdevkit-0.10.0 → pgdevkit-0.11.0}/tests/testdb/fixtures/database/app/types/mood.sql +0 -0
- {pgdevkit-0.10.0 → pgdevkit-0.11.0}/tests/testdb/fixtures/database/app/views/a_wrapper_view.sql +0 -0
- {pgdevkit-0.10.0 → pgdevkit-0.11.0}/tests/testdb/fixtures/database/app/views/b_base_view.sql +0 -0
- {pgdevkit-0.10.0 → pgdevkit-0.11.0}/tests/testdb/fixtures/database/permissions/grants.sql +0 -0
- {pgdevkit-0.10.0 → pgdevkit-0.11.0}/tests/testdb/fixtures/database/schema/app.sql +0 -0
- {pgdevkit-0.10.0 → pgdevkit-0.11.0}/tests/testdb/fixtures/database_mssql/app/tables/widget.sql +0 -0
- {pgdevkit-0.10.0 → pgdevkit-0.11.0}/tests/testdb/fixtures/database_mssql/app/tables/widget.test_data.json +0 -0
- {pgdevkit-0.10.0 → pgdevkit-0.11.0}/tests/testdb/fixtures/database_mssql/app/views/a_wrapper_view.sql +0 -0
- {pgdevkit-0.10.0 → pgdevkit-0.11.0}/tests/testdb/fixtures/database_mssql/app/views/b_base_view.sql +0 -0
- {pgdevkit-0.10.0 → pgdevkit-0.11.0}/tests/testdb/fixtures/database_mssql/schema/app.sql +0 -0
- {pgdevkit-0.10.0 → pgdevkit-0.11.0}/tests/testdb/test_api.py +0 -0
- {pgdevkit-0.10.0 → pgdevkit-0.11.0}/tests/testdb/test_api_mssql.py +0 -0
- {pgdevkit-0.10.0 → pgdevkit-0.11.0}/tests/testdb/test_api_mssql_live.py +0 -0
- {pgdevkit-0.10.0 → pgdevkit-0.11.0}/tests/testdb/test_cli.py +0 -0
- {pgdevkit-0.10.0 → pgdevkit-0.11.0}/tests/testdb/test_config.py +0 -0
- {pgdevkit-0.10.0 → pgdevkit-0.11.0}/tests/testdb/test_config_mssql.py +0 -0
- {pgdevkit-0.10.0 → pgdevkit-0.11.0}/tests/testdb/test_constants.py +0 -0
- {pgdevkit-0.10.0 → pgdevkit-0.11.0}/tests/testdb/test_container.py +0 -0
- {pgdevkit-0.10.0 → pgdevkit-0.11.0}/tests/testdb/test_container_mssql.py +0 -0
- {pgdevkit-0.10.0 → pgdevkit-0.11.0}/tests/testdb/test_docker.py +0 -0
- {pgdevkit-0.10.0 → pgdevkit-0.11.0}/tests/testdb/test_mssql_constants.py +0 -0
- {pgdevkit-0.10.0 → pgdevkit-0.11.0}/tests/testdb/test_naming.py +0 -0
- {pgdevkit-0.10.0 → pgdevkit-0.11.0}/tests/testdb/test_query.py +0 -0
- {pgdevkit-0.10.0 → pgdevkit-0.11.0}/tests/testdb/test_schema.py +0 -0
- {pgdevkit-0.10.0 → pgdevkit-0.11.0}/tests/testdb/test_schema_filtering.py +0 -0
- {pgdevkit-0.10.0 → pgdevkit-0.11.0}/tests/testdb/test_schema_mssql.py +0 -0
|
@@ -227,16 +227,22 @@ def update_stats(
|
|
|
227
227
|
table: list[str] = typer.Option([], "--table", help="Only update schema.name (repeatable); default is every table"),
|
|
228
228
|
exact: bool = typer.Option(False, "--exact", help="Use count(*) for row counts instead of the planner estimate"),
|
|
229
229
|
analyze: bool = typer.Option(False, "--analyze", help="Run ANALYZE first so column stats are fresh"),
|
|
230
|
+
dialect: str = typer.Option("postgres", "--dialect", help="postgres (default) or mssql"),
|
|
230
231
|
) -> None:
|
|
231
232
|
"""Store table stats in _stats/_tables.json (keyed by schema.name, sorted)
|
|
232
233
|
and column stats in _stats/<schema.name>.json."""
|
|
233
234
|
if not scripts_dir.is_dir():
|
|
234
235
|
err_console.print(f"[red]Error:[/red] {scripts_dir} is not a directory")
|
|
235
236
|
raise typer.Exit(2)
|
|
237
|
+
try:
|
|
238
|
+
dialect = get_backend(dialect).dialect.name
|
|
239
|
+
except ValueError as e:
|
|
240
|
+
err_console.print(f"[red]Error:[/red] {e}")
|
|
241
|
+
raise typer.Exit(2)
|
|
236
242
|
conninfo = build_conninfo(url, entra_user)
|
|
237
243
|
try:
|
|
238
244
|
with console.status("Collecting stats..."):
|
|
239
|
-
tables, columns = stats.collect_stats(conninfo, set(table) or None, exact=exact, analyze=analyze)
|
|
245
|
+
tables, columns = stats.collect_stats(conninfo, set(table) or None, exact=exact, analyze=analyze, dialect=dialect)
|
|
240
246
|
except KeyError as e:
|
|
241
247
|
err_console.print(f"[red]Error:[/red] unknown table(s): {e.args[0]}")
|
|
242
248
|
raise typer.Exit(2)
|
|
@@ -8,6 +8,8 @@ import psycopg
|
|
|
8
8
|
import psycopg.sql
|
|
9
9
|
from psycopg.rows import dict_row
|
|
10
10
|
|
|
11
|
+
from .dialect import resolve_dialect
|
|
12
|
+
|
|
11
13
|
STATS_DIRNAME = "_stats"
|
|
12
14
|
TABLES_FILE = "_tables.json"
|
|
13
15
|
|
|
@@ -55,14 +57,132 @@ def _write_json(path: Path, data: Any) -> None:
|
|
|
55
57
|
path.write_text(json.dumps(data, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
|
56
58
|
|
|
57
59
|
|
|
60
|
+
_MSSQL_TABLES_SQL = """
|
|
61
|
+
SELECT s.name AS [schema], t.name AS name,
|
|
62
|
+
SUM(CASE WHEN ps.index_id IN (0, 1) THEN ps.row_count ELSE 0 END) AS estimated_rows,
|
|
63
|
+
SUM(CASE WHEN ps.index_id IN (0, 1) THEN ps.used_page_count ELSE 0 END) * 8192 AS table_bytes,
|
|
64
|
+
SUM(CASE WHEN ps.index_id > 1 THEN ps.used_page_count ELSE 0 END) * 8192 AS index_bytes,
|
|
65
|
+
SUM(ps.used_page_count) * 8192 AS total_bytes
|
|
66
|
+
FROM sys.tables t
|
|
67
|
+
JOIN sys.schemas s ON s.schema_id = t.schema_id
|
|
68
|
+
LEFT JOIN sys.dm_db_partition_stats ps ON ps.object_id = t.object_id
|
|
69
|
+
GROUP BY s.name, t.name
|
|
70
|
+
ORDER BY 1, 2
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
_MSSQL_COLUMNS_SQL = """
|
|
74
|
+
SELECT c.name AS name, ty.name AS base_type, c.max_length AS max_length,
|
|
75
|
+
c.precision AS precision, c.scale AS scale
|
|
76
|
+
FROM sys.columns c
|
|
77
|
+
JOIN sys.types ty ON ty.user_type_id = c.user_type_id
|
|
78
|
+
WHERE c.object_id = OBJECT_ID(?)
|
|
79
|
+
ORDER BY c.column_id
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
# Types that can't be COUNT(DISTINCT)ed / measured with DATALENGTH.
|
|
83
|
+
_MSSQL_UNMEASURABLE = {"text", "ntext", "image", "xml", "geography", "geometry", "hierarchyid", "sql_variant", "json", "vector"}
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _mssql_q(conn: Any, sql: str, params: tuple = ()) -> list[dict[str, Any]]:
|
|
87
|
+
cur = conn.cursor()
|
|
88
|
+
try:
|
|
89
|
+
cur.execute(sql, params)
|
|
90
|
+
names = [c[0] for c in cur.description]
|
|
91
|
+
return [dict(zip(names, row)) for row in cur.fetchall()]
|
|
92
|
+
finally:
|
|
93
|
+
cur.close()
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _mssql_ident(name: str) -> str:
|
|
97
|
+
return "[" + name.replace("]", "]]") + "]"
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _collect_mssql_stats(
|
|
101
|
+
conninfo: str, only: set[str] | None, exact: bool, analyze: bool
|
|
102
|
+
) -> tuple[dict[str, dict[str, Any]], dict[str, dict[str, Any]]]:
|
|
103
|
+
"""MSSQL flavour of collect_stats. Row counts/sizes come from
|
|
104
|
+
sys.dm_db_partition_stats. SQL Server has no pg_stats equivalent, so column
|
|
105
|
+
null_fraction/n_distinct/avg_width are only filled in with exact=True
|
|
106
|
+
(computed by scanning each table); otherwise they are null.
|
|
107
|
+
analyze=True runs UPDATE STATISTICS."""
|
|
108
|
+
# Lazy: the mssql extra isn't required for postgres-only use.
|
|
109
|
+
import mssql_python
|
|
110
|
+
|
|
111
|
+
from .mssql_introspect import _format_type, _is_system_schema
|
|
112
|
+
|
|
113
|
+
tables: dict[str, dict[str, Any]] = {}
|
|
114
|
+
columns: dict[str, dict[str, Any]] = {}
|
|
115
|
+
conn = mssql_python.connect(conninfo, autocommit=True)
|
|
116
|
+
try:
|
|
117
|
+
rows = [r for r in _mssql_q(conn, _MSSQL_TABLES_SQL) if not _is_system_schema(r["schema"])]
|
|
118
|
+
if only is not None:
|
|
119
|
+
unknown = only - {f"{r['schema']}.{r['name']}" for r in rows}
|
|
120
|
+
if unknown:
|
|
121
|
+
raise KeyError(", ".join(sorted(unknown)))
|
|
122
|
+
for r in rows:
|
|
123
|
+
qn = f"{r['schema']}.{r['name']}"
|
|
124
|
+
if only is not None and qn not in only:
|
|
125
|
+
continue
|
|
126
|
+
ident = f"{_mssql_ident(r['schema'])}.{_mssql_ident(r['name'])}"
|
|
127
|
+
if analyze:
|
|
128
|
+
conn.execute(f"UPDATE STATISTICS {ident}")
|
|
129
|
+
col_defs = _mssql_q(conn, _MSSQL_COLUMNS_SQL, (ident,))
|
|
130
|
+
measured: dict[str, Any] = {}
|
|
131
|
+
if exact:
|
|
132
|
+
# One scan per table, so row and column counts share a snapshot.
|
|
133
|
+
parts = ["COUNT_BIG(*) AS n"]
|
|
134
|
+
for i, c in enumerate(col_defs):
|
|
135
|
+
if c["base_type"].lower() not in _MSSQL_UNMEASURABLE:
|
|
136
|
+
col = _mssql_ident(c["name"])
|
|
137
|
+
parts.append(
|
|
138
|
+
f"COUNT_BIG({col}) AS nn{i}, COUNT_BIG(DISTINCT {col}) AS nd{i}, "
|
|
139
|
+
f"AVG(CAST(DATALENGTH({col}) AS float)) AS w{i}"
|
|
140
|
+
)
|
|
141
|
+
measured = _mssql_q(conn, f"SELECT {', '.join(parts)} FROM {ident}")[0]
|
|
142
|
+
row_count = measured["n"]
|
|
143
|
+
else:
|
|
144
|
+
row_count = r["estimated_rows"]
|
|
145
|
+
tables[qn] = {
|
|
146
|
+
"row_count": row_count,
|
|
147
|
+
"row_count_exact": exact,
|
|
148
|
+
"table_bytes": r["table_bytes"],
|
|
149
|
+
"index_bytes": r["index_bytes"],
|
|
150
|
+
"total_bytes": r["total_bytes"],
|
|
151
|
+
}
|
|
152
|
+
col_stats: dict[str, Any] = {}
|
|
153
|
+
for i, c in enumerate(col_defs):
|
|
154
|
+
entry: dict[str, Any] = {
|
|
155
|
+
"data_type": _format_type(c["base_type"], c["max_length"], c["precision"], c["scale"]),
|
|
156
|
+
"null_fraction": None,
|
|
157
|
+
"n_distinct": None,
|
|
158
|
+
"avg_width": None,
|
|
159
|
+
}
|
|
160
|
+
if row_count and f"nn{i}" in measured:
|
|
161
|
+
entry["null_fraction"] = 1 - measured[f"nn{i}"] / row_count
|
|
162
|
+
entry["n_distinct"] = measured[f"nd{i}"]
|
|
163
|
+
w = measured[f"w{i}"]
|
|
164
|
+
entry["avg_width"] = None if w is None else round(w)
|
|
165
|
+
col_stats[c["name"]] = entry
|
|
166
|
+
columns[qn] = col_stats
|
|
167
|
+
finally:
|
|
168
|
+
conn.close()
|
|
169
|
+
return tables, columns
|
|
170
|
+
|
|
171
|
+
|
|
58
172
|
def collect_stats(
|
|
59
|
-
conninfo: str,
|
|
173
|
+
conninfo: str,
|
|
174
|
+
only: set[str] | None = None,
|
|
175
|
+
exact: bool = False,
|
|
176
|
+
analyze: bool = False,
|
|
177
|
+
dialect: str = "postgres",
|
|
60
178
|
) -> tuple[dict[str, dict[str, Any]], dict[str, dict[str, Any]]]:
|
|
61
179
|
"""Returns (table_stats, column_stats), both keyed by "schema.table".
|
|
62
180
|
|
|
63
181
|
Row counts come from pg_class.reltuples (null if the table was never
|
|
64
182
|
analyzed) unless exact=True, which runs count(*). analyze=True runs ANALYZE
|
|
65
183
|
first so column stats (null_frac/n_distinct/avg_width) are populated."""
|
|
184
|
+
if resolve_dialect(dialect).name == "mssql":
|
|
185
|
+
return _collect_mssql_stats(conninfo, only, exact, analyze)
|
|
66
186
|
tables: dict[str, dict[str, Any]] = {}
|
|
67
187
|
columns: dict[str, dict[str, Any]] = {}
|
|
68
188
|
with psycopg.connect(conninfo, autocommit=True) as conn:
|
|
@@ -293,6 +293,8 @@ pgdb get-stats database/ public.users public.orders # JSON t
|
|
|
293
293
|
pgdb get-stats database/ --no-columns # all tables, table-level stats only
|
|
294
294
|
```
|
|
295
295
|
|
|
296
|
+
MSSQL: add `--dialect mssql` (needs the `mssql` extra). Row counts and sizes come from `sys.dm_db_partition_stats`; SQL Server has no `pg_stats`, so column `null_fraction`/`n_distinct`/`avg_width` are `null` unless `--exact` is given, which scans each table to compute them. `--analyze` runs `UPDATE STATISTICS`.
|
|
297
|
+
|
|
296
298
|
To read the stats from code or a script, just `json.load` `database/_stats/_tables.json` (and `database/_stats/<schema.table>.json` for columns).
|
|
297
299
|
|
|
298
300
|
---
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import mssql_python
|
|
7
|
+
import pytest
|
|
8
|
+
from typer.testing import CliRunner
|
|
9
|
+
|
|
10
|
+
from pgdevkit.cli import app
|
|
11
|
+
from pgdevkit.testdb.api import clean_testdb, ensure_testdb, status
|
|
12
|
+
from tests.testdb.conftest import _make_project, requires_mssql
|
|
13
|
+
|
|
14
|
+
# Selected only by the dedicated `mssql-test` CI job; see test_compare_mssql_live.py.
|
|
15
|
+
pytestmark = pytest.mark.mssql
|
|
16
|
+
|
|
17
|
+
runner = CliRunner()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _exec(dsn: str, sql: str) -> None:
|
|
21
|
+
conn = mssql_python.connect(dsn, autocommit=True)
|
|
22
|
+
try:
|
|
23
|
+
conn.cursor().execute(sql)
|
|
24
|
+
finally:
|
|
25
|
+
conn.close()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@requires_mssql
|
|
29
|
+
def test_update_stats_mssql_live(tmp_path: Path):
|
|
30
|
+
project = _make_project(tmp_path, "mssqlstats", "main", engine="mssql")
|
|
31
|
+
out = tmp_path / "out"
|
|
32
|
+
out.mkdir()
|
|
33
|
+
try:
|
|
34
|
+
ensure_testdb(project)
|
|
35
|
+
dsn = status(project)["dsn"]
|
|
36
|
+
_exec(dsn, "INSERT INTO app.widget (id, name, tags) VALUES (2, N'gear', NULL), (3, N'cog', NULL)")
|
|
37
|
+
|
|
38
|
+
r = runner.invoke(app, ["update-stats", str(out), "--url", dsn, "--dialect", "mssql", "--exact", "--analyze"])
|
|
39
|
+
assert r.exit_code == 0, r.output
|
|
40
|
+
tables = json.loads((out / "_stats" / "_tables.json").read_text())
|
|
41
|
+
assert "app.widget" in tables and not any(k.startswith("sys.") for k in tables)
|
|
42
|
+
assert tables["app.widget"]["row_count"] == 3
|
|
43
|
+
assert tables["app.widget"]["row_count_exact"] is True
|
|
44
|
+
assert tables["app.widget"]["total_bytes"] > 0
|
|
45
|
+
cols = json.loads((out / "_stats" / "app.widget.json").read_text())
|
|
46
|
+
assert cols["name"]["data_type"] == "nvarchar(100)"
|
|
47
|
+
assert cols["id"]["n_distinct"] == 3 and cols["id"]["null_fraction"] == 0
|
|
48
|
+
assert cols["tags"]["null_fraction"] == 1
|
|
49
|
+
|
|
50
|
+
# Without --exact: engine-maintained row count, no column measurements.
|
|
51
|
+
r = runner.invoke(app, ["update-stats", str(out), "--url", dsn, "--dialect", "mssql", "--table", "app.widget"])
|
|
52
|
+
assert r.exit_code == 0, r.output
|
|
53
|
+
assert json.loads((out / "_stats" / "_tables.json").read_text())["app.widget"]["row_count"] == 3
|
|
54
|
+
assert json.loads((out / "_stats" / "app.widget.json").read_text())["id"]["n_distinct"] is None
|
|
55
|
+
|
|
56
|
+
bad = ["update-stats", str(out), "--url", dsn, "--dialect", "mssql", "--table", "app.nope"]
|
|
57
|
+
assert runner.invoke(app, bad).exit_code == 2
|
|
58
|
+
finally:
|
|
59
|
+
clean_testdb(project)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pgdevkit-0.10.0 → pgdevkit-0.11.0}/tests/testdb/fixtures/database/app/tables/event_kind.sql
RENAMED
|
File without changes
|
|
File without changes
|
{pgdevkit-0.10.0 → pgdevkit-0.11.0}/tests/testdb/fixtures/database/app/tables/gadget.test_data.json
RENAMED
|
File without changes
|
{pgdevkit-0.10.0 → pgdevkit-0.11.0}/tests/testdb/fixtures/database/app/tables/prod_only.prod.sql
RENAMED
|
File without changes
|
{pgdevkit-0.10.0 → pgdevkit-0.11.0}/tests/testdb/fixtures/database/app/tables/widget.init.sql
RENAMED
|
File without changes
|
|
File without changes
|
{pgdevkit-0.10.0 → pgdevkit-0.11.0}/tests/testdb/fixtures/database/app/tables/widget.test_data.json
RENAMED
|
File without changes
|
{pgdevkit-0.10.0 → pgdevkit-0.11.0}/tests/testdb/fixtures/database/app/tables/widget_part.sql
RENAMED
|
File without changes
|
{pgdevkit-0.10.0 → pgdevkit-0.11.0}/tests/testdb/fixtures/database/app/tables/widget_part_detail.sql
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pgdevkit-0.10.0 → pgdevkit-0.11.0}/tests/testdb/fixtures/database/app/views/a_wrapper_view.sql
RENAMED
|
File without changes
|
{pgdevkit-0.10.0 → pgdevkit-0.11.0}/tests/testdb/fixtures/database/app/views/b_base_view.sql
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pgdevkit-0.10.0 → pgdevkit-0.11.0}/tests/testdb/fixtures/database_mssql/app/tables/widget.sql
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pgdevkit-0.10.0 → pgdevkit-0.11.0}/tests/testdb/fixtures/database_mssql/app/views/b_base_view.sql
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|