dataowl
dataowl reports facts about a table in Databricks (Unity Catalog / Delta Lake): size, files, rows, schema, table properties and an excerpt of the Delta history, and facts about the columns you choose: grain and uniqueness of keys, time span and cadence of time columns, and how two time columns compare. It is meant for data engineers who build a dbt staging model and want the facts without writing exploratory SQL. It reports what exists and never gives recommendations.
Requirements
- Unity Catalog.
- Databricks Runtime 14.3 LTS or above, or serverless compute. Verified on serverless (Spark Connect).
- PySpark is provided by Databricks. dataowl has no other runtime dependencies.
Installation
In a Databricks notebook:
%pip install dataowl==0.2.0
Usage
dataowl works in two steps: inspect gives an overview of the table, and analyze gives
facts about columns you choose.
inspect
import dataowl
dataowl.inspect("catalog.schema.table").show()
As a JSON-serializable dict, where every fact has a value, a source and a reason when it is unavailable:
overview = dataowl.inspect("catalog.schema.table")
overview.to_dict()
Row counts are skipped for views, materialized views and foreign tables unless you ask for them:
dataowl.inspect("catalog.schema.some_view", count_views=True).show()
Names with characters other than letters, digits and underscore are quoted with backticks:
dataowl.inspect("`my-catalog`.schema.table").
The overview includes an excerpt of the Delta history from DESCRIBE HISTORY: the oldest and
newest commit, the number of commits and the number of commits per operation. The header of
the HISTORY block always shows the observation window (oldest – newest commit), because an
operation that is missing is only missing for the period the history covers:
- The history is limited by
delta.logRetentionDuration(see Log retention). Older commits are not included. WRITE (overwrite)is aWRITEcommit with modeOverwrite. It also includes partial overwrites, for example withreplaceWhere.CREATE OR REPLACE TABLE AS SELECT, which dbt uses for thetablematerialization, is shown under its own operation name, not asWRITE (overwrite).
analyze
analysis = dataowl.analyze(
"catalog.schema.table",
key=["customer_id", ("customer_id", "order_id")],
timestamp="updated_at",
compare=("created_at", "updated_at"),
)
analysis.show()
Full signature:
analyze(
table: str,
*,
key: str | tuple[str, ...] | list[str | tuple[str, ...]] | None = None,
timestamp: str | list[str] | None = None,
compare: tuple[str, str] | None = None,
days: int = 30,
spark: SparkSession | None = None,
) -> ColumnAnalysis
At least one of key, timestamp and compare must be given. Only the columns you give are
analyzed; dataowl never chooses or suggests columns. Each key, each timestamp column and the
comparison run their own queries against the table's data, so every column or combination
you add costs compute. Column names are matched case insensitively.
- key analyzes grain and uniqueness. A string is one column, a tuple is one composite
key, and a list holds several keys of either kind, each analyzed separately:
key=["customer_id", ("customer_id", "order_id")]analyzescustomer_idon its own and the combination ofcustomer_idandorder_id. MAP columns cannot be used. - Primary key. When
keyis given, dataowl also reads the declared primary key frominformation_schema. A primary key in Databricks is informational and not enforced. InColumnAnalysis.primary_key,Nonemeans it was not collected (nokey), a fact with valueNonemeans no primary key is declared, and an unavailable fact has a reason. - timestamp analyzes one column or a list of columns of type
TIMESTAMP,TIMESTAMP_NTZorDATE, each separately: min, max, nulls, values after the current time, distinct dates, the gaps in whole days between distinct dates, and rows per day. Other types, including dates stored asSTRING, raiseValueError. - days (default 30) sets the window for rows per day: the last
dayswhole days, not including today, in the session time zone. Days without rows count as 0. - compare counts the rows where the second column is after, equal to or before the
first, and the rows where at least one of them is null. Columns of different types are
compared with Spark's implicit type conversion: a
DATEis compared as that date at 00:00:00, so aDATEon the same day as aTIMESTAMPlater than midnight counts as before it.
analysis.show(show_days=True) # also prints one line per day in the window
analysis.to_dict() # JSON-serializable, like Overview.to_dict()
analyze raises:
ValueErrorfor an invalid table name, whenkey,timestampandcompareare allNone, for invaliddays, and for an unknown or unsupported column. All input is validated before any query against the table's data.RuntimeErrorwhen no SparkSession is available, and when the schema cannot be read frominformation_schema.columns, since the columns cannot then be validated.TableNotFoundErrorwhen the table does not exist or is not accessible.
Every other failure makes the affected facts n/a (<reason>).
Example output
Output for Databricks' public sample data (samples.nyctaxi.trips).
dataowl.inspect("samples.nyctaxi.trips").show()
samples.nyctaxi.trips (MANAGED, DELTA)
Size: 354.2 KB
Files: 1 (avg 354.2 KB)
Rows: 21 932
Columns: 6 (6 incl. nested)
Partitioned by: –
Clustered by: –
Change Data Feed: true
Log retention: not set (default)
Deleted file retention: not set (default)
Created: 2025-09-30 11:28
Last modified: 2026-09-14 15:07
Owner: System user
Comment: –
HISTORY (2025-09-09 15:05 – 2026-09-14 15:07, 223 commits)
CREATE OR REPLACE TABLE AS SELECT: 212
SET TBLPROPERTIES: 11
SCHEMA
# name type nullable comment
1 tpep_pickup_datetime timestamp yes
2 tpep_dropoff_datetime timestamp yes
3 trip_distance double yes
4 fare_amount double yes
5 pickup_zip int yes
6 dropoff_zip int yes
dataowl.analyze(
"samples.nyctaxi.trips",
key=["pickup_zip", ("pickup_zip", "dropoff_zip")],
timestamp="tpep_pickup_datetime",
compare=("tpep_pickup_datetime", "tpep_dropoff_datetime"),
).show()
samples.nyctaxi.trips
PRIMARY KEY none declared
KEY pickup_zip
Rows: 21 932
Rows with null in key: 0
Distinct keys: 128
Keys occurring >1 time: 100
Rows in those keys: 21 904
Rows per key: median 19 · max 1 227
1 row: 28
2–10 rows: 27
11–100 rows: 31
>100 rows: 42
KEY pickup_zip + dropoff_zip
Rows: 21 932
Rows with null in key: 0
Distinct keys: 3 369
Keys occurring >1 time: 2 060
Rows in those keys: 20 623
Rows per key: median 2 · max 143
1 row: 1 309
2–10 rows: 1 533
11–100 rows: 518
>100 rows: 9
TIMESTAMP tpep_pickup_datetime
Min: 2016-01-01 00:04
Max: 2016-02-29 23:51
Nulls: 0 (0.00 %)
Future values: 0
Distinct dates: 60
Days between distinct dates:
min 1 · median 1 · mean 1 · max 1
Rows per day, last 30 complete days (2026-09-08 – 2026-10-07):
median 0 · mean 0 · min 0 · max 0
Note: gaps are measured in whole days; cadence below one day is not visible.
Note: counts reflect current values of the column, not historical changes.
COMPARE tpep_pickup_datetime → tpep_dropoff_datetime
tpep_dropoff_datetime > tpep_pickup_datetime: 21 931
tpep_dropoff_datetime = tpep_pickup_datetime: 1
tpep_dropoff_datetime < tpep_pickup_datetime: 0
Either is null: 0
The data in samples.nyctaxi.trips is from January and February 2016, so every day in the
rows-per-day window has 0 rows.
Principles
- Read-only. Only
SELECT,DESCRIBEandSHOW. Nothing is written, optimized or analyzed. - Facts only. No recommendations or assessments.
- No row values leave Spark. Only aggregates and metadata are collected. Min and max of time columns are the only exception.
- You choose the columns.
analyzeonly analyzes the columns you give. - Unavailable facts are reported with a reason. Missing permissions or an unsupported
object type make the affected facts
n/a (<reason>); the rest of the report is still produced. Only a table that does not exist or is not accessible raisesTableNotFoundError.
Facts per object type
| Facts | Source | MANAGED, EXTERNAL | STREAMING_TABLE, MATERIALIZED_VIEW | FOREIGN | VIEW | UNKNOWN or n/a type |
|---|---|---|---|---|---|---|
| Object type, format, owner, comment, created | information_schema.tables |
yes | yes | yes | yes | yes (n/a if the query fails) |
| Size, files, avg file size, last modified, partitioning, clustering | DESCRIBE DETAIL |
yes | attempted¹ | attempted¹ | no | attempted¹ |
| Columns and schema | information_schema.columns |
yes | yes | yes | yes | yes |
| Field count incl. nested | Spark schema | yes | yes | yes | yes | yes |
| Change Data Feed, log and deleted file retention | SHOW TBLPROPERTIES |
yes | attempted¹ | attempted¹ | no | attempted¹ |
| History excerpt | DESCRIBE HISTORY |
yes | attempted¹ | attempted¹ | no | attempted¹ |
| Rows | COUNT(*) |
yes | MATERIALIZED_VIEW: with count_views=True; STREAMING_TABLE: yes |
with count_views=True |
with count_views=True |
UNKNOWN: yes; n/a type: with count_views=True |
¹ The query runs. If Databricks does not support it for the object, the facts are shown as
n/a (<reason>).
"UNKNOWN" is a table_type dataowl does not recognize, such as MANAGED_SHALLOW_CLONE. The
original value is shown in the header. "n/a type" means the object type could not be read.
Fields
inspect
- Last modified is the last data change, from
DESCRIBE DETAIL. Created is frominformation_schema.tables. - Rows is an exact
COUNT(*). On large tables this costs compute. - Files (avg ...) is the size divided by the number of files.
- Columns (... incl. nested) counts every field, including fields inside structs, array elements and map keys and values.
- not set (default) means the table property is not set, so Databricks uses its default. dataowl does not report what the default is.
- # in the schema table is a running number from 1 in column order.
- HISTORY shows the oldest and newest commit in the available history, the number of commits, and the number of commits per operation.
analyze
- PRIMARY KEY is the declared primary key, in declared order. It is informational and
not enforced by Databricks. The line is left out when
keyis not given. - KEY ... Rows is every row in the table, including rows with null in a key column.
- Rows with null in key counts rows where at least one key column is null. These rows are left out of every other line in the KEY block.
- Distinct keys is the number of distinct key values without null.
- Keys occurring >1 time is the number of key values that occur in more than one row. Rows in those keys is the number of rows that have one of those key values.
- Rows per key is the median and the maximum number of rows per key value, followed by the number of key values with 1, 2–10, 11–100 and more than 100 rows.
- TIMESTAMP ... Nulls is the number of rows with null, and their share of all rows.
- Future values counts values after the current time of the session. A
DATEis compared as that date at 00:00:00. - Days between distinct dates is measured in whole days between consecutive distinct dates. Cadence below one day is not visible.
- Rows per day covers the last
dayswhole days without today, in the session time zone, and counts current values of the column, not historical changes. - COMPARE first → second counts rows where the second column is after (
>), equal to (=) or before (<) the first. Rows with null in either column are counted only in Either is null, so the four numbers add up to all rows. - Medians and means are rounded to one decimal and shown without decimals when the rounded value is whole.
Known deviations
information_schema.columns.ordinal_positionis documented as numbered from 1, but has been observed 0-based in Databricks (serverless, October 2026). dataowl keeps the raw value inColumnInfo.positionand shows a running number in the#column.
License
MIT. Copyright (c) 2026 Alexandro Dronnen. See LICENSE.
Metadata
Release files for dataowl 0.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| dataowl-0.2.0.tar.gz | 74.0 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| dataowl-0.2.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 114.3 kB
Release files / dataowl-0.2.0.tar.gz
| Download URL | dataowl-0.2.0.tar.gz |
|---|---|
| Size | 74.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
caef85fbdbb0c876e841a6637e570ec854debd4a22bd03e0ebef10471a6a2946
|
|
BLAKE2b-256 checksum How to use checksums |
0cf78863988c6fb665c7fb892687f948d4c1d61583d4c8533c69337bab17cf28
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 8, 2026.
Transparency logRelease files / dataowl-0.2.0-py3-none-any.whl
| Download URL | dataowl-0.2.0-py3-none-any.whl |
|---|---|
| Size | 40.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
16c77b35313f53776d242b5588f7be6fc9165e3bede23e58c5b0441bc92762b6
|
|
BLAKE2b-256 checksum How to use checksums |
fb5da4fab3176951c9f078ce2b05606a2c65a60783b19ca2869533d36f9e2867
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 8, 2026.
Transparency log