Skip to content

test: improve unit test coverage for core modules#80

Open
veksen wants to merge 13 commits intomainfrom
veksen/improve-test-coverage
Open

test: improve unit test coverage for core modules#80
veksen wants to merge 13 commits intomainfrom
veksen/improve-test-coverage

Conversation

@veksen
Copy link
Member

@veksen veksen commented Mar 17, 2026

Summary

  • Add test files for 7 previously untested modules: json.ts, errors.ts, schema_differ.ts, sanitize.ts, recent-query.ts, config.ts, dependency-tree.ts
  • Self-reviewed and strengthened all new tests: fixed weak assertions, added missing branch/edge-case coverage, corrected API usage
  • Test count: 55 → 125 (+70 tests), all passing on Node 24

Details

Module Coverage added
json.ts \r/\t preservation, whitespace-only input, non-whitespace prefix
errors.ts All 3 error classes: serialization, HTTP responses, instanceof
schema_differ.ts Add/remove/replace ops, constraints, per-connectable tracking (uses Connectable.fromString)
sanitize.ts HOSTED vs non-HOSTED, determinism, sensitive value scrubbing
recent-query.ts Constructor derived booleans, withOptimization, analyze integration, static helpers
config.ts Success/error/network paths, URL encoding, partial responses, lastSeenQueries
dependency-tree.ts Multi-level FK chains, dedup, maxRows/incomplete_dependency_chain, error paths, onStartAnalyze

Test plan

  • All 125 tests pass locally (npx vitest run)
  • Verified on Node 24
  • Docker-dependent tests pass with Docker Desktop running

🤖 Generated with Claude Code

veksen and others added 13 commits March 17, 2026 15:49
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Use Connectable.fromString() instead of private constructor.
Assert specific op types (add/remove/replace) instead of just
checking that diffs exist. Add coverage for removals, modifications,
and constraint changes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…maxRows, and error paths

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…rage

Cover constructor derived booleans (isTargetlessSelectQuery only true
for SELECTs), data field copying, withOptimization mutation,
analyze integration, and edge cases for static helpers.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Found New Recommended Indexes

The following indexes are likely to make the most impact to your queries. They're ordered by how many queries seen in your tests

Index Definition Usage Count
assets(event_id, uploader_id, inserted_at desc) 1
guest_ip_addresses(ip_address) 1

Statistics Mode

When generating recommendations, we made the following changes to your database statistics:

fixed rows per table

  • Rows per table: 10000
  • Pages per table: 1

Instead of assuming a fixed number of rows per table (which can cause unnecessary recommendations), you can export statistics from your production database and import it using the STATISTICS_PATH environment variable. You can read more about how to sync stats here.

- name: Analyze
  uses: query-doctor/analyzer@v0
  env:
    STATISTICS_PATH: ./statistics.json

Optimization Overview

Query Base Cost Optimized Cost Improvement
30f59ed4d6cab15b 15922.03 1638.53 9.72x
62cddf9e2b2dad50 126 9.04 13.94x

Query 30f59ed4d6cab15b

New indexes improve cost by 9.72x:
  1. assets(event_id, uploader_id, inserted_at desc)
View Query (too long to display inline)
SELECT
  "guests"."id",
  "guests"."session_id",
  "guests"."username",
  "guests"."avatar_path",
  "guests"."color",
  "guests"."side",
  "guests"."audio_recording_path",
  "guests"."audio_recording_public",
  "guests"."memo",
  "guests"."memo_public",
  "guests"."setup_at",
  "guests"."last_upload",
  "guests"."inserted_at",
  "guests"."updated_at",
  "userAssets"."id",
  "userAssets"."kind",
  "userAssets"."event_id",
  "userAssets"."uploader_id",
  "userAssets"."uploader_ip",
  "userAssets"."path",
  "userAssets"."file_size",
  "userAssets"."width",
  "userAssets"."height",
  "userAssets"."visible_at",
  "userAssets"."deleted_at",
  "userAssets"."inserted_at",
  "userAssets"."updated_at"
