From 5c1383d4184cdd49f4c7087f189edc1d35008321 Mon Sep 17 00:00:00 2001 From: codeliner Date: Sun, 7 Jul 2019 00:36:20 +0200 Subject: [PATCH 1/2] Store metada in dedicated columns --- composer.json | 2 +- src/Index/RawSqlIndexCmd.php | 54 ++++ src/Metadata/Column.php | 19 ++ src/Metadata/MetadataColumnIndex.php | 75 +++++ src/PostgresDocumentStore.php | 175 ++++++++++- tests/MetadataPostgresDocumentStoreTest.php | 327 ++++++++++++++++++++ tests/PostgresDocumentStoreTest.php | 11 +- tests/TestUtil.php | 24 ++ 8 files changed, 666 insertions(+), 21 deletions(-) create mode 100644 src/Index/RawSqlIndexCmd.php create mode 100644 src/Metadata/Column.php create mode 100644 src/Metadata/MetadataColumnIndex.php create mode 100644 tests/MetadataPostgresDocumentStoreTest.php diff --git a/composer.json b/composer.json index 4f36920..90d005e 100644 --- a/composer.json +++ b/composer.json @@ -18,7 +18,7 @@ "require": { "php": "^7.1", "ext-pdo": "*", - "event-engine/php-persistence": "^0.3" + "event-engine/php-persistence": "^0.4" }, "require-dev": { "roave/security-advisories": "dev-master", diff --git a/src/Index/RawSqlIndexCmd.php b/src/Index/RawSqlIndexCmd.php new file mode 100644 index 0000000..87c6c8c --- /dev/null +++ b/src/Index/RawSqlIndexCmd.php @@ -0,0 +1,54 @@ +sql = $sql; + $this->name = $name; + } + + + public function toArray() + { + return [ + 'sql' => $this->sql, + 'name' => $this->name, + ]; + } + + public function sql(): string + { + return $this->sql; + } + + public function name(): ?string + { + return $this->name; + } +} diff --git a/src/Metadata/Column.php b/src/Metadata/Column.php new file mode 100644 index 0000000..276a893 --- /dev/null +++ b/src/Metadata/Column.php @@ -0,0 +1,19 @@ +sql = $sql; + } + + public function sql(): string + { + return $this->sql; + } +} diff --git a/src/Metadata/MetadataColumnIndex.php b/src/Metadata/MetadataColumnIndex.php new file mode 100644 index 0000000..02e98a6 --- /dev/null +++ b/src/Metadata/MetadataColumnIndex.php @@ -0,0 +1,75 @@ +columns = $columns; + $this->indexCmd = $indexCmd; + } + + /** + * @return Column[] + */ + public function columns(): array + { + return $this->columns; + } + + /** + * @return Index + */ + public function indexCmd(): Index + { + return $this->indexCmd; + } + + public function toArray() + { + return [ + 'columns' => array_map(function (Column $column) { + return $column->sql(); + }, $this->columns), + 'index' => $this->indexCmd->toArray(), + 'indexClass' => get_class($this->indexCmd), + ]; + } +} diff --git a/src/PostgresDocumentStore.php b/src/PostgresDocumentStore.php index 1c4fea0..ddb34a6 100644 --- a/src/PostgresDocumentStore.php +++ b/src/PostgresDocumentStore.php @@ -17,6 +17,7 @@ use EventEngine\DocumentStore\OrderBy\OrderBy; use EventEngine\DocumentStore\Postgres\Exception\InvalidArgumentException; use EventEngine\DocumentStore\Postgres\Exception\RuntimeException; +use EventEngine\Util\VariableType; final class PostgresDocumentStore implements DocumentStore\DocumentStore { @@ -31,11 +32,14 @@ final class PostgresDocumentStore implements DocumentStore\DocumentStore private $manageTransactions; + private $useMetadataColumns = false; + public function __construct( \PDO $connection, string $tablePrefix = null, string $docIdSchema = null, - bool $transactional = true + bool $transactional = true, + bool $useMetadataColumns = false ) { $this->connection = $connection; $this->connection->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION); @@ -49,6 +53,8 @@ public function __construct( } $this->manageTransactions = $transactional; + + $this->useMetadataColumns = $useMetadataColumns; } /** @@ -130,10 +136,22 @@ public function hasCollection(string $collectionName): bool */ public function addCollection(string $collectionName, Index ...$indices): void { + $metadataColumns = ''; + + foreach ($indices as $i => $index) { + if($index instanceof DocumentStore\Postgres\Metadata\MetadataColumnIndex) { + foreach ($index->columns() as $column) { + $metadataColumns .= $column->sql().', '; + } + $indices[$i] = $index->indexCmd(); + } + } + $cmd = <<tableName($collectionName)} ( id {$this->docIdSchema}, doc JSONB NOT NULL, + $metadataColumns PRIMARY KEY (id) ); EOT; @@ -191,21 +209,80 @@ public function hasCollectionIndex(string $collectionName, string $indexName): b */ public function addCollectionIndex(string $collectionName, Index $index): void { - $cmd = $this->indexToSqlCmd($index, $collectionName); + $metadataColumnCmd = null; + + if($index instanceof DocumentStore\Postgres\Metadata\MetadataColumnIndex) { + + $columnsSql = ''; + + foreach ($index->columns() as $column) { + $columnsSql .= ', ADD COLUMN ' . $column->sql(); + } + + $columnsSql = substr($columnsSql, 2); + + $metadataColumnCmd = <<tableName($collectionName)} + $columnsSql; +EOT; + + $index = $index->indexCmd(); + } + + $indexCmd = $this->indexToSqlCmd($index, $collectionName); - $this->connection->prepare($cmd)->execute(); + $this->transactional(function() use ($metadataColumnCmd, $indexCmd) { + + if($metadataColumnCmd) { + $this->connection->prepare($metadataColumnCmd)->execute(); + } + + $this->connection->prepare($indexCmd)->execute(); + }); } /** * @param string $collectionName - * @param string $indexName + * @param string|Index $index * @throws \EventEngine\DocumentStore\Exception\RuntimeException if dropping did not succeed + * @throws \Throwable */ - public function dropCollectionIndex(string $collectionName, string $indexName): void + public function dropCollectionIndex(string $collectionName, $index): void { + $metadataColumnCmd = null; + + if($index instanceof DocumentStore\Postgres\Metadata\MetadataColumnIndex) { + + $columnsSql = ''; + + foreach ($index->columns() as $column) { + $columnsSql .= ', DROP COLUMN IF EXISTS ' . $this->getColumnNameFromSql($column->sql()); + } + + $columnsSql = substr($columnsSql, 2); + + $metadataColumnCmd = <<tableName($collectionName)} + $columnsSql; +EOT; + $index = $index->indexCmd(); + } + + $indexName = is_string($index)? $index : $this->getIndexName($index); + + if($indexName === null) { + throw new DocumentStore\Exception\RuntimeException("Given index does not have a name: ". VariableType::determine($index)); + } + $cmd = "DROP INDEX $indexName"; - $this->connection->prepare($cmd)->execute(); + $this->transactional(function () use($cmd, $metadataColumnCmd) { + $this->connection->prepare($cmd)->execute(); + + if($metadataColumnCmd) { + $this->connection->prepare($metadataColumnCmd)->execute(); + } + }); } /** @@ -216,14 +293,33 @@ public function dropCollectionIndex(string $collectionName, string $indexName): */ public function addDoc(string $collectionName, string $docId, array $doc): void { + $metadataKeysStr = ''; + $metadataValsStr = ''; + $metadata = []; + + if($this->useMetadataColumns && array_key_exists('metadata', $doc)) { + $metadata = $doc['metadata']; + unset($doc['metadata']); + + if(!is_array($metadata)) { + throw new RuntimeException("metadata should be of type array"); + } + + foreach ($metadata as $k => $v) { + $metadataKeysStr .= ', '.$k; + $metadataValsStr .= ', :'.$k; + } + } + $cmd = <<tableName($collectionName)} (id, doc) VALUES (:id, :doc); +INSERT INTO {$this->tableName($collectionName)} (id, doc$metadataKeysStr) VALUES (:id, :doc$metadataValsStr); EOT; - $this->transactional(function () use ($cmd, $docId, $doc) { - $this->connection->prepare($cmd)->execute([ + + $this->transactional(function () use ($cmd, $docId, $doc, $metadata) { + $this->connection->prepare($cmd)->execute(array_merge([ 'id' => $docId, 'doc' => json_encode($doc) - ]); + ], $metadata)); }); } @@ -235,17 +331,30 @@ public function addDoc(string $collectionName, string $docId, array $doc): void */ public function updateDoc(string $collectionName, string $docId, array $docOrSubset): void { + $metadataStr = ''; + $metadata = []; + + if($this->useMetadataColumns && array_key_exists('metadata', $docOrSubset)) { + $metadata = $docOrSubset['metadata']; + unset($docOrSubset['metadata']); + + + foreach ($metadata as $k => $v) { + $metadataStr .= ', '.$k.' = :'.$k; + } + } + $cmd = <<tableName($collectionName)} -SET doc = (to_jsonb(doc) || :doc) +SET doc = (to_jsonb(doc) || :doc)$metadataStr WHERE id = :id ; EOT; - $this->transactional(function () use ($cmd, $docId, $docOrSubset) { - $this->connection->prepare($cmd)->execute([ + $this->transactional(function () use ($cmd, $docId, $docOrSubset, $metadata) { + $this->connection->prepare($cmd)->execute(array_merge([ 'id' => $docId, 'doc' => json_encode($docOrSubset) - ]); + ], $metadata)); }); } @@ -261,13 +370,27 @@ public function updateMany(string $collectionName, Filter $filter, array $set): $where = $filterStr? "WHERE $filterStr" : ''; + $metadataStr = ''; + $metadata = []; + + if($this->useMetadataColumns && array_key_exists('metadata', $set)) { + $metadata = $set['metadata']; + unset($set['metadata']); + + + foreach ($metadata as $k => $v) { + $metadataStr .= ', '.$k.' = :'.$k; + } + } + $cmd = <<tableName($collectionName)} -SET doc = (to_jsonb(doc) || :doc) +SET doc = (to_jsonb(doc) || :doc)$metadataStr $where; EOT; $args['doc'] = json_encode($set); + $args = array_merge($args, $metadata); $this->transactional(function () use ($cmd, $args) { $this->connection->prepare($cmd)->execute($args); @@ -506,6 +629,10 @@ private function filterToWhereClause(Filter $filter, $argsCount = 0): array private function propToJsonPath(string $field): string { + if($this->useMetadataColumns && strpos($field, 'metadata.') === 0) { + return str_replace('metadata.', '', $field); + } + return "doc->'" . str_replace('.', "'->'", $field) . "'"; } @@ -565,6 +692,8 @@ private function indexToSqlCmd(Index $index, string $collectionName): string $type = $index->unique() ? 'UNIQUE INDEX' : 'INDEX'; $fieldParts = array_map([$this, 'extractFieldPartFromFieldIndex'], $index->fields()); $fields = '('.implode(', ', $fieldParts).')'; + } elseif ($index instanceof DocumentStore\Postgres\Index\RawSqlIndexCmd) { + return $index->sql(); } else { throw new RuntimeException('Unsupported index type. Got ' . get_class($index)); } @@ -579,6 +708,22 @@ private function indexToSqlCmd(Index $index, string $collectionName): string return $cmd; } + private function getIndexName(Index $index): ?string + { + if(method_exists($index, 'name')) { + return $index->name(); + } + + return null; + } + + private function getColumnNameFromSql(string $columnSql): string + { + $parts = explode(' ', $columnSql); + + return $parts[0]; + } + private function extractFieldPartFromFieldIndex(DocumentStore\FieldIndex $fieldIndex): string { $direction = $fieldIndex->sort() === Index::SORT_ASC ? 'ASC' : 'DESC'; diff --git a/tests/MetadataPostgresDocumentStoreTest.php b/tests/MetadataPostgresDocumentStoreTest.php new file mode 100644 index 0000000..d4abfe5 --- /dev/null +++ b/tests/MetadataPostgresDocumentStoreTest.php @@ -0,0 +1,327 @@ +connection = TestUtil::getConnection(); + $this->documentStore = new PostgresDocumentStore($this->connection, self::TABLE_PREFIX, null, true, true); + } + + public function tearDown(): void + { + TestUtil::tearDownDatabase(); + } + + /** + * @test + */ + public function it_adds_collection_with_metadata_column_index() + { + $collectionName = 'test_collection_with_metadata_column_index'; + $tablePrefix = self::TABLE_PREFIX; + $metadataColumnIndex = new MetadataColumnIndex( + new RawSqlIndexCmd("CREATE INDEX test_column_index ON {$tablePrefix}{$collectionName}(version)", 'test_column_index'), + new Column('version INTEGER') + ); + + $this->documentStore->addCollection($collectionName, $metadataColumnIndex); + + $columns = $this->getColumns($collectionName); + + $this->assertCount(3, $columns); + $this->assertEquals('version', $columns[2]); + + $indexes = $this->getIndexes($collectionName); + + $this->assertCount(2, $indexes); + $this->assertStringEndsWith("USING btree (version)", $indexes[1]['indexdef']); + } + + /** + * @test + */ + public function it_adds_metadata_index_to_existing_collection() + { + $collectionName = 'test_collection_with_altered_metadata_column_index'; + + $this->documentStore->addCollection($collectionName); + + $index = new MetadataColumnIndex( + FieldIndex::namedIndexForField('meta_field_idx_version', 'metadata.version'), + new Column('version INTEGER') + ); + + $this->documentStore->addCollectionIndex($collectionName, $index); + + $columns = $this->getColumns($collectionName); + + $this->assertCount(3, $columns); + $this->assertEquals('version', $columns[2]); + + $indexes = $this->getIndexes($collectionName); + + $this->assertCount(2, $indexes); + $this->assertStringEndsWith("USING btree (version)", $indexes[1]['indexdef']); + } + + /** + * @test + */ + public function it_drops_metadata_index_and_column() + { + $collectionName = 'test_collection_with_dropped_metadata_column_index'; + + $index = new MetadataColumnIndex( + FieldIndex::namedIndexForField('meta_field_idx_version', 'metadata.version'), + new Column('version INTEGER') + ); + + $this->documentStore->addCollection($collectionName, $index); + + $columns = $this->getColumns($collectionName); + $this->assertCount(3, $columns); + + $this->documentStore->dropCollectionIndex($collectionName, $index); + + $columns = $this->getColumns($collectionName); + $this->assertCount(2, $columns); + $this->assertEquals(['id', 'doc'], $columns); + } + + /** + * @test + */ + public function it_adds_collection_with_mulitple_metadata_columns() + { + $collectionName = 'test_collection_with_multi_metadata_column_index'; + + $index1 = new MetadataColumnIndex( + FieldIndex::namedIndexForField('meta_field_idx_version', 'metadata.version'), + new Column('version INTEGER') + ); + + $index2 = new MetadataColumnIndex( + MultiFieldIndex::namedIndexForFields('multi_meta_idx_stars_downloads', ['metadata.stars', 'metadata.downloads']), + new Column('stars INTEGER'), + new Column('downloads INTEGER') + ); + + $this->documentStore->addCollection($collectionName, $index1, $index2); + + $columns = $this->getColumns($collectionName); + $this->assertCount(5, $columns); + $this->assertEquals(['id', 'doc', 'version', 'stars', 'downloads'], $columns); + + $indexes = $this->getIndexes($collectionName); + + $this->assertCount(3, $indexes); + $this->assertStringEndsWith("USING btree (version)", $indexes[1]['indexdef']); + $this->assertStringEndsWith("USING btree (stars, downloads)", $indexes[2]['indexdef']); + } + + /** + * @test + */ + public function it_adds_multiple_metadata_indexes_to_collection() + { + $collectionName = 'test_collection_with_altered_metadata_column_index'; + + $this->documentStore->addCollection($collectionName); + + $index1 = new MetadataColumnIndex( + FieldIndex::namedIndexForField('meta_field_idx_version', 'metadata.version'), + new Column('version INTEGER') + ); + + $this->documentStore->addCollectionIndex($collectionName, $index1); + + $index2 = new MetadataColumnIndex( + MultiFieldIndex::namedIndexForFields('multi_meta_idx_stars_downloads', ['metadata.stars', 'metadata.downloads']), + new Column('stars INTEGER'), + new Column('downloads INTEGER') + ); + + $this->documentStore->addCollectionIndex($collectionName, $index2); + + $columns = $this->getColumns($collectionName); + $this->assertCount(5, $columns); + $this->assertEquals(['id', 'doc', 'version', 'stars', 'downloads'], $columns); + + $indexes = $this->getIndexes($collectionName); + + $this->assertCount(3, $indexes); + $this->assertStringEndsWith("USING btree (version)", $indexes[1]['indexdef']); + $this->assertStringEndsWith("USING btree (stars, downloads)", $indexes[2]['indexdef']); + } + + /** + * @test + */ + public function it_drops_multi_column_metadata_index() + { + $collectionName = 'test_collection_with_dropped_multi_column_index'; + + $index1 = new MetadataColumnIndex( + FieldIndex::namedIndexForField('meta_field_idx_version', 'metadata.version'), + new Column('version INTEGER') + ); + + $index2 = new MetadataColumnIndex( + MultiFieldIndex::namedIndexForFields('multi_meta_idx_stars_downloads', ['metadata.stars', 'metadata.downloads']), + new Column('stars INTEGER'), + new Column('downloads INTEGER') + ); + + $this->documentStore->addCollection($collectionName, $index1, $index2); + + $columns = $this->getColumns($collectionName); + $this->assertCount(5, $columns); + + $this->documentStore->dropCollectionIndex($collectionName, $index2); + + $columns = $this->getColumns($collectionName); + $this->assertCount(3, $columns); + $this->assertEquals('version', $columns[2]); + + $indexes = $this->getIndexes($collectionName); + + $this->assertCount(2, $indexes); + $this->assertStringEndsWith("USING btree (version)", $indexes[1]['indexdef']); + } + + /** + * @test + */ + public function it_fills_and_queries_metadata_column() + { + $collectionName = 'test_col_query_version_meta'; + + $index1 = new MetadataColumnIndex( + FieldIndex::namedIndexForField('meta_field_idx_version', 'metadata.version'), + new Column('version INTEGER') + ); + + $this->documentStore->addCollection($collectionName, $index1); + + $docId1 = Uuid::uuid4()->toString(); + $docId2 = Uuid::uuid4()->toString(); + $docId3 = Uuid::uuid4()->toString(); + + $this->documentStore->addDoc($collectionName, $docId1, ['state' => ['name' => 'v1'], 'metadata' => ['version' => 1]]); + $this->documentStore->addDoc($collectionName, $docId2, ['state' => ['name' => 'v2'], 'metadata' => ['version' => 2]]); + $this->documentStore->addDoc($collectionName, $docId3, ['state' => ['name' => 'v3'], 'metadata' => ['version' => 3]]); + + $prefix = self::TABLE_PREFIX; + $stmt = "SELECT * FROM $prefix{$collectionName} WHERE version = 2;"; + $stmt = $this->connection->prepare($stmt); + $stmt->execute(); + $docs = $stmt->fetchAll(); + + $this->assertCount(1, $docs); + $this->assertEquals($docId2, $docs[0]['id']); + $this->assertEquals(['state' => ['name' => 'v2']], json_decode($docs[0]['doc'], true)); + $this->assertEquals(2, $docs[0]['version']); + + $docs = iterator_to_array($this->documentStore->filterDocs( + $collectionName, + new GteFilter('metadata.version', 2), + null, + null, + Desc::byProp('metadata.version') + )); + + $this->assertCount(2, $docs); + $this->assertEquals('v3', $docs[0]['state']['name']); + $this->assertEquals('v2', $docs[1]['state']['name']); + + + $this->documentStore->updateDoc($collectionName, $docId1, ['state' => ['name' => 'v4'], 'metadata' => ['version' => 4]]); + + $docs = iterator_to_array($this->documentStore->filterDocs( + $collectionName, + new GteFilter('metadata.version', 2), + null, + null, + Desc::byProp('metadata.version') + )); + + $this->assertCount(3, $docs); + $this->assertEquals('v4', $docs[0]['state']['name']); + + $this->documentStore->updateMany( + $collectionName, + new LtFilter('metadata.version', 4), + [ + 'state' => ['name' => 'v5'], + 'metadata' => ['version' => 5] + ] + ); + + $docs = iterator_to_array($this->documentStore->filterDocs( + $collectionName, + new GtFilter('metadata.version', 4), + null, + null, + Desc::byProp('metadata.version') + )); + + $this->assertCount(2, $docs); + $this->assertEquals('v5', $docs[0]['state']['name']); + $this->assertEquals('v5', $docs[1]['state']['name']); + + $this->documentStore->deleteMany($collectionName, new EqFilter('metadata.version', 5)); + + $docs = iterator_to_array($this->documentStore->filterDocs( + $collectionName, + new AnyFilter(), + null, + null, + Desc::byProp('metadata.version') + )); + + $this->assertCount(1, $docs); + $this->assertEquals('v4', $docs[0]['state']['name']); + } + + private function getIndexes(string $collectionName): array + { + return TestUtil::getIndexes($this->connection, self::TABLE_PREFIX.$collectionName); + } + + private function getColumns(string $collectionName): array + { + return TestUtil::getColumns($this->connection, self::TABLE_PREFIX.$collectionName); + } +} diff --git a/tests/PostgresDocumentStoreTest.php b/tests/PostgresDocumentStoreTest.php index a3da0b5..fbfd40f 100644 --- a/tests/PostgresDocumentStoreTest.php +++ b/tests/PostgresDocumentStoreTest.php @@ -307,11 +307,12 @@ public function it_handles_not_any_of_id_filter() private function getIndexes(string $collectionName): array { - $stmt = $this->connection->prepare( - "select * from pg_indexes where schemaname = 'public' and tablename = :name" - ); - $stmt->execute(['name' => self::TABLE_PREFIX . $collectionName]); - return $stmt->fetchAll(); + return TestUtil::getIndexes($this->connection, self::TABLE_PREFIX.$collectionName); + } + + private function getColumns(string $collectionName): array + { + return TestUtil::getColumns($this->connection, self::TABLE_PREFIX.$collectionName); } } diff --git a/tests/TestUtil.php b/tests/TestUtil.php index f248b97..fe263d0 100644 --- a/tests/TestUtil.php +++ b/tests/TestUtil.php @@ -105,6 +105,30 @@ public static function subMilliseconds(\DateTimeImmutable $time, int $ms): \Date return $time->sub($interval); } + public static function getIndexes(PDO $connection, string $tableName): array + { + $stmt = $connection->prepare( + "select * from pg_indexes where schemaname = 'public' and tablename = :name" + ); + $stmt->execute(['name' => $tableName]); + return $stmt->fetchAll(); + } + + public static function getColumns(PDO $connection, string $tableName): array + { + $stmt = $connection->prepare( + "SELECT * +FROM information_schema.columns +WHERE table_schema = 'public' + AND table_name = :name" + ); + + $stmt->execute(['name' => $tableName]); + return array_map(function (array $info) { + return $info['column_name']; + }, $stmt->fetchAll()); + } + private static function hasRequiredConnectionParams(): bool { $env = getenv(); From 46ff17715cc47fbc59d7b05f414d37e80cad04e0 Mon Sep 17 00:00:00 2001 From: codeliner Date: Sun, 7 Jul 2019 00:51:06 +0200 Subject: [PATCH 2/2] Improve sql stmt readability --- src/PostgresDocumentStore.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/PostgresDocumentStore.php b/src/PostgresDocumentStore.php index ddb34a6..6e724f5 100644 --- a/src/PostgresDocumentStore.php +++ b/src/PostgresDocumentStore.php @@ -312,7 +312,7 @@ public function addDoc(string $collectionName, string $docId, array $doc): void } $cmd = <<tableName($collectionName)} (id, doc$metadataKeysStr) VALUES (:id, :doc$metadataValsStr); +INSERT INTO {$this->tableName($collectionName)} (id, doc{$metadataKeysStr}) VALUES (:id, :doc{$metadataValsStr}); EOT; $this->transactional(function () use ($cmd, $docId, $doc, $metadata) { @@ -346,7 +346,7 @@ public function updateDoc(string $collectionName, string $docId, array $docOrSub $cmd = <<tableName($collectionName)} -SET doc = (to_jsonb(doc) || :doc)$metadataStr +SET doc = (to_jsonb(doc) || :doc){$metadataStr} WHERE id = :id ; EOT; @@ -385,7 +385,7 @@ public function updateMany(string $collectionName, Filter $filter, array $set): $cmd = <<tableName($collectionName)} -SET doc = (to_jsonb(doc) || :doc)$metadataStr +SET doc = (to_jsonb(doc) || :doc){$metadataStr} $where; EOT;