Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
*.php text eol=lf
*.phpt text eol=lf
/.gitattributes export-ignore
/.github/ export-ignore
/.gitignore export-ignore
/composer.lock export-ignore
/docs/ export-ignore
/phpbench.json export-ignore
/phpcs.xml.dist export-ignore
/phpstan.neon export-ignore
/phpunit.xml.dist export-ignore
/tests/ export-ignore
/tools/ export-ignore
48 changes: 48 additions & 0 deletions .github/workflows/docs-check.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# https://help.github.com/en/categories/automating-your-workflow-with-github-actions

name: "Check Docs"

on:
pull_request:
push:
branches:
- "[0-9]+.[0-9]+.x"

jobs:
checkdocs:
name: "Check Docs"

runs-on: ${{ matrix.operating-system }}

strategy:
matrix:
dependencies:
- "locked"
php-version:
- "8.4"
operating-system:
- "ubuntu-latest"

steps:
- name: "Checkout"
uses: actions/checkout@v6

- name: "Install PHP"
uses: "shivammathur/setup-php@2.37.2"
with:
coverage: none
php-version: "${{ matrix.php-version }}"
ini-values: memory_limit=-1, opcache.enable_cli=1

- uses: ramsey/composer-install@4.0.0
with:
dependency-versions: ${{ matrix.dependencies }}

- name: "extract php code"
run: "bin/docs-extract-php-code"

- name: "lint php"
run: "php -l docs_php/*.php"

- name: "docs code style"
run: "vendor/bin/phpcbf docs_php --exclude=SlevomatCodingStandard.TypeHints.DeclareStrictTypes,SlevomatCodingStandard.ControlStructures.EarlyExit"
22 changes: 22 additions & 0 deletions .github/workflows/docs-deploy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
name: Publish docs

on:
push:
branches:
- "[0-9]+.[0-9]+.x"
release:
types:
- published

jobs:
trigger:
runs-on: ubuntu-latest
steps:
- name: Trigger workflow in other repo
run: |
curl -L -X POST \
-H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer ${{ secrets.ORGANIZATION_ADMIN_TOKEN }}" \
-H "X-GitHub-Api-Version: 2026-03-10" \
https://api.github.com/repos/patchlevel/patchlevel.dev/actions/workflows/prod-deployment.yaml/dispatches \
-d '{"ref":"main"}'
22 changes: 22 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,25 @@ benchmark-diff-test: vendor

.PHONY: dev
dev: static test ## run dev tools

.PHONY: docs
docs: docs-extract-php docs-php-lint docs-phpcs docs-inject-php

.PHONY: docs-extract-php
docs-extract-php:
bin/docs-extract-php-code

.PHONY: docs-inject-php
docs-inject-php:
bin/docs-inject-php-code

.PHONY: docs-format ## format docs
docs-format: docs-phpcs docs-inject-php

