Skip to content

Filter internal __pgsqlite_* tables from client sqlite_master queries#81

Closed
robertjbass wants to merge 3 commits into
erans:mainfrom
robertjbass:fix/filter-internal-tables-from-sqlite-master
Closed

Filter internal __pgsqlite_* tables from client sqlite_master queries#81
robertjbass wants to merge 3 commits into
erans:mainfrom
robertjbass:fix/filter-internal-tables-from-sqlite-master

Conversation

@robertjbass

Copy link
Copy Markdown

Filter internal __pgsqlite_* tables from client sqlite_master queries

What

pgsqlite keeps its own bookkeeping in tables named __pgsqlite_*
(__pgsqlite_schema, __pgsqlite_migrations, __pgsqlite_enum_types,
__pgsqlite_string_constraints, __pgsqlite_numeric_constraints,
__pgsqlite_datetime_cache, ...). The catalog queries pgsqlite synthesizes
itself already exclude these (for example src/catalog/pg_class.rs appends
AND name NOT LIKE '__pgsqlite_%'), but a sqlite_master / sqlite_schema
query written directly by a client is passed through verbatim, so tools that
introspect the native SQLite catalog see pgsqlite's internal tables alongside
the user's own.

How

This adds a branch to CatalogInterceptor::intercept_query
(src/catalog/query_interceptor.rs) that runs before the existing
pg_catalog / information_schema gate, because a sqlite_master query
contains none of those substrings and would otherwise fall straight through.

The new branch:

  • parses the query with the already-bundled sqlparser;
  • walks the FROM clause (including JOINs) for every sqlite_master /
    sqlite_schema relation, honoring table aliases;
  • ANDs <relation>.name NOT LIKE '__pgsqlite_%' into the WHERE clause,
    qualified by the relation's alias (or bare name) so it stays unambiguous
    across joins;
  • executes the rewritten query and returns the result.

It fails open (runs the original query unchanged) on any parse failure, on
anything that is not a single SELECT, and when sqlite_master appears only
as a string literal or column name rather than as a real FROM relation. Only
the unambiguous __pgsqlite_% prefix is filtered; sqlite_% internal objects
are left untouched (they are part of SQLite's own catalog and are what a normal
SQLite client expects to see).

The core rewrite is factored into a pure rewrite_sqlite_master_query(&str) -> Option<String> so it can be unit-tested without a database.

Tests

  • Unit tests in src/catalog/query_interceptor.rs: plain select, existing
    WHERE preserved via AND, aliased relation uses the alias qualifier,
    JOINed relation stays join-safe, sqlite_schema alias, unparseable SQL fails
    open, sqlite_master as a literal is left untouched, and a similarly-named
    user table (sqlite_master_backup) is not matched.
  • New integration test tests/catalog_sqlite_master_filter_test.rs: over the
    wire, internal tables are hidden from plain / aliased / sqlite_schema
    queries while user tables, indexes and views are still returned, and a
    non-sqlite_master query is untouched.

Scope note

pgsqlite also materializes its pg_catalog / information_schema emulation as
real SQLite tables (pg_class, information_schema_tables, ...), which also
appear in a client sqlite_master listing. Filtering those is a broader policy
question (a user could name a table pg_*), so this PR intentionally limits
itself to the __pgsqlite_* prefix. See the linked issue for details.

Closes #80.

pgsqlite keeps its own bookkeeping in tables named __pgsqlite_* (schema,
migrations, enum metadata, string/numeric constraints, datetime cache,
etc.). The catalog handlers that pgsqlite synthesizes itself already
exclude these (e.g. src/catalog/pg_class.rs appends
"AND name NOT LIKE '__pgsqlite_%'"), but a sqlite_master / sqlite_schema
query written directly by a client is passed through verbatim, so tools
that introspect the native SQLite catalog see pgsqlite's internal tables
alongside the user's own.

Add a branch to CatalogInterceptor::intercept_query that runs before the
pg_catalog / information_schema gate (a sqlite_master query contains none
of those substrings). It parses the query with the bundled sqlparser,
finds every sqlite_master / sqlite_schema relation in the FROM clause
(alias-aware and JOIN-safe, matching the SQLite sqlite_schema alias too),
ANDs "<relation>.name NOT LIKE '__pgsqlite_%'" into the WHERE clause, and
executes the rewritten query. It fails open (runs the original query
unchanged) on any parse failure or when sqlite_master is not an actual
FROM relation, so ordinary queries are untouched. Only the __pgsqlite_%
prefix is filtered; sqlite_% internal objects are left as-is.

Covered by new unit tests (rewrite: plain / aliased / joined /
sqlite_schema / fail-open / literal-not-table / similarly-named-table)
and an integration test (catalog_sqlite_master_filter_test) asserting the
internal tables are hidden over the wire while user tables, indexes and
views are still returned.
…lite_master

The sqlite_master / sqlite_schema filter previously hid only the __pgsqlite_* bookkeeping tables. Extend the rewritten predicate to also exclude the four pg_catalog tables that are physically materialized as real SQLite tables (pg_attrdef, pg_constraint, pg_depend, pg_index) and any index or trigger owned by an internal table, by filtering on tbl_name as well as name. This keeps an unqualified SELECT * FROM sqlite_master clean, not just a type = 'table' scan.

The four table names are a closed set defined next to the __pgsqlite_ prefix as PG_CATALOG_INTERNAL_TABLES, with src/migration/registry.rs cited as the source of truth. Matching is exact-set, not a pg_% wildcard, so a user table such as pg_indexes is still returned. The rewrite keeps its alias/JOIN awareness and fail-open-on-parse-failure behavior.
…e_master

Extends the client sqlite_master/sqlite_schema filter to exclude the 24
pg_catalog and information_schema compatibility views pgsqlite
materializes as real SQLite views (source of truth: migration/registry.rs)
in addition to the __pgsqlite_* tables, the four materialized pg_catalog
tables, and their internal indexes. A raw type='view' scan or an
unqualified SELECT * now shows only the user's own schema.

The exclusion is an exact-name set, never a pg_% / information_schema_%
wildcard, so a user-owned view or table such as pg_my_report is still
returned. Filter applies to both name and tbl_name for symmetry, keeps
the same alias/JOIN awareness, and fails open on any parse failure.
@robertjbass

robertjbass commented Jul 11, 2026

Copy link
Copy Markdown
Author

Superseded by #83 (the current, working PR: result filtering rather than query rewriting). #82 was an interim attempt, now closed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Internal __pgsqlite_* tables leak into client sqlite_master queries

1 participant