Variable DRIVER_LIVE_SUITE_PHPConst
DRIVER_LIVE_SUITE_PHP: "\nuse Drupal\\cfw_do_sqlite\\Driver\\Database\\cfw_do_sqlite\\CfwSqlClient;\nuse Drupal\\cfw_do_sqlite\\Driver\\Database\\cfw_do_sqlite\\Connection;\nuse Drupal\\Core\\Database\\IntegrityConstraintViolationException;\nuse Drupal\\Core\\Database\\InvalidQueryException;\nuse Drupal\\Core\\DrupalKernel;\n\n// __CFW_HOST_HELPERS__\n\n$out = ['passed' => 0, 'failed' => 0, 'checks' => []];\n$ok = function ($label, $condition, $detail = null) use (&$out) {\n\tif ($condition) {\n\t\t$out['passed']++;\n\t} else {\n\t\t$out['failed']++;\n\t}\n\t$out['checks'][] = ['label' => $label, 'ok' => (bool) $condition, 'detail' => $detail];\n};\n\n// require, NEVER require_once, and the guard tests the VALUE rather than the key. require_once\n// returns TRUE when the file is already included, so capturing its result yields the boolean\n// instead of the ClassLoader -- and a heap restore reaches exactly that state, with autoload.php in\n// the included-files table and this global not restored beside it. Measured on an imaged site:\n// Call to a member function addPsr4() on true. Composer getLoader() memoizes, so a plain require\n// hands back the same loader and re-registers nothing\nif (!isset($GLOBALS['__pw_autoloader']) || !is_object($GLOBALS['__pw_autoloader'])) {\n\t$GLOBALS['__pw_autoloader'] = require '/drupal/autoload.php';\n}\n$autoloader = $GLOBALS['__pw_autoloader'];\n$autoloader->addPsr4(\n\t'Drupal\\\\sqlite\\\\Driver\\\\Database\\\\sqlite\\\\',\n\t'/drupal/core/modules/sqlite/src/Driver/Database/sqlite/',\n);\n$autoloader->addPsr4(\n\t'Drupal\\\\cfw_do_sqlite\\\\Driver\\\\Database\\\\cfw_do_sqlite\\\\',\n\t'/drupal/modules/custom/cfw_do_sqlite/src/Driver/Database/cfw_do_sqlite/',\n);\n// this suite constructs the driver directly rather than through settings.php, so it is on its own\n// for the userland PDO the statement classes need; see src/site-do.js for the served path\nrequire_once '/drupal/modules/custom/cfw_do_sqlite/src/pdo-shim.php';\n\ntry {\n\tDrupalKernel::bootEnvironment();\n} catch (Throwable $e) {\n\t// already booted in this interpreter, which is fine\n}\n\ntry {\n\t$client = new CfwSqlClient();\n\t$ok('client constructs against the live bridge', true);\n\t$ok('client reports transaction support', $client->supportsTransactions());\n\n\t$connection = new Connection($client, ['prefix' => '']);\n\t$ok('connection constructs', true);\n\t$ok(\n\t\t'driver() is cfw_do_sqlite',\n\t\t$connection->driver() === 'cfw_do_sqlite',\n\t\t$connection->driver(),\n\t);\n\t$ok(\n\t\t'databaseType() is sqlite',\n\t\t$connection->databaseType() === 'sqlite',\n\t\t$connection->databaseType(),\n\t);\n\n\t$version = $connection->version();\n\t$ok(\n\t\t'version() returns an engine version through the DBAL',\n\t\t(bool) preg_match('/^3\\./', (string) $version),\n\t\t$version,\n\t);\n\t$ok('supportsAtomicCommit()', $connection->supportsAtomicCommit());\n\n\t$schema = $connection->schema();\n\t$connection->query('DROP TABLE IF EXISTS cfw_live');\n\t$schema->createTable('cfw_live', [\n\t\t'fields' => [\n\t\t\t'id' => ['type' => 'serial', 'not null' => true],\n\t\t\t// binary FALSE is the only thing that makes core emit a collation clause,\n\t\t\t// which is what Schema then rewrites to builtin NOCASE\n\t\t\t'name' => ['type' => 'varchar', 'length' => 64, 'not null' => true, 'binary' => false],\n\t\t\t'bin' => ['type' => 'varchar', 'length' => 64, 'not null' => false],\n\t\t\t'n' => ['type' => 'int', 'not null' => false],\n\t\t],\n\t\t'primary key' => ['id'],\n\t\t'indexes' => ['name' => ['name']],\n\t]);\n\t$ddl = $connection\n\t\t->query('SELECT sql FROM sqlite_master WHERE name = :n', [':n' => 'cfw_live'])\n\t\t->fetchField();\n\t$ok(\n\t\t'createTable() emitted COLLATE NOCASE, not NOCASE_UTF8',\n\t\tstr_contains((string) $ddl, 'COLLATE NOCASE') &&\n\t\t\t!str_contains((string) $ddl, 'NOCASE_UTF8'),\n\t\t$ddl,\n\t);\n\t$ok('schema()->createTable() through the DBAL', $schema->tableExists('cfw_live'));\n\t$ok('fieldExists() uses PRAGMA table_info', $schema->fieldExists('cfw_live', 'name'));\n\t$ok('indexExists() uses PRAGMA index_list', $schema->indexExists('cfw_live', 'name'));\n\n\t$id = $connection\n\t\t->insert('cfw_live')\n\t\t->fields(['name' => 'first', 'n' => 7])\n\t\t->execute();\n\t$ok('insert() returns a rowid', (string) $id === '1', $id);\n\n\t$connection\n\t\t->insert('cfw_live')\n\t\t->fields(['name' => 'second', 'n' => 8])\n\t\t->execute();\n\t$count = $connection->select('cfw_live', 'c')->countQuery()->execute()->fetchField();\n\t$ok('select() countQuery sees both rows', (string) $count === '2', $count);\n\n\t$name = $connection\n\t\t->query('SELECT name FROM {cfw_live} WHERE n = :n', [':n' => 8])\n\t\t->fetchField();\n\t$ok('query() with a named placeholder', $name === 'second', $name);\n\n\t$connection\n\t\t->update('cfw_live')\n\t\t->fields(['n' => 9])\n\t\t->condition('name', 'first')\n\t\t->execute();\n\t$n = $connection\n\t\t->query('SELECT n FROM {cfw_live} WHERE name = :name', [':name' => 'first'])\n\t\t->fetchField();\n\t$ok('update() through the DBAL', (string) $n === '9', $n);\n\n\t// ASCII case-insensitivity survives the NOCASE substitution\n\t$hit = $connection\n\t\t->query('SELECT COUNT(*) FROM {cfw_live} WHERE name = :name', [':name' => 'FIRST'])\n\t\t->fetchField();\n\t$ok('NOCASE folds ASCII on a binary=FALSE column', (string) $hit === '1', $hit);\n\n\t// the documented limitation, asserted rather than assumed: builtin NOCASE is\n\t// ASCII-only, so non-ASCII comparison stays case-SENSITIVE\n\t$connection\n\t\t->insert('cfw_live')\n\t\t->fields(['name' => \"\\u{00DC}nicode\", 'n' => 1])\n\t\t->execute();\n\t$folded = $connection\n\t\t->query('SELECT COUNT(*) FROM {cfw_live} WHERE name = :name', [':name' => \"\\u{00FC}nicode\"])\n\t\t->fetchField();\n\t$ok('NOCASE does NOT fold non-ASCII (documented gap)', (string) $folded === '0', $folded);\n\t$connection->delete('cfw_live')->condition('name', \"\\u{00DC}nicode\")->execute();\n\n\t// a column without binary=FALSE gets no collation clause, so it stays\n\t// case-sensitive; this is the control that proves the check above means\n\t// something\n\t$connection\n\t\t->update('cfw_live')\n\t\t->fields(['bin' => 'Exact'])\n\t\t->condition('name', 'first')\n\t\t->execute();\n\t$binHit = $connection\n\t\t->query('SELECT COUNT(*) FROM {cfw_live} WHERE bin = :v', [':v' => 'exact'])\n\t\t->fetchField();\n\t$ok(\n\t\t'a column with default collation stays case-sensitive (control)',\n\t\t(string) $binHit === '0',\n\t\t$binHit,\n\t);\n\n\t// LIKE BINARY, which threw before likeToGlob() was wired in. Every one of\n\t// these is a case where builtin GLOB alone would have been silently wrong.\n\t$connection\n\t\t->insert('cfw_live')\n\t\t->fields(['name' => 'Alpha%Beta', 'n' => 20])\n\t\t->execute();\n\t$connection\n\t\t->insert('cfw_live')\n\t\t->fields(['name' => 'Alpha*Beta', 'n' => 21])\n\t\t->execute();\n\t$connection\n\t\t->insert('cfw_live')\n\t\t->fields(['name' => 'alphaxbeta', 'n' => 22])\n\t\t->execute();\n\n\t$lb = function (string $pattern) use ($connection): string {\n\t\treturn (string) $connection\n\t\t\t->select('cfw_live', 'c')\n\t\t\t->condition('name', $pattern, 'LIKE BINARY')\n\t\t\t->countQuery()\n\t\t\t->execute()\n\t\t\t->fetchField();\n\t};\n\t$ok('LIKE BINARY no longer throws', true);\n\t$ok('LIKE BINARY % is a wildcard', $lb('Alpha%') === '2', $lb('Alpha%'));\n\t$ok('LIKE BINARY is case-sensitive', $lb('alpha%') === '1', $lb('alpha%'));\n\t$ok('LIKE BINARY _ matches one character', $lb('alpha_beta') === '1', $lb('alpha_beta'));\n\t$ok('LIKE BINARY treats * as a literal', $lb('%*%') === '1', $lb('%*%'));\n\t$ok('LIKE BINARY finds a literal percent', $lb('%\\\\%%') === '0', $lb('%\\\\%%'));\n\t$notLike = (string) $connection\n\t\t->select('cfw_live', 'c')\n\t\t->condition('name', 'Alpha%', 'NOT LIKE BINARY')\n\t\t->countQuery()\n\t\t->execute()\n\t\t->fetchField();\n\t$ok('NOT LIKE BINARY negates', $notLike !== '0', $notLike);\n\n\t// the entity-query path that generates LIKE BINARY in the first place\n\t$starts = (string) $connection\n\t\t->select('cfw_live', 'c')\n\t\t->condition('name', $connection->escapeLike('Alpha') . '%', 'LIKE BINARY')\n\t\t->countQuery()\n\t\t->execute()\n\t\t->fetchField();\n\t$ok('STARTS_WITH shape through escapeLike()', $starts === '2', $starts);\n\n\t$connection->delete('cfw_live')->condition('n', 20, '>=')->execute();\n\n\t// a real transaction, buffered in PHP and replayed atomically in the host\n\t$txn = $connection->startTransaction();\n\t$connection\n\t\t->insert('cfw_live')\n\t\t->fields(['name' => 'buffered', 'n' => 10])\n\t\t->execute();\n\t$inside = $connection->select('cfw_live', 'c')->countQuery()->execute()->fetchField();\n\t$ok(\n\t\t'a read inside the transaction sees its own buffered write',\n\t\t(string) $inside === '3',\n\t\t$inside,\n\t);\n\tunset($txn);\n\t$afterCommit = $connection->select('cfw_live', 'c')->countQuery()->execute()->fetchField();\n\t$ok('commit replays the buffer', (string) $afterCommit === '3', $afterCommit);\n\n\t$txn2 = $connection->startTransaction();\n\t$connection\n\t\t->insert('cfw_live')\n\t\t->fields(['name' => 'doomed', 'n' => 11])\n\t\t->execute();\n\t$txn2->rollBack();\n\tunset($txn2);\n\t$afterRollback = $connection->select('cfw_live', 'c')->countQuery()->execute()->fetchField();\n\t$ok('rollback writes nothing', (string) $afterRollback === '3', $afterRollback);\n\n\t// queryRange, which the core sqlite driver implements with LIMIT/OFFSET\n\t$range = $connection->queryRange('SELECT name FROM {cfw_live} ORDER BY id', 1, 1)->fetchField();\n\t$ok('queryRange()', $range === 'second', $range);\n\n\t// CREATE TEMPORARY TABLE is refused by the host authorizer, so the contract is\n\t// that queryTemporary() throws a message naming the reason rather than\n\t// surfacing a raw SQLITE_AUTH from somewhere deeper\n\ttry {\n\t\t$connection->queryTemporary('SELECT name FROM {cfw_live}', []);\n\t\t$ok('queryTemporary() refuses loudly', false, 'no exception thrown');\n\t} catch (InvalidQueryException $e) {\n\t\t$ok(\n\t\t\t'queryTemporary() refuses loudly',\n\t\t\tstr_contains($e->getMessage(), 'SQLITE_AUTH'),\n\t\t\t$e->getMessage(),\n\t\t);\n\t}\n\n\t// a constraint violation must map onto Drupal's exception, not a raw error\n\ttry {\n\t\t$connection->query('INSERT INTO {cfw_live} (id, name, n) VALUES (1, :name, 0)', [\n\t\t\t':name' => 'dupe',\n\t\t]);\n\t\t$ok(\n\t\t\t'duplicate primary key throws IntegrityConstraintViolationException',\n\t\t\tfalse,\n\t\t\t'no exception',\n\t\t);\n\t} catch (IntegrityConstraintViolationException $e) {\n\t\t$ok('duplicate primary key throws IntegrityConstraintViolationException', true);\n\t} catch (Throwable $e) {\n\t\t$ok(\n\t\t\t'duplicate primary key throws IntegrityConstraintViolationException',\n\t\t\tfalse,\n\t\t\tget_class($e) . ': ' . $e->getMessage(),\n\t\t);\n\t}\n\n\t$out['statementCount'] = $client->statementCount();\n\t$connection->query('DROP TABLE IF EXISTS cfw_live');\n} catch (Throwable $e) {\n\t$out['fatal'] = get_class($e) . ': ' . $e->getMessage();\n\t$out['trace'] = substr($e->getTraceAsString(), 0, 1200);\n}\n\necho json_encode($out);\n"