FROM
  (
    SELECT
      "id",
      "session_id",
      "username",
      "avatar_path",
      "color",
      "side",
      "audio_recording_path",
      "audio_recording_public",
      "memo",
      "memo_public",
      "setup_at",
      "last_upload",
      "inserted_at",
      "updated_at"
    FROM
      "guests"
    ORDER BY
      "guests"."last_upload" DESC,
      "guests"."id" DESC
    LIMIT
      100
  ) "guests"
  CROSS JOIN LATERAL (
    SELECT
      "id",
      "kind",
      "event_id",
      "uploader_id",
      "uploader_ip",
      "path",
      "file_size",
      "width",
      "height",
      "visible_at",
      "deleted_at",
      "inserted_at",
      "updated_at"
    FROM
      "assets"
    WHERE
      (
        "assets"."event_id" = (
          SELECT
            "id"
          FROM
            "events"
          WHERE
            "events"."event_key" = '01JKCVP4M2CH34SVTQGHSW4Y5G'
        )
        AND "assets"."uploader_id" = "guests"."id"
      )
    ORDER BY
      "assets"."inserted_at" DESC
    LIMIT
      100
  ) "userAssets";
View Explain Plan (before optimization)
{
  "Node Type": "Nested Loop",
  "Parallel Aware": false,
  "Async Capable": false,
  "Join Type": "Inner",
  "Startup Cost": 159.35,
  "Total Cost": 15922.03,
  "Plan Rows": 100,
  "Plan Width": 370,
  "Inner Unique": false,
  "Plans": [
    {
      "Node Type": "Limit",
      "Parent Relationship": "Outer",
      "Parallel Aware": false,
      "Async Capable": false,
      "Startup Cost": 0.16,
      "Total Cost": 1.78,
      "Plan Rows": 100,
      "Plan Width": 238,
      "Plans": [
        {
          "Node Type": "Index Scan",
          "Parent Relationship": "Outer",
          "Parallel Aware": false,
          "Async Capable": false,
          "Scan Direction": "Forward",
          "Index Name": "guests_last_upload_desc_id_desc_index",
          "Relation Name": "guests",
          "Alias": "guests",
          "Startup Cost": 0.16,
          "Total Cost": 162.16,
          "Plan Rows": 10000,
          "Plan Width": 238
        }
      ]
    },
    {
      "Node Type": "Limit",
      "Parent Relationship": "Inner",
      "Parallel Aware": false,
      "Async Capable": false,
      "Startup Cost": 159.19,
      "Total Cost": 159.19,
      "Plan Rows": 1,
      "Plan Width": 132,
      "Plans": [
        {
          "Node Type": "Index Scan",
          "Parent Relationship": "InitPlan",
          "Subplan Name": "InitPlan 1 (returns $0)",
          "Parallel Aware": false,
          "Async Capable": false,
          "Scan Direction": "Forward",
          "Index Name": "events_event_key_index",
          "Relation Name": "events",
          "Alias": "events",
          "Startup Cost": 0.16,
          "Total Cost": 8.18,
          "Plan Rows": 1,
          "Plan Width": 8,
          "Index Cond": "(event_key = '01JKCVP4M2CH34SVTQGHSW4Y5G'::text)"
        },
        {
          "Node Type": "Sort",
          "Parent Relationship": "Outer",
          "Parallel Aware": false,
          "Async Capable": false,
          "Startup Cost": 151.01,
          "Total Cost": 151.01,
          "Plan Rows": 1,
          "Plan Width": 132,
          "Sort Key": [
            "assets.inserted_at DESC"
          ],
          "Plans": [
            {
              "Node Type": "Seq Scan",
              "Parent Relationship": "Outer",
              "Parallel Aware": false,
              "Async Capable": false,
              "Relation Name": "assets",
              "Alias": "assets",
              "Startup Cost": 0,
              "Total Cost": 151,
              "Plan Rows": 1,
              "Plan Width": 132,
              "Filter": "((event_id = $0) AND (uploader_id = guests.id))"
            }
          ]
        }
      ]
    }
  ]
}
View Explain Plan (after optimization)
{
  "Node Type": "Nested Loop",
  "Parallel Aware": false,
  "Async Capable": false,
  "Join Type": "Inner",
  "Startup Cost": 8.5,
  "Total Cost": 1638.53,
  "Plan Rows": 100,
  "Plan Width": 370,
  "Inner Unique": false,
  "Plans": [
    {
      "Node Type": "Limit",
      "Parent Relationship": "Outer",
      "Parallel Aware": false,
      "Async Capable": false,
      "Startup Cost": 0.16,
      "Total Cost": 1.78,
      "Plan Rows": 100,
      "Plan Width": 238,
      "Plans": [
        {
          "Node Type": "Index Scan",
          "Parent Relationship": "Outer",
          "Parallel Aware": false,
          "Async Capable": false,
          "Scan Direction": "Forward",
          "Index Name": "guests_last_upload_desc_id_desc_index",
          "Relation Name": "guests",
          "Alias": "guests",
          "Startup Cost": 0.16,
          "Total Cost": 162.16,
          "Plan Rows": 10000,
          "Plan Width": 238
        }
      ]
    },
    {
      "Node Type": "Limit",
      "Parent Relationship": "Inner",
      "Parallel Aware": false,
      "Async Capable": false,
      "Startup Cost": 8.34,
      "Total Cost": 16.36,
      "Plan Rows": 1,
      "Plan Width": 132,
      "Plans": [
        {
          "Node Type": "Index Scan",
          "Parent Relationship": "InitPlan",
          "Subplan Name": "InitPlan 1 (returns $0)",
          "Parallel Aware": false,
          "Async Capable": false,
          "Scan Direction": "Forward",
          "Index Name": "events_event_key_index",
          "Relation Name": "events",
          "Alias": "events",
          "Startup Cost": 0.16,
          "Total Cost": 8.18,
          "Plan Rows": 1,
          "Plan Width": 8,
          "Index Cond": "(event_key = '01JKCVP4M2CH34SVTQGHSW4Y5G'::text)"
        },
        {
          "Node Type": "Index Scan",
          "Parent Relationship": "Outer",
          "Parallel Aware": false,
          "Async Capable": false,
          "Scan Direction": "Forward",
          "Index Name": "assets(event_id, uploader_id, inserted_at desc)",
          "Relation Name": "assets",
          "Alias": "assets",
          "Startup Cost": 0.16,
          "Total Cost": 8.18,
          "Plan Rows": 1,
          "Plan Width": 132,
          "Index Cond": "((event_id = $0) AND (uploader_id = guests.id))"
        }
      ]
    }
  ]
}

