@mosaic-code/pg-index-check 0.0.0-stage → 0.1.1

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.
package/LICENSE ADDED
@@ -0,0 +1,207 @@
1
+ # Do No Harm License
2
+
3
+ ## 1. Preamble
4
+
5
+ Most software today is developed with little to no thought of how it will be used, or the
6
+ consequences for our society and planet.
7
+
8
+ As software developers, we engineer the infrastructure of the 21st century. We recognise that our
9
+ infrastructure has great power to shape the world and the lives of those we share it with, and we
10
+ choose to consciously take responsibility for the social and environmental impacts of what we build.
11
+
12
+ We envisage a world free from injustice, inequality, and the reckless destruction of lives and our
13
+ planet. We reject slavery in all its forms, whether by force, indebtedness, or by algorithms that
14
+ hack human vulnerabilities. We seek a world where humankind is at peace with our neighbours, nature,
15
+ and ourselves. We want our work to enrich the physical, mental and spiritual wellbeing of all
16
+ society.
17
+
18
+ We build software to further this vision of a just world, or at the very least, to not put that
19
+ vision further from reach.
20
+
21
+ ## 2. Definitions
22
+
23
+ "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by
24
+ Sections 1 through 9 of this document.
25
+
26
+ "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is
27
+ granting the License.
28
+
29
+ "Legal Entity" shall mean the union of the acting entity and all other entities that control, are
30
+ controlled by, or are under common control with that entity. For the purposes of this definition,
31
+ "control" means (i) the power, direct or indirect, to cause the direction or management of such
32
+ entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the
33
+ outstanding shares, or (iii) beneficial ownership of such entity.
34
+
35
+ "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this
36
+ License.
37
+
38
+ "Source" form shall mean the preferred form for making modifications, including but not limited to
39
+ software source code, documentation source, and configuration files.
40
+
41
+ "Object" form shall mean any form resulting from mechanical transformation or translation of a
42
+ Source form, including but not limited to compiled object code, generated documentation, and
43
+ conversions to other media types.
44
+
45
+ "Work" shall mean the work of authorship, whether in Source or Object form, made available under the
46
+ License, as indicated by a copyright notice that is included in or attached to the work (an example
47
+ is provided in the Appendix below).
48
+
49
+ "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or
50
+ derived from) the Work and for which the editorial revisions, annotations, elaborations, or other
51
+ modifications represent, as a whole, an original work of authorship. For the purposes of this
52
+ License, Derivative Works shall not include works that remain separable from, or merely link (or
53
+ bind by name) to the interfaces of, the Work and Derivative Works thereof.
54
+
55
+ "Contribution" shall mean any work of authorship, including the original version of the Work and any
56
+ modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted
57
+ to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity
58
+ authorized to submit on behalf of the copyright owner. For the purposes of this definition,
59
+ "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or
60
+ its representatives, including but not limited to communication on electronic mailing lists, source
61
+ code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor
62
+ for the purpose of discussing and improving the Work, but excluding communication that is
63
+ conspicuously marked or otherwise designated in writing by the copyright owner as "Not a
64
+ Contribution."
65
+
66
+ "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a
67
+ Contribution has been received by Licensor and subsequently incorporated within the Work.
68
+
69
+ "Forests" shall mean 0.5 or more hectares of trees that were either planted more than 50 years ago
70
+ or were not planted by humans or human made equipment.
71
+
72
+ "Deforestation" shall mean the clearing, burning or destruction of 0.5 or more hectares of forests
73
+ within a 1 year period.
74
+
75
+ ## 3. Grant of Copyright License
76
+
77
+ Subject to the terms and conditions of this License, each Contributor hereby grants to You a
78
+ perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to
79
+ reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and
80
+ distribute the Work and such Derivative Works in Source or Object form.
81
+
82
+ ## 4. Grant of Patent License
83
+
84
+ Subject to the terms and conditions of this License, each Contributor hereby grants to You a
85
+ perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this
86
+ section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer
87
+ the Work, where such license applies only to those patent claims licensable by such Contributor that
88
+ are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s)
89
+ with the Work to which such Contribution(s) was submitted. If You institute patent litigation
90
+ against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or
91
+ a Contribution incorporated within the Work constitutes direct or contributory patent infringement,
92
+ then any patent licenses granted to You under this License for that Work shall terminate as of the
93
+ date such litigation is filed.
94
+
95
+ ## 5. Redistribution
96
+
97
+ You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with
98
+ or without modifications, and in Source or Object form, provided that You meet the following
99
+ conditions:
100
+
101
+ 1. You must give any other recipients of the Work or Derivative Works a copy of this License; and
102
+
103
+ 2. You must cause any modified files to carry prominent notices stating that You changed the
104
+ files; and
105
+
106
+ 3. You must retain, in the Source form of any Derivative Works that You distribute, all copyright,
107
+ patent, trademark, and attribution notices from the Source form of the Work, excluding those
108
+ notices that do not pertain to any part of the Derivative Works; and
109
+
110
+ 4. Neither the name of the copyright holder nor the names of its contributors may be used to endorse
111
+ or promote products derived from this software without specific prior written permission; and
112
+
113
+ 5. This software must not be used by any organisation, website, product, or service that:
114
+ 1. promotes, lobbies for or derives a majority of income from:
115
+ 1. **abuses of human rights**:
116
+ * human trafficking
117
+ * sex trafficking
118
+ * slavery or indentured servitude
119
+ * discrimination based on age, gender, gender identity, race, sexuality, religion, nationality
120
+ * hate speech
121
+ 2. **environmental destruction**:
122
+ * the extraction or sale of fossil fuels
123
+ * the destruction of habitats for threatened or endangered species, including through deforestation or burning of forests
124
+ * the abuse, inhumane killing or neglect of animals under human control
125
+ * industrial processes that generate waste products that threaten life
126
+ 3. **conflict and war**:
127
+ * warfare
128
+ * war crimes
129
+ * weapons manufacturing
130
+ * violence (except when required to protect public safety)
131
+ 4. **addictive or destructive products and services**:
132
+ * gambling
133
+ * tobacco
134
+ * products that encourage adversely addictive behaviours
135
+
136
+ 2. dissuades, lobbies against, or derives a majority of income from actions that discourage or frustrate:
137
+ * peace
138
+ * access to the rights set out in the Universal Declaration of Human Rights and the Convention on the Rights of the Child
139
+ * democratic processes
140
+ * peaceful assembly and association (including worker associations)
141
+ * a sustainable environment
142
+ ; and
143
+
144
+ 5. If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative
145
+ Works that You distribute must include a readable copy of the attribution notices contained
146
+ within such NOTICE file, excluding those notices that do not pertain to any part of the
147
+ Derivative Works, in at least one of the following places: within a NOTICE text file
148
+ distributed as part of the Derivative Works; within the Source form or documentation, if
149
+ provided along with the Derivative Works; or, within a display generated by the Derivative
150
+ Works, if and wherever such third-party notices normally appear. The contents of the NOTICE
151
+ file are for informational purposes only and do not modify the License. You may add Your own
152
+ attribution notices within Derivative Works that You distribute, alongside or as an addendum to
153
+ the NOTICE text from the Work, provided that such additional attribution notices cannot be
154
+ construed as modifying the License.
155
+
156
+ You may add Your own copyright statement to Your modifications and may provide additional or
157
+ different license terms and conditions for use, reproduction, or distribution of Your modifications,
158
+ or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of
159
+ the Work otherwise complies with the conditions stated in this License.
160
+
161
+ ## 6. Submission of Contributions
162
+
163
+ Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the
164
+ Work by You to the Licensor shall be under the terms and conditions of this License, without any
165
+ additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify
166
+ the terms of any separate license agreement you may have executed with Licensor regarding such
167
+ Contributions.
168
+
169
+ ## 7. Trademarks
170
+
171
+ This License does not grant permission to use the trade names, trademarks, service marks, or product
172
+ names of the Licensor, except as required for reasonable and customary use in describing the origin
173
+ of the Work and reproducing the content of the NOTICE file.
174
+
175
+ ## 8. Disclaimer of Warranty
176
+
177
+ Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each
178
+ Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
179
+ KIND, either express or implied, including, without limitation, any warranties or conditions of
180
+ TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely
181
+ responsible for determining the appropriateness of using or redistributing the Work and assume any
182
+ risks associated with Your exercise of permissions under this License.
183
+
184
+ ## 9. Limitation of Liability
185
+
186
+ In no event and under no legal theory, whether in tort (including negligence), contract, or
187
+ otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or
188
+ agreed to in writing, shall any Contributor be liable to You for damages, including any direct,
189
+ indirect, special, incidental, or consequential damages of any character arising as a result of this
190
+ License or out of the use or inability to use the Work (including but not limited to damages for
191
+ loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial
192
+ damages or losses), even if such Contributor has been advised of the possibility of such damages.
193
+
194
+ ## 10. Accepting Warranty or Additional Liability
195
+
196
+ While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee
197
+ for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights
198
+ consistent with this License. However, in accepting such obligations, You may act only on Your own
199
+ behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You
200
+ agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or
201
+ claims asserted against, such Contributor by reason of your accepting any such warranty or
202
+ additional liability.
203
+
204
+ ## Attribution
205
+
206
+ Do No Harm License [Contributor Covenant], (pre 1.0),
207
+ available at https://github.com/raisely/NoHarm
package/README.md CHANGED
@@ -1,3 +1,177 @@
1
- # Temporary Holding Version
1
+ # pg-index-check
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Fail your test suite when PostgreSQL queries have no usable index.
4
+
5
+ Your tests already run the queries your app makes. pg-index-check reads them back from `pg_stat_statements` after the suite, plans each one with sequential scans disabled, and reports any query that still has to read a whole table (or a whole index) to apply its filter.
6
+
7
+ ## Features
8
+
9
+ - **Works on tiny test tables** - With `enable_seqscan = off`, a sequential scan only survives when no index can serve the query, however few rows the table has
10
+ - **Any driver or ORM** - Pure PostgreSQL: captures statements with `pg_stat_statements`, so Prisma, Drizzle, Knex or raw `pg` all work without code changes
11
+ - **Source locations** - Picks up `/* path:line */` comments from [pg-source-comments](https://github.com/mosaic-code-coop/pg-source-comments) and shows where each query came from
12
+ - **Auditable allowlist** - Ignore rules need a reason, and rules that no longer match anything are reported
13
+
14
+ ## Requirements
15
+
16
+ - PostgreSQL 16+ (uses `EXPLAIN (GENERIC_PLAN)`)
17
+ - `pg_stat_statements` in `shared_preload_libraries` on the test database server
18
+ - Node.js 22.9.0+, pg 8.0.0+
19
+
20
+ ## Installation
21
+
22
+ ```bash
23
+ npm install --save-dev @mosaic-code/pg-index-check pg
24
+ ```
25
+
26
+ ## Quick Start
27
+
28
+ ### 1. Load pg_stat_statements on the test database
29
+
30
+ ```yaml
31
+ # docker-compose.yml
32
+ services:
33
+ postgres-test:
34
+ image: postgres:18-alpine
35
+ command: postgres -c shared_preload_libraries=pg_stat_statements
36
+ ```
37
+
38
+ GitHub Actions `services:` can't pass server flags, so start Postgres with `docker compose up -d --wait` (or `docker run ... postgres -c shared_preload_libraries=pg_stat_statements`) in a step instead.
39
+
40
+ ### 2. Check after the test suite
41
+
42
+ ```typescript
43
+ // tests/global-setup.ts
44
+ import { assertIndexes, resetCapture } from '@mosaic-code/pg-index-check';
45
+
46
+ const connectionString = process.env.TEST_DATABASE_URL!;
47
+
48
+ export async function setup() {
49
+ // Installs the extension in the database if needed, and clears earlier statements
50
+ await resetCapture({ connectionString });
51
+ }
52
+
53
+ export async function teardown() {
54
+ // Throws an IndexCheckError listing every query without a usable index
55
+ await assertIndexes({
56
+ connectionString,
57
+ ignore: [{ table: 'public.countries', reason: 'fixed lookup table, ~250 rows' }],
58
+ });
59
+ }
60
+ ```
61
+
62
+ ```typescript
63
+ // vitest.config.ts
64
+ export default defineConfig({
65
+ test: { globalSetup: ['tests/global-setup.ts'] },
66
+ });
67
+ ```
68
+
69
+ A failing check fails the run (vitest exits non-zero when teardown throws):
70
+
71
+ ```
72
+ pg-index-check: 1 finding(s) across 42 statement(s)
73
+
74
+ SEQ_SCAN_FILTER public.orders at src/orders/service.ts:88
75
+ condition: (orders.customer_id = $1)
76
+ fix: CREATE INDEX ON public.orders (customer_id)
77
+ statement: SELECT /* src/orders/service.ts:88 */ "public"."orders"."id", ... WHERE "public"."orders"."customer_id" = $1
78
+ ```
79
+
80
+ ## What Gets Flagged
81
+
82
+ Every distinct `SELECT`, `WITH`, `UPDATE` and `DELETE` run against the current database since `resetCapture()` is planned with `EXPLAIN (GENERIC_PLAN, VERBOSE, FORMAT JSON)`.
83
+
84
+ | Kind | Plan shape | Typical cause |
85
+ |---|---|---|
86
+ | `SEQ_SCAN_FILTER` | Sequential scan with a filter | No index on the filtered column(s) |
87
+ | `FULL_INDEX_SCAN` | Index scan with no index condition, filtering on one column with `=` | `WHERE status = $1 ORDER BY id LIMIT n` with no index on `status` |
88
+ | `TRAILING_INDEX_COLUMN` | Index condition with `=` that skips the index's first column (PostgreSQL 16/17 only) | Filtering on `created_at` with an index on `(org_id, created_at)` |
89
+ | `UNPLANNABLE` | PostgreSQL could not plan the statement | Statement refers to a table the tests dropped |
90
+
91
+ Not flagged:
92
+
93
+ - `<>` / `NOT IN` filters, which a btree index can't serve
94
+ - Range and `IS NULL` filters on a whole-index scan, since pagination walks the primary key on purpose
95
+ - `TRAILING_INDEX_COLUMN` on PostgreSQL 18+, which can skip-scan
96
+ - `INSERT`, DDL, utility statements, and queries on `pg_catalog` / `information_schema`
97
+
98
+ ## Configuration
99
+
100
+ ```typescript
101
+ await assertIndexes({
102
+ connectionString,
103
+
104
+ // Findings to accept. Needs a table and/or statement, and a reason.
105
+ // table: string matches exactly (`public.orders`), or a RegExp
106
+ // statement: string matches if the statement contains it, or a RegExp
107
+ ignore: [
108
+ { table: 'public.countries', reason: 'fixed lookup table' },
109
+ { statement: /FROM "public"."audit_log"/, reason: 'admin-only export, runs nightly' },
110
+ ],
111
+
112
+ // 'fail' (default): unplannable statements fail the check
113
+ // 'report': list them in the report without failing
114
+ unplannable: 'fail',
115
+ });
116
+ ```
117
+
118
+ `checkIndexes()` takes the same options and returns the result without throwing:
119
+
120
+ ```typescript
121
+ const { statementCount, findings, reported, staleIgnoreRules } = await checkIndexes({ connectionString });
122
+ ```
123
+
124
+ ## Using with pg-source-comments
125
+
126
+ When the app's `pg` pool is wrapped with [pg-source-comments](https://github.com/mosaic-code-coop/pg-source-comments) 0.4.0+, findings include the source file and line of the query.
127
+
128
+ - **Raw `pg` queries:** wrap the pool with `createTrackedPool`.
129
+ - **Prisma queries** (`findMany` etc.): also wrap the client with `trackPrismaClient(new PrismaClient({ adapter: new PrismaPg(pool) }))`. Prisma sends queries to the pool after an async hop, so without this the calling code is off the stack and no location is recorded. Needs pg-source-comments 0.4.0+.
130
+
131
+ `pg_stat_statements` keeps the text of the first call it sees for each distinct query, so a query made from several places shows the first location only.
132
+
133
+ ## Limitations
134
+
135
+ - **Only tested queries are checked** - A query the test suite never runs isn't seen
136
+ - **Joins** - An index that only a join would use (e.g. an unindexed foreign key on the inner side) isn't reported
137
+ - **Generic plans** - Plans ignore actual parameter values. The check answers "can an index serve this?", not "will production use it?"
138
+ - **One database** - Only statements from the database in `connectionString` are checked; `resetCapture()` clears statistics for the whole server
139
+
140
+ ## How It Works
141
+
142
+ ```
143
+ resetCapture() test suite runs assertIndexes()
144
+ │ │ │
145
+ pg_stat_statements_reset app queries recorded read statements for this database
146
+ in pg_stat_statements SET enable_seqscan = off (own connection)
147
+ EXPLAIN (GENERIC_PLAN) each statement
148
+ apply rules → ignore rules → report
149
+ ```
150
+
151
+ The check runs on its own connection, so `enable_seqscan` never changes for the app's sessions.
152
+
153
+ ## Development
154
+
155
+ ```bash
156
+ # Install dependencies
157
+ npm install
158
+
159
+ # Unit tests
160
+ npm test
161
+
162
+ # Integration tests (starts PostgreSQL 18 with docker compose)
163
+ npm run test:integration:setup
164
+
165
+ # Against another PostgreSQL version
166
+ docker compose down && POSTGRES_VERSION=16 npm run test:integration:setup
167
+
168
+ # Type check
169
+ npm run typecheck
170
+
171
+ # Build
172
+ npm run build
173
+ ```
174
+
175
+ ## License
176
+
177
+ [Do No Harm](https://github.com/raisely/NoHarm)