.PHONY: docs-php-lint ## lint docs code
docs-php-lint: docs-extract-php
php -l docs_php/*.php | grep 'Parse error: ' || true

.PHONY: docs-phpcs
docs-phpcs: docs-extract-php
vendor/bin/phpcbf docs_php --exclude=SlevomatCodingStandard.TypeHints.DeclareStrictTypes,SlevomatCodingStandard.ControlStructures.EarlyExit || true
141 changes: 30 additions & 111 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,132 +1,51 @@
[![Mutation testing badge](https://img.shields.io/endpoint?style=flat&url=https%3A%2F%2Fbadge-api.stryker-mutator.io%2Fgithub.com%2Fpatchlevel%2Frango%2F1.1.x)](https://dashboard.stryker-mutator.io/reports/github.com/patchlevel/rango/1.1.x)
[![Latest Stable Version](https://poser.pugx.org/patchlevel/rango/v)](https://packagist.org/packages/patchlevel/rango)
[![License](https://poser.pugx.org/patchlevel/rango/license)](https://packagist.org/packages/patchlevel/rango)

# Rango

<p align="center">
<img src="logo.png" width="50%">
</p>

Rango is a high-performance PHP library that reimplements the **MongoDB PHP API** on top of **PostgreSQL** using the power of `JSONB`.
Rango is a high-performance PHP library that reimplements the **MongoDB PHP API** on top of **PostgreSQL** using the
power of `JSONB`.

It provides a drop-in compatible API, allowing you to use familiar MongoDB-style operations while storing your data in a reliable PostgreSQL database. This is ideal for applications that want to leverage PostgreSQL's ACID compliance and ecosystem without giving up the flexible document-based development experience of MongoDB.
It provides a drop-in compatible API, allowing you to use familiar MongoDB-style operations while storing your data in a
reliable PostgreSQL database. This is ideal for applications that want to leverage PostgreSQL's ACID compliance and
ecosystem without giving up the flexible document-based development experience of MongoDB.

## 🚀 Why Rango?
## Features

- **PostgreSQL Reliability**: Benefit from PostgreSQL's mature engine, backups, and ACID transactions.
- **JSONB Performance**: Rango leverages PostgreSQL's binary JSON format (`JSONB`) for efficient storage and indexing.
- **Seamless Migration**: Switch between PostgreSQL and MongoDB with minimal code changes.
- **Hybrid Power**: Mix relational and document data within the same database.
* [Drop-in MongoDB API](https://patchlevel.dev/docs/rango/latest/getting-started) with `Client`, `Database`, and `Collection`
* [CRUD operations](https://patchlevel.dev/docs/rango/latest/crud-operations) like `insertOne`, `find`, `updateMany`, and `deleteOne`
* [Rich query operators](https://patchlevel.dev/docs/rango/latest/querying) such as `$gt`, `$in`, `$or`, and `$elemMatch`
* [Update operators](https://patchlevel.dev/docs/rango/latest/update-operators) like `$set`, `$inc`, `$push`, and `$rename`
* [Projection and sorting](https://patchlevel.dev/docs/rango/latest/querying#projection) with dot-notation support
* [Aggregation pipelines](https://patchlevel.dev/docs/rango/latest/aggregation) with `$match`, `$group`, `$unwind`, and `$lookup`
* [Bulk writes](https://patchlevel.dev/docs/rango/latest/crud-operations#bulk-writes) wrapped in a single transaction
* [Index management](https://patchlevel.dev/docs/rango/latest/indexes) backed by native PostgreSQL indexes

## 📦 Installation
## Installation

```bash
composer require patchlevel/rango
```

## 🛠 How it Works

Rango translates MongoDB queries into optimized PostgreSQL SQL statements.

- **Databases** are mapped to **PostgreSQL Schemas**.
- **Collections** are mapped to **PostgreSQL Tables** with a single `data` column of type `JSONB`.
- **Indexes** are created as **GIN or B-tree indexes** on the JSONB field.
- **Queries** are translated using PostgreSQL's rich set of JSONB operators (like `@>`, `?`, `->>`).

## 🚦 Quick Start

```php
use Patchlevel\Rango\Client;

// Connect to PostgreSQL using a standard PDO DSN
$client = new Client('pgsql:host=localhost;port=5432;dbname=app;user=postgres;password=postgres');

// Select database and collection (auto-created on first write)
$collection = $client->selectDatabase('test')->selectCollection('users');

// Insert a document (automatically generates an _id if missing)
$collection->insertOne([
'name' => 'John Doe',
'email' => 'john@example.com',
'tags' => ['php', 'postgres'],
'metadata' => ['logins' => 0]
]);

// Find documents using MongoDB syntax
$users = $collection->find([
'tags' => 'php',
'metadata.logins' => ['$lt' => 10]
]);

foreach ($users as $user) {
echo "Found: " . $user['name'] . " (" . $user['_id'] . ")\n";
}

// Atomic updates
$collection->updateOne(
['email' => 'john@example.com'],
['$inc' => ['metadata.logins' => 1]]
);
```
## Documentation

## ✨ Supported Features
* Latest [Docs](https://patchlevel.dev/docs/rango/latest)
* Related [Blog](https://patchlevel.dev/blog)

### CRUD Operations
| Category | Supported Methods |
| --- | --- |
| **Create** | `insertOne`, `insertMany` |
| **Read** | `find`, `findOne`, `countDocuments`, `distinct` |
| **Update** | `updateOne`, `updateMany`, `replaceOne`, `bulkWrite` |
| **Delete** | `deleteOne`, `deleteMany` |
| **Atomic** | `findOneAndUpdate`, `findOneAndReplace`, `findOneAndDelete` |
## Integration

### Query Operators
| Category | Operators |
| --- | --- |
| **Comparison** | `$eq`, `$ne`, `$gt`, `$gte`, `$lt`, `$lte`, `$in`, `$nin` |
| **Logical** | `$and`, `$or`, `$nor`, `$not` |
| **Element** | `$exists`, `$type` |
| **Evaluation** | `$regex`, `$mod` |
| **Array** | `$all`, `$size`, `$elemMatch` |
* [odm](https://github.com/patchlevel/odm)
* [event-sourcing](https://github.com/patchlevel/event-sourcing)
* [hydrator](https://github.com/patchlevel/hydrator)

Projection supports dot-notation in both inclusion and exclusion (e.g. `['profile.stats.score' => 0]`).
## Contributing

### Aggregation Framework
| Feature | Details |
| --- | --- |
| **Stages** | `$match`, `$sort`, `$limit`, `$skip`, `$project`, `$unwind`, `$group`, `$lookup` (Join) |
| **Accumulators** | `$sum`, `$avg`, `$min`, `$max`, `$first`, `$last` |

### Update Operators
| Category | Operators |
| --- | --- |
| **Field** | `$set`, `$setOnInsert`, `$inc`, `$mul`, `$unset`, `$rename`, `$min`, `$max`, `$currentDate` (incl. dot-notation) |
| **Array** | `$push` (inc. `$each`), `$pull`, `$addToSet`, `$pop` |
| **Bitwise** | `$bit` (`and`, `or`, `xor`) |

### Management
- **Index Management**: `createIndex`, `dropIndex`, `listIndexes`.
- **Schema Management**: `listDatabases`, `listCollections`, `renameCollection`, `drop`.

## ⚠️ Current Limitations

While Rango covers the most common use cases, some MongoDB features are not yet implemented:
- **Geospatial queries** (`$near`, `$geoWithin`, etc.)
- **Capped collections**
- **Text search** (MongoDB-specific syntax)
- **Complex Aggregation expressions** (only basic accumulators are supported)

## 👩‍💻 Development

### Prerequisites
- PHP 8.3+
- Docker & Docker Compose (for integration tests)

### Running Tests

We test Rango against **both** a real MongoDB and PostgreSQL to ensure 100% compatibility.

```bash
docker compose up -d
vendor/bin/phpunit
```
We are open to contributions as long as they are in line with
our [BC-Policy](https://patchlevel.dev/our-backward-compatibility-promise).

---
Built with ❤️ by the patchlevel team.
Also note that the `composer.lock` is always generated with the newest supported PHP version as this is the version our tools run in the CI.
53 changes: 53 additions & 0 deletions bin/docs-extract-php-code
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
#!/usr/bin/env php
<?php

use League\CommonMark\Environment\Environment;
use League\CommonMark\Extension\CommonMark\Node\Block\FencedCode;
use League\CommonMark\Node\Query;
use League\CommonMark\Parser\MarkdownParser;
use Wnx\CommonmarkMarkdownRenderer\MarkdownRendererExtension;

require __DIR__ . '/../vendor/autoload.php';

$environment = new Environment([]);
$environment->addExtension(new MarkdownRendererExtension());

$parser = new MarkdownParser($environment);

$targetDir = __DIR__ . '/../docs_php';

if (file_exists($targetDir)) {
exec('rm -rf ' . $targetDir);
}

mkdir($targetDir);

$finder = new Symfony\Component\Finder\Finder();
$finder->files()->in(__DIR__ . '/../docs')->name('*.md');

foreach ($finder as $file) {
$fileName = pathinfo($file->getBasename(), PATHINFO_FILENAME);

$content = file_get_contents($file->getPathname());
$document = $parser->parse($content);

$result = (new Query())
->where(Query::type(FencedCode::class))
->findAll($document);

/**
* @var FencedCode $node
*/
foreach ($result as $i => $node) {
if ($node->getInfo() !== 'php') {
continue;
}

$source = sprintf('%s:%s', $file->getRealPath(), $node->getStartLine());

$code = "<?php\n// " . $source . "\n\n" . $node->getLiteral();

$targetPath = $targetDir . '/' . $fileName . '_' . $i . '.php';
file_put_contents($targetPath, $code);
}
}
Loading
Loading