-
Notifications
You must be signed in to change notification settings - Fork 626
Postgres #5365
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Postgres #5365
Changes from 26 commits
Commits
Show all changes
62 commits
Select commit
Hold shift + click to select a range
de730f5
postgres experimental WIP
boutell dccb36d
astonishingly, all mocha tests of apostrophe pass with this
boutell 266841c
mocha tests pass, actual sites work
boutell 9d67565
lint clean
boutell 04d4edd
listDatabases support, but changes are coming
boutell bc7b72b
wip
boutell 9ba57fc
dump and restore updates
boutell 5ad412a
backpressure, adequate handling of ObjectId for our needs (becomes it…
boutell db1434b
mild performance optimization
boutell a083c25
profiling
boutell ddf80df
testing issue resolved
boutell 3c7a23b
refactored to db-connect module, introduced sqlite adapter
boutell 8b095f1
sqlite WIP
boutell 80dd7ab
debugging
d08f502
programmatic API for dump/restore/copy dbs
374b5d8
linting, documentation
boutell 6a5b931
MIT license
boutell fea05a7
text ranking is more accurate, documentation is more complete
boutell b67e2cd
good full text search for sqlite
boutell 54dc9b5
updates for compatibility with the rest of the public and private mod…
boutell f0f45f9
requirements found by testing private modules
boutell f4ae5d8
Merge branch 'main' into postgres
boutell 9c9105d
fixes from full cypress run
boutell ab875ef
eslint passing
boutell c95c9b3
restore permissions
boutell 88e8e48
maximize atomicity
boutell 2eadf8e
bug fixes
boutell 9494c00
* exit properly when asset tests fail
boutell d3090df
ignore claude-tools in eslint
boutell 5b3c675
postgres and sqlite-inclusive ci matrix attempt
boutell fe2cc10
clean up logs
boutell 50897fd
We hit github's limit on total configurations because every package g…
boutell 3c29e2a
hardened the asset tests, made them less timing sensitive, fixed a ba…
boutell 1370762
fix a root cause of asset test instability
boutell 4b1716e
log mess
boutell 2fc2224
implemented missing $size operator
boutell c260229
test compatibility
boutell 7408e4b
advanced permission uses regex in $in
boutell f0bfb0e
regex in $in
boutell 1f1e6b3
.db() should not make false promises in plain postgres mode, it shoul…
boutell c198a01
ability to specify a default adapter
boutell 14dffe3
obsolete file
boutell 7e60e35
put escapeHost back where it belongs
boutell bef7978
dead code removal, test cleanup
boutell 8cb3c5a
emulate-mongo-3-driver only needed in db-connect
boutell ec9ec17
no claude logs in repo (tools are welcome)
boutell 0283d85
* shared aggregation implementation, other shared things
boutell 3a8901d
vanilla postgres should not attempt to use .db() with alternate names…
boutell 061136a
documentation corrections
boutell 9c91f9f
documentation errors
boutell 4e11aad
listDatabases and documentation corrections
boutell 615399f
Merge branch 'main' into postgres
boutell f492eb4
more edge cases revealed by latest work from Miro
boutell c7afe8d
anchored prefix regexps are optimized
boutell 7aa5fd1
* matchesQuery in the aggregation cursor implementation doesn't thr…
boutell cfdd3c6
do not swallow dump/restore errors on indexes
boutell a9338c4
cover how to run the utilities
boutell c863da6
fix detection of source
boutell 6971100
separate sanitization for index names
boutell 3885742
Merge branch 'main' into postgres
boutell 8bc9d3f
regex prefix safety
boutell 9af8358
pnpm
boutell File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -18,6 +18,7 @@ | |
| }, | ||
| "pnpm": { | ||
| "onlyBuiltDependencies": [ | ||
| "better-sqlite3", | ||
| "sharp", | ||
| "vue-demi", | ||
| "@parcel/watcher", | ||
|
|
||
160 changes: 160 additions & 0 deletions
160
packages/apostrophe/modules/@apostrophecms/db/CLAUDE.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,160 @@ | ||
| # @apostrophecms/db - Universal Database Adapter | ||
|
|
||
| ## Project Overview | ||
|
|
||
| A universal database adapter for ApostropheCMS that provides a MongoDB-compatible interface with pluggable backends. Currently supports MongoDB (passthrough) and PostgreSQL (full implementation). | ||
|
|
||
| **Design Philosophy:** | ||
| - Expose only operations actually used by ApostropheCMS (not the full MongoDB API) | ||
| - Make backends easy to write by keeping the interface minimal | ||
| - Bias toward escaping inputs rather than rejecting them (except where no safe representation exists) | ||
| - All `createIndex` calls happen at startup, so no runtime schema introspection needed | ||
|
|
||
| ## File Structure | ||
|
|
||
| ``` | ||
| db/ | ||
| ├── index.js # Main entry point, exports { mongodb, postgres } | ||
| ├── adapters/ | ||
| │ ├── mongodb.js # Thin wrapper around native MongoDB driver | ||
| │ └── postgres.js # Full PostgreSQL implementation (~1500 lines) | ||
| ├── test/ | ||
| │ ├── adapter.test.js # Comprehensive test suite (106 MongoDB / 125 PostgreSQL tests) | ||
| │ └── security.test.js # SQL injection prevention tests | ||
| ├── package.json | ||
| └── CLAUDE.md | ||
| ``` | ||
|
|
||
| ## Connection API | ||
|
|
||
| Both adapters use URI as the first argument: | ||
|
|
||
| ```javascript | ||
| const { mongodb, postgres } = require('@apostrophecms/db'); | ||
|
|
||
| // MongoDB | ||
| const client = await mongodb.connect('mongodb://localhost:27017/mydb'); | ||
|
|
||
| // PostgreSQL | ||
| const client = await postgres.connect('postgres://user:pass@localhost:5432/mydb'); | ||
|
|
||
| // Get database reference (uses URI's database if no argument) | ||
| const db = client.db(); | ||
|
|
||
| // Or switch databases | ||
| const otherDb = client.db('other-database'); | ||
|
|
||
| await client.close(); | ||
| ``` | ||
|
|
||
| Each adapter exports `protocols` array for URI scheme matching: | ||
| - MongoDB: `['mongodb', 'mongodb+srv']` | ||
| - PostgreSQL: `['postgres', 'postgresql']` | ||
|
|
||
| ## Supported Operations | ||
|
|
||
| ### Collection Methods | ||
| - `insertOne`, `insertMany` | ||
| - `find` (returns cursor), `findOne` | ||
| - `updateOne`, `updateMany`, `replaceOne` | ||
| - `deleteOne`, `deleteMany` | ||
| - `countDocuments`, `distinct` | ||
| - `aggregate` (with `$match`, `$sort`, `$limit`, `$skip`, `$project`, `$unwind`) | ||
| - `bulkWrite` | ||
| - `findOneAndUpdate` | ||
| - `createIndex`, `dropIndex`, `indexes` | ||
| - `drop`, `rename` | ||
|
|
||
| ### Query Operators | ||
| `$eq`, `$ne`, `$gt`, `$gte`, `$lt`, `$lte`, `$in`, `$nin`, `$and`, `$or`, `$not`, `$exists`, `$regex`, `$all` | ||
|
|
||
| ### Update Operators | ||
| `$set`, `$unset`, `$inc`, `$push`, `$pull`, `$addToSet`, `$currentDate` | ||
|
|
||
| ### Cursor Methods | ||
| `sort`, `limit`, `skip`, `project`, `toArray`, `count`, `clone` | ||
|
|
||
| ## PostgreSQL Implementation Details | ||
|
|
||
| ### Storage Model | ||
| - Documents stored as JSONB in `data` column | ||
| - `_id` extracted to TEXT primary key column | ||
| - Table names: `{dbname}_{collectionname}` (hyphens converted to underscores) | ||
|
|
||
| ### Date Handling | ||
| Dates are serialized as `{ $date: "ISO8601 string" }` wrapper objects because: | ||
| 1. JSON.stringify calls `toJSON()` on Dates before any replacer sees them | ||
| 2. Need to distinguish dates from strings for proper deserialization | ||
| 3. ISO 8601 strings sort correctly as text (important for indexes) | ||
|
|
||
| ### Security Approach | ||
| - **Table/index names**: Validated against `^[a-zA-Z_][a-zA-Z0-9_]*$` (PostgreSQL limitation) | ||
| - **Field names in JSONB**: Escaped with `escapeString()` (single quotes doubled) | ||
| - **Values**: Always parameterized (`$1`, `$2`, etc.) | ||
| - **LIMIT/OFFSET**: Validated as non-negative integers | ||
|
|
||
| ### Query Building | ||
| `buildWhereClause` and `buildOperatorClause` **mutate** the `params` array by pushing values. The returned SQL contains positional placeholders referencing array indices. This is documented in the code. | ||
|
|
||
| ### Typed Indexes for Range Queries | ||
|
|
||
| PostgreSQL requires explicit typing for numeric indexes. The `type` option enables this: | ||
|
|
||
| ```javascript | ||
| // Text index (default) - for $eq, $in, $regex | ||
| await collection.createIndex({ slug: 1 }); | ||
|
|
||
| // Numeric index - for $gt/$lt on numbers | ||
| await collection.createIndex({ price: 1 }, { type: 'number' }); | ||
| // Creates: ((data->>'price')::numeric) | ||
|
|
||
| // Date index - for $gt/$lt on dates | ||
| await collection.createIndex({ createdAt: 1 }, { type: 'date' }); | ||
| // Creates: (data->'createdAt'->>'$date') - text, since ISO sorts correctly | ||
| ``` | ||
|
|
||
| Without `type`, range queries won't use the index (they still work, just slower). | ||
|
|
||
| ### Sparse Indexes | ||
| Implemented via PostgreSQL partial indexes: | ||
| ```javascript | ||
| await collection.createIndex({ field: 1 }, { sparse: true }); | ||
| // Creates: ... WHERE data->'field' IS NOT NULL | ||
| ``` | ||
|
|
||
| ## Testing | ||
|
|
||
| ```bash | ||
| # Run all tests with MongoDB | ||
| npm run test:mongodb | ||
|
|
||
| # Run all tests with PostgreSQL | ||
| npm run test:postgres | ||
|
|
||
| # Run both | ||
| npm test | ||
| ``` | ||
|
|
||
| **Test Prerequisites:** | ||
| - MongoDB running on localhost:27017 | ||
| - PostgreSQL running on localhost:5432 with database `dbtest_adapter` | ||
| - Set `PGUSER`/`PGPASSWORD` env vars if needed | ||
|
|
||
| ## Known Limitations / Future Work | ||
|
|
||
| 1. **Aggregation**: Only basic stages implemented (`$match`, `$sort`, `$limit`, `$skip`, `$project`, `$unwind`) | ||
| 2. **Text search**: Basic GIN index support, not full MongoDB text search semantics | ||
| 3. **Transactions**: No (not an ApostropheCMS requirement) | ||
| 4. **Change streams**: No (not an ApostropheCMS requirement) | ||
|
|
||
| ## Common Gotchas | ||
|
|
||
| 1. **Date comparisons**: Dates are stored as `{$date: "..."}` wrapper, so raw JSONB queries won't work - use the adapter's query interface | ||
|
|
||
| 2. **Nested field paths**: Use dot notation (`user.profile.name`) - works in queries, updates, indexes, and projections | ||
|
|
||
| 3. **Index type matters**: If you're doing `{ price: { $gt: 100 } }` queries, you need `{ type: 'number' }` on the index for PostgreSQL to use it | ||
|
|
||
| 4. **Database switching**: In PostgreSQL, different "databases" are actually table prefixes in the same PostgreSQL database. This is intentional for simpler connection management. | ||
|
|
||
| 5. **Collection name validation**: Hyphens are converted to underscores internally. Names with special characters beyond that will be rejected. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,7 +5,7 @@ | |
| "main": "index.js", | ||
| "scripts": { | ||
| "pretest": "npm run lint", | ||
| "test": "npm run test:base && npm run test:missing && npm run test:assets && npm run test:esm", | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. asset tests are now a good citizen, don't need to be broken out separately anymore |
||
| "test": "npm run test:base && npm run test:missing && npm run test:assets && npm run test:esm && npm run test:db", | ||
| "test:base": "nyc mocha -t 10000 --ignore=test/assets.js", | ||
| "test:missing": "nyc mocha -t 10000 test/add-missing-schema-fields-project/test.js", | ||
| "test:assets": "nyc mocha -t 10000 test/assets.js", | ||
|
|
@@ -15,7 +15,10 @@ | |
| "i18n": "node scripts/lint-i18n", | ||
| "stylelint": "stylelint modules/**/*.{scss,vue}", | ||
| "lint": "npm run eslint && npm run i18n && npm run stylelint", | ||
| "mocha": "mocha" | ||
| "mocha": "mocha", | ||
| "test:db": "cd ../db-connect && npm test", | ||
| "test:db:mongodb": "cd ../db-connect && ADAPTER=mongodb npx mocha test/**/*.test.js --timeout 30000", | ||
| "test:db:postgres": "cd ../db-connect && ADAPTER=postgres npx mocha test/**/*.test.js --timeout 30000" | ||
| }, | ||
| "repository": { | ||
| "type": "git", | ||
|
|
@@ -38,6 +41,7 @@ | |
| "author": "Apostrophe Technologies, Inc.", | ||
| "license": "MIT", | ||
| "dependencies": { | ||
| "@apostrophecms/db-connect": "workspace:^", | ||
| "@apostrophecms/emulate-mongo-3-driver": "workspace:^", | ||
| "@apostrophecms/vue-material-design-icons": "^1.0.0", | ||
| "@ctrl/tinycolor": "^4.1.0", | ||
|
|
@@ -141,6 +145,7 @@ | |
| "xregexp": "^2.0.0" | ||
| }, | ||
| "devDependencies": { | ||
| "chai": "^4.3.10", | ||
| "eslint": "^9.39.1", | ||
| "eslint-config-apostrophe": "workspace:^", | ||
| "form-data": "^4.0.4", | ||
|
|
||
Empty file.
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unrelated fix for clean shutdown