Query 62cddf9e2b2dad50

New indexes improve cost by 13.94x:
  1. guest_ip_addresses(ip_address)
SELECT
  *
FROM
  guest_ip_addresses
WHERE
  ip_address = '127.0.0.1';
View Explain Plan (before optimization)
{
  "Node Type": "Seq Scan",
  "Parallel Aware": false,
  "Async Capable": false,
  "Relation Name": "guest_ip_addresses",
  "Alias": "guest_ip_addresses",
  "Startup Cost": 0,
  "Total Cost": 126,
  "Plan Rows": 50,
  "Plan Width": 56,
  "Filter": "(ip_address = '127.0.0.1'::text)"
}
View Explain Plan (after optimization)
{
  "Node Type": "Index Scan",
  "Parallel Aware": false,
  "Async Capable": false,
  "Scan Direction": "Forward",
  "Index Name": "guest_ip_addresses(ip_address)",
  "Relation Name": "guest_ip_addresses",
  "Alias": "guest_ip_addresses",
  "Startup Cost": 0.16,
  "Total Cost": 9.04,
  "Plan Rows": 50,
  "Plan Width": 56,
  "Index Cond": "(ip_address = '127.0.0.1'::text)"
}

What are the numbers next to the query? The numbers are a fingerprint uniquely identifying the query. Let us know in the Discord if you'd like to be able to assign unique names to your queries.
What is cost? Cost is an arbitrary value representing the amount of work postgres decided it needs to do to execute a query based on what it knows about the database and the query itself.
We use cost to look for improvements when checking if an index helps optimize a query in CI as the full production dataset is simply not available to work with.
Execution metadata
Log size
209261 bytes
Time elapsed
2201ms
Queries Seen
26
Queries matched
10
Queries optimized
2
Queries errored
1

Copy link
Collaborator

@Xetera Xetera left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't import find dependencies or follow FKs of source db anymore so almost everything from dependenct-tree.test.ts can be removed

@veksen
Copy link
Member Author

veksen commented Mar 20, 2026

We don't import find dependencies or follow FKs of source db anymore so almost everything from dependency-tree.test.ts can be removed

https://github.com/Query-Doctor/analyzer/blob/1b5e873/src/sync/syncer.ts#L96-L97

it seems like it's still in use?

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.

2 participants