{"id":20742436,"url":"https://github.com/matthewoden/pg_promise_sandbox","last_synced_at":"2026-02-24T02:02:17.620Z","repository":{"id":57322625,"uuid":"322985312","full_name":"matthewoden/pg_promise_sandbox","owner":"matthewoden","description":"Transaction based testing tools for Postgres. Run all your database tests (and CI Pipelines) concurrently!","archived":false,"fork":false,"pushed_at":"2020-12-21T20:58:51.000Z","size":24,"stargazers_count":6,"open_issues_count":0,"forks_count":2,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-12-02T14:58:38.302Z","etag":null,"topics":["postgres","testing"],"latest_commit_sha":null,"homepage":"","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/matthewoden.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2020-12-20T03:28:06.000Z","updated_at":"2023-03-19T06:41:54.000Z","dependencies_parsed_at":"2022-08-25T21:01:17.226Z","dependency_job_id":null,"html_url":"https://github.com/matthewoden/pg_promise_sandbox","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/matthewoden/pg_promise_sandbox","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/matthewoden%2Fpg_promise_sandbox","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/matthewoden%2Fpg_promise_sandbox/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/matthewoden%2Fpg_promise_sandbox/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/matthewoden%2Fpg_promise_sandbox/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/matthewoden","download_url":"https://codeload.github.com/matthewoden/pg_promise_sandbox/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/matthewoden%2Fpg_promise_sandbox/sbom","scorecard":{"id":626786,"data":{"date":"2025-08-11","repo":{"name":"github.com/matthewoden/pg_promise_sandbox","commit":"a507f2dce6913319209fd156cb928131a95b53a1"},"scorecard":{"version":"v5.2.1-40-gf6ed084d","commit":"f6ed084d17c9236477efd66e5b258b9d4cc7b389"},"score":1.3,"checks":[{"name":"Pinned-Dependencies","score":-1,"reason":"no dependencies found","details":null,"documentation":{"short":"Determines if the project has declared and pinned the dependencies of its build process.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#pinned-dependencies"}},{"name":"Packaging","score":-1,"reason":"packaging workflow not detected","details":["Warn: no GitHub/GitLab publishing workflow detected."],"documentation":{"short":"Determines if the project is published as a package that others can easily download, install, easily update, and uninstall.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#packaging"}},{"name":"Maintained","score":0,"reason":"0 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 0","details":null,"documentation":{"short":"Determines if the project is \"actively maintained\".","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#maintained"}},{"name":"Binary-Artifacts","score":10,"reason":"no binaries found in the repo","details":null,"documentation":{"short":"Determines if the project has generated executable (binary) artifacts in the source repository.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#binary-artifacts"}},{"name":"Dangerous-Workflow","score":-1,"reason":"no workflows found","details":null,"documentation":{"short":"Determines if the project's GitHub Action workflows avoid dangerous patterns.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#dangerous-workflow"}},{"name":"SAST","score":0,"reason":"no SAST tool detected","details":["Warn: no pull requests merged into dev branch"],"documentation":{"short":"Determines if the project uses static code analysis.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#sast"}},{"name":"Code-Review","score":0,"reason":"Found 0/4 approved changesets -- score normalized to 0","details":null,"documentation":{"short":"Determines if the project requires human code review before pull requests (aka merge requests) are merged.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#code-review"}},{"name":"Token-Permissions","score":-1,"reason":"No tokens found","details":null,"documentation":{"short":"Determines if the project's workflows follow the principle of least privilege.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#token-permissions"}},{"name":"CII-Best-Practices","score":0,"reason":"no effort to earn an OpenSSF best practices badge detected","details":null,"documentation":{"short":"Determines if the project has an OpenSSF (formerly CII) Best Practices Badge.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#cii-best-practices"}},{"name":"Security-Policy","score":0,"reason":"security policy file not detected","details":["Warn: no security policy file detected","Warn: no security file to analyze","Warn: no security file to analyze","Warn: no security file to analyze"],"documentation":{"short":"Determines if the project has published a security policy.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#security-policy"}},{"name":"Fuzzing","score":0,"reason":"project is not fuzzed","details":["Warn: no fuzzer integrations found"],"documentation":{"short":"Determines if the project uses fuzzing.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#fuzzing"}},{"name":"License","score":0,"reason":"license file not detected","details":["Warn: project does not have a license file"],"documentation":{"short":"Determines if the project has defined a license.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#license"}},{"name":"Signed-Releases","score":-1,"reason":"no releases found","details":null,"documentation":{"short":"Determines if the project cryptographically signs release artifacts.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#signed-releases"}},{"name":"Branch-Protection","score":0,"reason":"branch protection not enabled on development/release branches","details":["Warn: branch protection not enabled for branch 'main'"],"documentation":{"short":"Determines if the default and release branches are protected with GitHub's branch protection settings.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#branch-protection"}},{"name":"Vulnerabilities","score":0,"reason":"12 existing vulnerabilities detected","details":["Warn: Project is vulnerable to: GHSA-93q8-gq69-wqmw","Warn: Project is vulnerable to: GHSA-v6h2-p8h4-qcjw","Warn: Project is vulnerable to: GHSA-grv7-fg5c-xmjg","Warn: Project is vulnerable to: GHSA-gxpj-cx7g-858c","Warn: Project is vulnerable to: GHSA-4q6p-r6v2-jvc5","Warn: Project is vulnerable to: GHSA-ww39-953v-wcq6","Warn: Project is vulnerable to: GHSA-f8q6-p94x-37v3","Warn: Project is vulnerable to: GHSA-qrpm-p2h7-hrv2","Warn: Project is vulnerable to: GHSA-mwcw-c2x4-8c55","Warn: Project is vulnerable to: GHSA-9wv6-86v2-598j","Warn: Project is vulnerable to: GHSA-g6ww-v8xp-vmwg","Warn: Project is vulnerable to: GHSA-ff9h-848c-4xfj"],"documentation":{"short":"Determines if the project has open, known unfixed vulnerabilities.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#vulnerabilities"}}]},"last_synced_at":"2025-08-21T06:42:59.196Z","repository_id":57322625,"created_at":"2025-08-21T06:42:59.196Z","updated_at":"2025-08-21T06:42:59.196Z"},"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29768333,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-24T01:40:24.820Z","status":"online","status_checked_at":"2026-02-24T02:00:07.497Z","response_time":75,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["postgres","testing"],"created_at":"2024-11-17T07:05:43.626Z","updated_at":"2026-02-24T02:02:17.584Z","avatar_url":"https://github.com/matthewoden.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# PG Promise Sandbox\n\nAn experiment with [pg-promise](https://github.com/vitaly-t/pg-promise) to turn database tests into an \"embarrassingly parallel problem\". I've extended the pg-promise API with a couple functions that move all queries per \"test\" into a transaction.\n\n## Goals\n\nBy running each integration tests to run in a transaction, we can enable a faster feedback loop: Each test can be run concurrently with any other test. No data would ever be committed to the test database.\n\nIn turn, CI Pipelines could run also run concurrently. When a PR triggers an automatic test suite, it wouldn't need to wait for any other PR to finish. (Deployments would still need to be serial operations, of course.)\n\nYou can move as fast as your test-environment instance will allow.\n\n## Installation\n\n`pg-promise@10.x` is a peer dependency.\n\n```shell\nnpm i pg-promise pg-promise-sandbox --save\n```\n\n## Usage\n\nIf you're already using `pg-promise`, then this library shouldn't change the way your app runs at all (see below for a few exceptions). I've used a proxy to copy every single property of pg-promise, allowing this to be a drop-in addition to most codebases.\n\nTwo new functions are added to pg-promise's, and are needed to enable sandbox-mode:\n\n- `createSandbox (Promise: void)` - creates a transaction, and begins all\n  subsequent promises to the task\n- `closeSandbox (Promise: void)` - closes the transaction.\n\n### Wrapping PG Promise\n\nSimply pass in your existing pg-promise instance, and whether or not sandbox mode should be enabled.\n\n```js\nconst pgpromise = require(\"pg-promise\")();\nconst { pgSandbox } = require(\"pg-promise-sandbox\");\n\nconst pg = pgpromise({\n  username: \"postgres\",\n  password: \"postgres\",\n  port: 5432,\n  database: \"concurrent\",\n  ssl: false,\n});\n\nconst mode = process.env.NODE_ENV === \"test\" ? \"sandbox\" : \"normal\";\n\nmodule.exports = pgSandbox({ pg, mode });\n```\n\n### Testing Example\n\nThe following example could be used to run mocha tests with --parallel, or by spinning up multiple mocha instances (and multiple cores) via `find ./test --name='*Spec.js | xargs -P 4 mocha`\n\n\u003e Note: Slam your test-database responsibly.\n\n```js\nconst db = require('./my/db/client')\nconst User = require('./my/user/module')\n\ndescribe('User registration', () =\u003e {\n\n  before(async () =\u003e {\n    // check out a connection from the pool, begin a transaction.\n    // all new queries are in a nested transaction.\n    await db.createSandbox()\n  })\n\n  after(async () =\u003e {\n    // rollback transaction, release connection.\n    // nothing is actually committed to the database.\n    await db.closeSandbox()\n  })\n\n  it('saves a user', () =\u003e {\n    // can be run concurrently with any other test\n    const user = await User.create({name: \"Bob McBoberson III\"})\n    user.id.should.eql(\"bob-mcboberson-iii\")\n  })\n})\n```\n\nThis could be even simpler. If you use a mocha setup file, you could set these functions globally, and omit the beforeEach entirely.\n\n```js\nconst db = require(\"./my/db/client\");\n\nglobal.beforeEach(() =\u003e db.createSandbox());\nglobal.afterEach(() =\u003e db.restoreSandbox());\n```\n\n## When to use this library\n\nUse this when your application:\n\n- has a LOT of postgres integration tests\n- has logic that is truly stateless, or can be made stateless.\n- doesn't care about whether a query is in a transaction or not\n\nAs a general rule - when you can't use a sandbox, move the test to a new file,\nand test serial operations seperately from everything else.\n\n### Why test counts matter.\n\nIf you only have a small number of tests, or very simple tests, the overhead for starting a parallel CI process might take as much time as running your tests serially.\n\n### Why stateless?\n\nIf your app hits multiple databases, like elasticsearch or redis, these will be shared across\ntests, and may interfere with concurrent testing.\n\nIf you have an in-memory cache, you can probably get away with concurrent testing - possibly by creating a new cache per test.\n\nIt's left to the reader to consider how you might refactor, or mock stateful functionality in your tests. But always prefer confidence in your tests, over speed.\n\n### Why not care about transactions?\n\nThere are two pg-promise methods that the wrapped API doesn't support well: `.txIf` and `.taskIf`.\n\nBy default, these methods use a transaction, depending on whether or not the call is currently in a transaction. Since this library wraps everything in a transaction by default, those queries obviously won't function as expected.\n\n## Background\n\nNode’s claim to fame is that it’s single-threaded concurrency model is super lightweight and perfect for network and IO heavy tasks. Yet the current state of database integration tests on node are this serial process of setup, assert, then teardown. We’re barely taking advantage of our runtime in our test environment.\n\nIn our CI pipelines, we have to wait on other jobs to finish before the next job can start.\n\nOther, threaded languages have a workaround for this. When testing, they use nested transactions. Each test checks out a connection from the pool, spins up a transaction, and assigns that connection ownership to the test thread. All queries that originate from that thread use that transaction. When the test is done, they roll back the parent transaction. No data is committed. No tests share memory, and your tests can run as fast as your CI agent allows.\n\nWhile we can’t do that with Node, we can get pretty close with the `async_hooks` module. It provides just enough functionality to create an async stack trace, and let us determine if a process is running in the scope, or “promise chain” of another promise. When we create a sandbox, we store a reference to the async scope, alongside a reference to a transaction.\n\nAnother requirement is one of syntax - postgres only sort-of supports nested transactions. After the top level transaction, postgres moves to a named savepoint syntax instead of additional transactions. So any transaction code would need SQL to be changed dynamically based on whether we're in a transaction yet or not.\n\nThe `pg-promise` library was chosen specificially because it handles this problem really well: the API allows developers to compose queries without caring if it’s in a transaction or not (which is why the `txIf` and `taskIf` api exists - because sometimes you have to care). By using a [Proxy](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy), we can check each database function call, look up which sandbox the current function belongs to, and inject the current query into the appropriate transaction.\n\n## Roadmap\n\n- Expand testing strategy without deeply relying on pg-promise private properties.\n- determine strategy for txIf and taskIf to allow a single level of nesting.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmatthewoden%2Fpg_promise_sandbox","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmatthewoden%2Fpg_promise_sandbox","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmatthewoden%2Fpg_promise_sandbox/lists"}