@mosaic-code/prisma-deadlock-avoidance-tests 0.1.0

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.
Files changed (43) hide show
  1. package/LICENSE +207 -0
  2. package/README.md +248 -0
  3. package/dist/assertions/row-assertion.d.ts +18 -0
  4. package/dist/assertions/row-assertion.d.ts.map +1 -0
  5. package/dist/assertions/row-assertion.js +53 -0
  6. package/dist/assertions/row-assertion.js.map +1 -0
  7. package/dist/assertions/table-assertion.d.ts +17 -0
  8. package/dist/assertions/table-assertion.d.ts.map +1 -0
  9. package/dist/assertions/table-assertion.js +33 -0
  10. package/dist/assertions/table-assertion.js.map +1 -0
  11. package/dist/extension.d.ts +60 -0
  12. package/dist/extension.d.ts.map +1 -0
  13. package/dist/extension.js +322 -0
  14. package/dist/extension.js.map +1 -0
  15. package/dist/graphs/row-graph.d.ts +61 -0
  16. package/dist/graphs/row-graph.d.ts.map +1 -0
  17. package/dist/graphs/row-graph.js +231 -0
  18. package/dist/graphs/row-graph.js.map +1 -0
  19. package/dist/graphs/table-graph.d.ts +61 -0
  20. package/dist/graphs/table-graph.d.ts.map +1 -0
  21. package/dist/graphs/table-graph.js +123 -0
  22. package/dist/graphs/table-graph.js.map +1 -0
  23. package/dist/index.d.ts +6 -0
  24. package/dist/index.d.ts.map +1 -0
  25. package/dist/index.js +8 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/types.d.ts +98 -0
  28. package/dist/types.d.ts.map +1 -0
  29. package/dist/types.js +2 -0
  30. package/dist/types.js.map +1 -0
  31. package/dist/utils/caller-extractor.d.ts +27 -0
  32. package/dist/utils/caller-extractor.d.ts.map +1 -0
  33. package/dist/utils/caller-extractor.js +144 -0
  34. package/dist/utils/caller-extractor.js.map +1 -0
  35. package/dist/utils/primary-key-extractor.d.ts +23 -0
  36. package/dist/utils/primary-key-extractor.d.ts.map +1 -0
  37. package/dist/utils/primary-key-extractor.js +128 -0
  38. package/dist/utils/primary-key-extractor.js.map +1 -0
  39. package/dist/utils/raw-query-parser.d.ts +17 -0
  40. package/dist/utils/raw-query-parser.d.ts.map +1 -0
  41. package/dist/utils/raw-query-parser.js +58 -0
  42. package/dist/utils/raw-query-parser.js.map +1 -0
  43. package/package.json +79 -0
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 ADDED
@@ -0,0 +1,248 @@
1
+ # @mosaic-code/prisma-deadlock-avoidance-tests
2
+
3
+ A Prisma extension for detecting deadlock risks by tracking table and row locking order across transactions. Intended for use in test suites only.
4
+
5
+ ## Purpose
6
+
7
+ Database deadlocks are a structural problem that occur when different code paths lock resources in conflicting orders. In a real application, deadlocks often emerge from the interaction between multiple unrelated parts of the codebase—each following its own locking pattern—that only reveal themselves when those paths execute concurrently under load.
8
+
9
+ This library helps ensure your codebase applies deadlock-prevention best practices before these issues become production problems. It does this by building a holistic graph of every table and row lock ordering across your entire test suite. By accumulating data from all tests, it can detect when different parts of your application would lock resources in ways that could deadlock.
10
+
11
+ **NOTE**: The library only works effectively when allowed to collect data continuously through your whole test suite. Configure it before your first test runs, check the risks its identified at the end of all tests.
12
+
13
+ The library detects two types of deadlock risk:
14
+
15
+ 1. **Table ordering detection** - Identifies when different code paths lock tables in inconsistent orders
16
+ 2. **Row ordering detection** - Identifies when rows within a table are locked in inconsistent orders
17
+
18
+ ## Installation
19
+
20
+ ```bash
21
+ npm install @mosaic-code/prisma-deadlock-avoidance-tests
22
+ ```
23
+
24
+ ## Quick Start
25
+
26
+ ```typescript
27
+ import { PrismaClient } from '@prisma/client'
28
+ import {
29
+ withDeadlockDetection,
30
+ assertNoDeadlockRisk,
31
+ resetDeadlockDetection,
32
+ } from '@mosaic-code/prisma-deadlock-avoidance-tests'
33
+
34
+ // Wrap your Prisma client - transactions are tracked automatically
35
+ const prisma = withDeadlockDetection(new PrismaClient())
36
+
37
+ // At the start of your test run, clear any previous data
38
+ beforeAll(() => {
39
+ resetDeadlockDetection()
40
+ })
41
+
42
+ // Your transactions are tracked automatically
43
+ await prisma.$transaction(async (tx) => {
44
+ await tx.user.update({ where: { id: 1 }, data: { name: 'Updated' } })
45
+ await tx.post.create({ data: { title: 'New Post', authorId: 1 } })
46
+ })
47
+
48
+ // At the end of your test suite, check for deadlock risks
49
+ afterAll(() => {
50
+ assertNoDeadlockRisk()
51
+ })
52
+ ```
53
+
54
+ ## API
55
+
56
+ ### Extension
57
+
58
+ #### `withDeadlockDetection(client, config?)`
59
+
60
+ Wraps a Prisma client with deadlock detection. All transactions are automatically tracked.
61
+
62
+ ```typescript
63
+ const prisma = withDeadlockDetection(new PrismaClient())
64
+
65
+ // Optional: disable tracking
66
+ const prisma = withDeadlockDetection(new PrismaClient(), { enabled: false })
67
+ ```
68
+
69
+ ### Tracking Functions
70
+
71
+ #### `trackForUpdate(model, fn)`
72
+
73
+ Wrapper for integrating with [@mosaic-code/prisma-select-for-update](https://github.com/mosaic-code-coop/prisma-select-for-update). Use this to track SELECT FOR UPDATE operations.
74
+
75
+ ```typescript
76
+ await prisma.$transaction(async (tx) => {
77
+ const user = await trackForUpdate('User', () =>
78
+ tx.user.findUniqueForUpdate({ where: { id: 1 } })
79
+ )
80
+ await tx.post.update({ where: { id: postId }, data: { ... } })
81
+ })
82
+ ```
83
+
84
+ ### Assertion Functions
85
+
86
+ #### `assertConsistentTableLocking()`
87
+
88
+ Checks that tables have been locked in a consistent order across all tracked transactions. Throws `TableLockingAssertionError` if a cycle is detected.
89
+
90
+ ```typescript
91
+ assertConsistentTableLocking()
92
+ // Throws if Transaction A locks User->Post but Transaction B locks Post->User
93
+ ```
94
+
95
+ #### `assertConsistentRowLocking(options?)`
96
+
97
+ Checks that rows within tables have been locked in a consistent order. Throws `RowLockingAssertionError` if violations are detected.
98
+
99
+ Options:
100
+ - `tables?: string[]` - Specific tables to check (defaults to all)
101
+ - `strict?: StrictMode` - Ordering strictness
102
+
103
+ StrictMode values:
104
+ - `false` (default) - Only cycles are violations
105
+ - `true` - Must be consistently ascending OR descending
106
+ - `'ASC'` - Must always be ascending order
107
+ - `'DESC'` - Must always be descending order
108
+
109
+ ```typescript
110
+ // Only check for cycles
111
+ assertConsistentRowLocking({ strict: false })
112
+
113
+ // Require consistent ordering direction
114
+ assertConsistentRowLocking({ strict: true })
115
+
116
+ // Require specific ordering
117
+ assertConsistentRowLocking({ tables: ['User'], strict: 'ASC' })
118
+ ```
119
+
120
+ #### `assertNoDeadlockRisk(rowOptions?)`
121
+
122
+ Convenience function that calls both `assertConsistentTableLocking()` and `assertConsistentRowLocking()`.
123
+
124
+ ```typescript
125
+ assertNoDeadlockRisk()
126
+ assertNoDeadlockRisk({ strict: 'ASC' })
127
+ ```
128
+
129
+ ### Utility Functions
130
+
131
+ #### `resetDeadlockDetection()`
132
+
133
+ Clears all tracked state. Use this to start fresh when beginning a new test run (e.g., in CI or when manually re-running tests).
134
+
135
+ **Important**: Do NOT call this between test files. The library needs to accumulate data across your entire test suite to detect cross-file deadlock risks.
136
+
137
+ ```typescript
138
+ beforeAll(() => {
139
+ resetDeadlockDetection()
140
+ })
141
+ ```
142
+
143
+ ## Error Types
144
+
145
+ ### `TableLockingAssertionError`
146
+
147
+ Thrown when inconsistent table locking order is detected.
148
+
149
+ ```typescript
150
+ try {
151
+ assertConsistentTableLocking()
152
+ } catch (error) {
153
+ if (error instanceof TableLockingAssertionError) {
154
+ console.log(error.cycleInfo.tables) // ['User', 'Post']
155
+ console.log(error.cycleInfo.callers) // Caller locations
156
+ }
157
+ }
158
+ ```
159
+
160
+ ### `RowLockingAssertionError`
161
+
162
+ Thrown when inconsistent row locking order is detected.
163
+
164
+ ```typescript
165
+ try {
166
+ assertConsistentRowLocking({ strict: 'ASC' })
167
+ } catch (error) {
168
+ if (error instanceof RowLockingAssertionError) {
169
+ console.log(error.issues) // Array of violations by table
170
+ }
171
+ }
172
+ ```
173
+
174
+ ## How It Works
175
+
176
+ ### Table Ordering
177
+
178
+ The library builds a directed graph of table lock ordering:
179
+ - Each node is a table name
180
+ - Each edge represents "Table A was locked before Table B" within a transaction
181
+ - A cycle in this graph indicates potential deadlock risk
182
+
183
+ ### Row Ordering
184
+
185
+ For each table, a separate directed graph tracks row lock ordering:
186
+ - Each node is a primary key value
187
+ - Edges represent the order rows were locked within operations
188
+ - Cycles indicate potential deadlock risk
189
+ - Strict mode can enforce ascending/descending order
190
+
191
+ ### What Gets Tracked
192
+
193
+ Locking operations tracked:
194
+ - `create`, `createMany`, `createManyAndReturn`
195
+ - `update`, `updateMany`
196
+ - `delete`, `deleteMany`
197
+ - `upsert`
198
+ - Raw queries (best-effort table inference from SQL)
199
+ - `trackForUpdate()` calls
200
+
201
+ ## Best Practices
202
+
203
+ 1. **Don't reset between test files** - The library needs to accumulate data across your entire test suite to detect cross-file deadlock risks. Only reset when starting a completely fresh test run (e.g., in CI or when manually re-running tests).
204
+
205
+ 2. **Assert at end of suite** - Call `assertNoDeadlockRisk()` in a global `afterAll`
206
+
207
+ 3. **Use with prisma-lock-for-update** - Wrap forUpdate calls with `trackForUpdate()`
208
+
209
+ ## Example: Full Test Setup
210
+
211
+ ```typescript
212
+ import { describe, it, beforeAll, afterAll } from 'vitest'
213
+ import { PrismaClient } from '@prisma/client'
214
+ import {
215
+ withDeadlockDetection,
216
+ assertNoDeadlockRisk,
217
+ resetDeadlockDetection,
218
+ } from '@mosaic-code/prisma-deadlock-avoidance-tests'
219
+
220
+ const prisma = withDeadlockDetection(new PrismaClient())
221
+
222
+ beforeAll(() => {
223
+ resetDeadlockDetection()
224
+ })
225
+
226
+ afterAll(() => {
227
+ assertNoDeadlockRisk()
228
+ })
229
+
230
+ describe('User operations', () => {
231
+ it('updates user and creates post', async () => {
232
+ await prisma.$transaction(async (tx) => {
233
+ await tx.user.update({ where: { id: 1 }, data: { name: 'New' } })
234
+ await tx.post.create({ data: { title: 'Post', authorId: 1 } })
235
+ })
236
+ })
237
+ })
238
+ ```
239
+
240
+ ## Requirements
241
+
242
+ - Node.js >= 18.18.0
243
+ - Prisma >= 7.0.0
244
+ - PostgreSQL (for raw query parsing)
245
+
246
+ ## License
247
+
248
+ [Do No Harm](https://github.com/raisely/NoHarm)
@@ -0,0 +1,18 @@
1
+ import type { RowCycleInfo, RowLockingOptions } from '../types.js';
2
+ import type { RowLockGraphs } from '../graphs/row-graph.js';
3
+ /**
4
+ * Error thrown when inconsistent row locking order is detected.
5
+ */
6
+ export declare class RowLockingAssertionError extends Error {
7
+ readonly issues: RowCycleInfo[];
8
+ constructor(issues: RowCycleInfo[]);
9
+ }
10
+ /**
11
+ * Assert that row locking has been consistent within tables.
12
+ *
13
+ * @param rowGraphs The row lock graphs to check
14
+ * @param options Options controlling which tables to check and strictness mode
15
+ * @throws RowLockingAssertionError if violations are detected
16
+ */
17
+ export declare function assertConsistentRowLocking(rowGraphs: RowLockGraphs, options?: RowLockingOptions): void;
18
+ //# sourceMappingURL=row-assertion.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"row-assertion.d.ts","sourceRoot":"","sources":["../../src/assertions/row-assertion.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,iBAAiB,EAAc,MAAM,aAAa,CAAA;AAC9E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAA;AAG3D;;GAEG;AACH,qBAAa,wBAAyB,SAAQ,KAAK;IACjD,SAAgB,MAAM,EAAE,YAAY,EAAE,CAAA;gBAE1B,MAAM,EAAE,YAAY,EAAE;CA0BnC;AAED;;;;;;GAMG;AACH,wBAAgB,0BAA0B,CACxC,SAAS,EAAE,aAAa,EACxB,OAAO,GAAE,iBAAsB,GAC9B,IAAI,CAkBN"}
@@ -0,0 +1,53 @@
1
+ import { formatCaller } from '../utils/caller-extractor.js';
2
+ /**
3
+ * Error thrown when inconsistent row locking order is detected.
4
+ */
5
+ export class RowLockingAssertionError extends Error {
6
+ issues;
7
+ constructor(issues) {
8
+ const details = issues
9
+ .map((issue) => {
10
+ let msg = `Table "${issue.table}": `;
11
+ if (issue.message) {
12
+ msg += issue.message;
13
+ }
14
+ else if (issue.primaryKeys.length > 0) {
15
+ msg += `keys [${issue.primaryKeys.join(', ')}] form a cycle`;
16
+ }
17
+ if (issue.callers.length > 0) {
18
+ msg +=
19
+ '\n Callers:\n' +
20
+ issue.callers.map((c) => ` - ${formatCaller(c)}`).join('\n');
21
+ }
22
+ return msg;
23
+ })
24
+ .join('\n\n');
25
+ const message = `Inconsistent row locking order detected!\n\n${details}`;
26
+ super(message);
27
+ this.name = 'RowLockingAssertionError';
28
+ this.issues = issues;
29
+ }
30
+ }
31
+ /**
32
+ * Assert that row locking has been consistent within tables.
33
+ *
34
+ * @param rowGraphs The row lock graphs to check
35
+ * @param options Options controlling which tables to check and strictness mode
36
+ * @throws RowLockingAssertionError if violations are detected
37
+ */
38
+ export function assertConsistentRowLocking(rowGraphs, options = {}) {
39
+ const { tables, strict = false } = options;
40
+ // Determine which tables to check
41
+ const tablesToCheck = tables ?? rowGraphs.getAllTables();
42
+ const issues = [];
43
+ for (const table of tablesToCheck) {
44
+ const issue = rowGraphs.checkStrictOrdering(table, strict);
45
+ if (issue) {
46
+ issues.push(issue);
47
+ }
48
+ }
49
+ if (issues.length > 0) {
50
+ throw new RowLockingAssertionError(issues);
51
+ }
52
+ }
53
+ //# sourceMappingURL=row-assertion.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"row-assertion.js","sourceRoot":"","sources":["../../src/assertions/row-assertion.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,YAAY,EAAE,MAAM,8BAA8B,CAAA;AAE3D;;GAEG;AACH,MAAM,OAAO,wBAAyB,SAAQ,KAAK;IACjC,MAAM,CAAgB;IAEtC,YAAY,MAAsB;QAChC,MAAM,OAAO,GAAG,MAAM;aACnB,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE;YACb,IAAI,GAAG,GAAG,UAAU,KAAK,CAAC,KAAK,KAAK,CAAA;YACpC,IAAI,KAAK,CAAC,OAAO,EAAE,CAAC;gBAClB,GAAG,IAAI,KAAK,CAAC,OAAO,CAAA;YACtB,CAAC;iBAAM,IAAI,KAAK,CAAC,WAAW,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBACxC,GAAG,IAAI,SAAS,KAAK,CAAC,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,gBAAgB,CAAA;YAC9D,CAAC;YAED,IAAI,KAAK,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBAC7B,GAAG;oBACD,gBAAgB;wBAChB,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,SAAS,YAAY,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;YACnE,CAAC;YAED,OAAO,GAAG,CAAA;QACZ,CAAC,CAAC;aACD,IAAI,CAAC,MAAM,CAAC,CAAA;QAEf,MAAM,OAAO,GAAG,+CAA+C,OAAO,EAAE,CAAA;QAExE,KAAK,CAAC,OAAO,CAAC,CAAA;QACd,IAAI,CAAC,IAAI,GAAG,0BAA0B,CAAA;QACtC,IAAI,CAAC,MAAM,GAAG,MAAM,CAAA;IACtB,CAAC;CACF;AAED;;;;;;GAMG;AACH,MAAM,UAAU,0BAA0B,CACxC,SAAwB,EACxB,UAA6B,EAAE;IAE/B,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,KAAK,EAAE,GAAG,OAAO,CAAA;IAE1C,kCAAkC;IAClC,MAAM,aAAa,GAAG,MAAM,IAAI,SAAS,CAAC,YAAY,EAAE,CAAA;IAExD,MAAM,MAAM,GAAmB,EAAE,CAAA;IAEjC,KAAK,MAAM,KAAK,IAAI,aAAa,EAAE,CAAC;QAClC,MAAM,KAAK,GAAG,SAAS,CAAC,mBAAmB,CAAC,KAAK,EAAE,MAAM,CAAC,CAAA;QAC1D,IAAI,KAAK,EAAE,CAAC;YACV,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;QACpB,CAAC;IACH,CAAC;IAED,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACtB,MAAM,IAAI,wBAAwB,CAAC,MAAM,CAAC,CAAA;IAC5C,CAAC;AACH,CAAC"}
@@ -0,0 +1,17 @@
1
+ import type { TableCycleInfo } from '../types.js';
2
+ import type { TableLockGraph } from '../graphs/table-graph.js';
3
+ /**
4
+ * Error thrown when inconsistent table locking order is detected.
5
+ */
6
+ export declare class TableLockingAssertionError extends Error {
7
+ readonly cycleInfo: TableCycleInfo;
8
+ constructor(cycleInfo: TableCycleInfo);
9
+ }
10
+ /**
11
+ * Assert that table locking has been consistent (no cycles in the table lock graph).
12
+ *
13
+ * @param tableGraph The table lock graph to check
14
+ * @throws TableLockingAssertionError if a cycle is detected
15
+ */
16
+ export declare function assertConsistentTableLocking(tableGraph: TableLockGraph): void;
17
+ //# sourceMappingURL=table-assertion.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"table-assertion.d.ts","sourceRoot":"","sources":["../../src/assertions/table-assertion.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAc,MAAM,aAAa,CAAA;AAC7D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAA;AAG9D;;GAEG;AACH,qBAAa,0BAA2B,SAAQ,KAAK;IACnD,SAAgB,SAAS,EAAE,cAAc,CAAA;gBAE7B,SAAS,EAAE,cAAc;CAgBtC;AAED;;;;;GAKG;AACH,wBAAgB,4BAA4B,CAAC,UAAU,EAAE,cAAc,GAAG,IAAI,CAO7E"}
@@ -0,0 +1,33 @@
1
+ import { formatCaller } from '../utils/caller-extractor.js';
2
+ /**
3
+ * Error thrown when inconsistent table locking order is detected.
4
+ */
5
+ export class TableLockingAssertionError extends Error {
6
+ cycleInfo;
7
+ constructor(cycleInfo) {
8
+ const cycleStr = [...cycleInfo.tables, cycleInfo.tables[0]].join(' -> ');
9
+ const callerDetails = cycleInfo.callers
10
+ .map((c) => ` ${c.table}: ${formatCaller(c.caller)}`)
11
+ .join('\n');
12
+ const message = `Inconsistent table locking order detected!\n\n` +
13
+ `Cycle: ${cycleStr}\n\n` +
14
+ `Callers involved:\n${callerDetails}`;
15
+ super(message);
16
+ this.name = 'TableLockingAssertionError';
17
+ this.cycleInfo = cycleInfo;
18
+ }
19
+ }
20
+ /**
21
+ * Assert that table locking has been consistent (no cycles in the table lock graph).
22
+ *
23
+ * @param tableGraph The table lock graph to check
24
+ * @throws TableLockingAssertionError if a cycle is detected
25
+ */
26
+ export function assertConsistentTableLocking(tableGraph) {
27
+ const cycles = tableGraph.findCyclesWithCallers();
28
+ if (cycles.length > 0) {
29
+ // Report only the first cycle (as per requirements)
30
+ throw new TableLockingAssertionError(cycles[0]);
31
+ }
32
+ }
33
+ //# sourceMappingURL=table-assertion.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"table-assertion.js","sourceRoot":"","sources":["../../src/assertions/table-assertion.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,YAAY,EAAE,MAAM,8BAA8B,CAAA;AAE3D;;GAEG;AACH,MAAM,OAAO,0BAA2B,SAAQ,KAAK;IACnC,SAAS,CAAgB;IAEzC,YAAY,SAAyB;QACnC,MAAM,QAAQ,GAAG,CAAC,GAAG,SAAS,CAAC,MAAM,EAAE,SAAS,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAA;QAExE,MAAM,aAAa,GAAG,SAAS,CAAC,OAAO;aACpC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,CAAC,KAAK,KAAK,YAAY,CAAC,CAAC,CAAC,MAAM,CAAC,EAAE,CAAC;aACrD,IAAI,CAAC,IAAI,CAAC,CAAA;QAEb,MAAM,OAAO,GACX,gDAAgD;YAChD,UAAU,QAAQ,MAAM;YACxB,sBAAsB,aAAa,EAAE,CAAA;QAEvC,KAAK,CAAC,OAAO,CAAC,CAAA;QACd,IAAI,CAAC,IAAI,GAAG,4BAA4B,CAAA;QACxC,IAAI,CAAC,SAAS,GAAG,SAAS,CAAA;IAC5B,CAAC;CACF;AAED;;;;;GAKG;AACH,MAAM,UAAU,4BAA4B,CAAC,UAA0B;IACrE,MAAM,MAAM,GAAG,UAAU,CAAC,qBAAqB,EAAE,CAAA;IAEjD,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACtB,oDAAoD;QACpD,MAAM,IAAI,0BAA0B,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAA;IACjD,CAAC;AACH,CAAC"}
@@ -0,0 +1,60 @@
1
+ import { PrismaClient } from '@prisma/client';
2
+ import type { DeadlockDetectionConfig, RowLockingOptions } from './types.js';
3
+ import { TableLockingAssertionError } from './assertions/table-assertion.js';
4
+ import { RowLockingAssertionError } from './assertions/row-assertion.js';
5
+ /**
6
+ * Wrap a Prisma client with deadlock detection.
7
+ * Automatically tracks table and row locking order across all transactions.
8
+ *
9
+ * Usage:
10
+ * ```typescript
11
+ * const prisma = withDeadlockDetection(new PrismaClient())
12
+ * ```
13
+ *
14
+ * @param client The PrismaClient instance to wrap
15
+ * @param config Optional configuration
16
+ * @returns A wrapped client with automatic transaction tracking
17
+ */
18
+ export declare function withDeadlockDetection<T extends PrismaClient>(client: T, config?: DeadlockDetectionConfig): T;
19
+ /**
20
+ * Assert that table locking has been consistent across all tracked operations.
21
+ * Throws TableLockingAssertionError if a cycle is detected.
22
+ */
23
+ export declare function assertConsistentTableLocking(): void;
24
+ /**
25
+ * Assert that row locking has been consistent within tables.
26
+ * Throws RowLockingAssertionError if violations are detected.
27
+ *
28
+ * @param options Options controlling which tables to check and strictness mode
29
+ */
30
+ export declare function assertConsistentRowLocking(options?: RowLockingOptions): void;
31
+ /**
32
+ * Assert no deadlock risk exists by checking both table and row locking consistency.
33
+ * This is a convenience function that calls both assertions.
34
+ *
35
+ * @param rowOptions Options for the row locking assertion
36
+ */
37
+ export declare function assertNoDeadlockRisk(rowOptions?: RowLockingOptions): void;
38
+ /**
39
+ * Reset all deadlock detection state.
40
+ * Call this between test runs if needed.
41
+ */
42
+ export declare function resetDeadlockDetection(): void;
43
+ /**
44
+ * Wrapper function for tracking operations from prisma-lock-for-update.
45
+ * Use this to wrap forUpdate calls so they're tracked in the deadlock detection graph.
46
+ *
47
+ * @param model The model name (e.g., 'User', 'Post')
48
+ * @param fn The async function that performs the forUpdate operation
49
+ * @returns The result of the operation
50
+ *
51
+ * @example
52
+ * ```typescript
53
+ * const user = await trackForUpdate('User', () =>
54
+ * tx.user.findUniqueForUpdate({ where: { id: 1 } })
55
+ * )
56
+ * ```
57
+ */
58
+ export declare function trackForUpdate<T>(model: string, fn: () => Promise<T>): Promise<T>;
59
+ export { TableLockingAssertionError, RowLockingAssertionError };
60
+ //# sourceMappingURL=extension.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"extension.d.ts","sourceRoot":"","sources":["../src/extension.ts"],"names":[],"mappings":"AAAA,OAAO,EAAU,YAAY,EAAE,MAAM,gBAAgB,CAAA;AACrD,OAAO,KAAK,EAAE,uBAAuB,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAA;AAM5E,OAAO,EAEL,0BAA0B,EAC3B,MAAM,iCAAiC,CAAA;AACxC,OAAO,EAEL,wBAAwB,EACzB,MAAM,+BAA+B,CAAA;AAoQtC;;;;;;;;;;;;GAYG;AACH,wBAAgB,qBAAqB,CAAC,CAAC,SAAS,YAAY,EAC1D,MAAM,EAAE,CAAC,EACT,MAAM,CAAC,EAAE,uBAAuB,GAC/B,CAAC,CAyCH;AAED;;;GAGG;AACH,wBAAgB,4BAA4B,IAAI,IAAI,CAGnD;AAED;;;;;GAKG;AACH,wBAAgB,0BAA0B,CAAC,OAAO,CAAC,EAAE,iBAAiB,GAAG,IAAI,CAG5E;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,UAAU,CAAC,EAAE,iBAAiB,GAAG,IAAI,CAGzE;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,IAAI,IAAI,CAE7C;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,cAAc,CAAC,CAAC,EACpC,KAAK,EAAE,MAAM,EACb,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GACnB,OAAO,CAAC,CAAC,CAAC,CAeZ;AAGD,OAAO,EAAE,0BAA0B,EAAE,wBAAwB,EAAE,CAAA"}