Skip to content

Commit 704a827

Browse files
authored
Fix table /w schema support for all DBs (#1024)
1 parent b763bb2 commit 704a827

13 files changed

+186
-175
lines changed

phpstan.neon.dist

+2-2
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,9 @@ parameters:
3232

3333
# fix https://github.com/phpstan/phpstan-deprecation-rules/issues/52 and https://github.com/phpstan/phpstan/issues/6444
3434
-
35-
message: '~^Call to method (getVarcharTypeDeclarationSQL|getClobTypeDeclarationSQL|getCreateTableSQL)\(\) of deprecated class Doctrine\\DBAL\\Platforms\\(SQLServerPlatform|AbstractPlatform):\nUse.+instead\.$~'
35+
message: '~^Call to method (getVarcharTypeDeclarationSQL|getClobTypeDeclarationSQL|getCreateTableSQL|getCurrentDatabaseExpression)\(\) of deprecated class Doctrine\\DBAL\\Platforms\\(PostgreSQLPlatform|SQLServerPlatform|AbstractPlatform):\nUse.+instead\.$~'
3636
path: '*'
37-
count: 3
37+
count: 5
3838

3939
# TODO these rules are generated, this ignores should be fixed in the code
4040
# for src/Schema/TestCase.php

src/Model/Join.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -240,7 +240,7 @@ protected function init(): void
240240
}
241241

242242
if (!$this->foreign_field) {
243-
$this->foreign_field = preg_replace('~^.+?\.~', '', $this->getModelTableString($this->getOwner())) . '_' . $id_field;
243+
$this->foreign_field = preg_replace('~^.+\.~s', '', $this->getModelTableString($this->getOwner())) . '_' . $id_field;
244244
}
245245
} else {
246246
$this->reverse = false;

src/Persistence/Sql/Mssql/PlatformTrait.php

+9
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,15 @@ public function getClobTypeDeclarationSQL(array $column)
3131
// $this->markDoctrineTypeCommented('text');
3232
// }
3333

34+
public function getCurrentDatabaseExpression(bool $includeSchema = false): string
35+
{
36+
if ($includeSchema) {
37+
return 'CONCAT(DB_NAME(), \'.\', SCHEMA_NAME())';
38+
}
39+
40+
return parent::getCurrentDatabaseExpression();
41+
}
42+
3443
// SQL Server DBAL platform has buggy identifier escaping, fix until fixed officially, see:
3544
// https://github.com/doctrine/dbal/pull/4360
3645

src/Persistence/Sql/Oracle/Connection.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ protected static function createDbalEventManager(): EventManager
3737
public function lastInsertId(string $sequence = null): string
3838
{
3939
if ($sequence) {
40-
return $this->dsql()->field($this->expr('{}.CURRVAL', [$sequence]))->getOne();
40+
return $this->dsql()->field($this->expr('{{}}.CURRVAL', [$sequence]))->getOne();
4141
}
4242

4343
return parent::lastInsertId($sequence);

src/Persistence/Sql/Oracle/PlatformTrait.php

+26-5
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,10 @@ public function getCreateAutoincrementSql($name, $table, $start = 1)
5656
{
5757
$sqls = parent::getCreateAutoincrementSql($name, $table, $start);
5858

59+
// fix table name when name /w schema is used
60+
// TODO submit a PR with fixed OraclePlatform to DBAL
61+
$sqls[0] = preg_replace('~(?<=WHERE TABLE_NAME = \').+\.(?=.+?\')~', '', $sqls[0]);
62+
5963
// replace trigger from https://github.com/doctrine/dbal/blob/3.1.3/src/Platforms/OraclePlatform.php#L526-L546
6064
$tableIdentifier = \Closure::bind(fn () => $this->normalizeIdentifier($table), $this, OraclePlatform::class)();
6165
$nameIdentifier = \Closure::bind(fn () => $this->normalizeIdentifier($name), $this, OraclePlatform::class)();
@@ -68,20 +72,20 @@ public function getCreateAutoincrementSql($name, $table, $start = 1)
6872
$sqls[count($sqls) - 1] = $conn->expr(
6973
// else branch should be maybe (because of concurrency) put into after update trigger
7074
str_replace('[pk_seq]', '\'' . str_replace('\'', '\'\'', $pkSeq) . '\'', <<<'EOT'
71-
CREATE TRIGGER {trigger}
75+
CREATE TRIGGER {{trigger}}
7276
BEFORE INSERT OR UPDATE
73-
ON {table}
77+
ON {{table}}
7478
FOR EACH ROW
7579
DECLARE
76-
atk4__pk_seq_last__ {table}.{pk}%TYPE;
80+
atk4__pk_seq_last__ {{table}}.{pk}%TYPE;
7781
BEGIN
7882
IF (:NEW.{pk} IS NULL) THEN
79-
SELECT {pk_seq}.NEXTVAL INTO :NEW.{pk} FROM DUAL;
83+
SELECT {{pk_seq}}.NEXTVAL INTO :NEW.{pk} FROM DUAL;
8084
ELSE
8185
SELECT LAST_NUMBER INTO atk4__pk_seq_last__ FROM USER_SEQUENCES WHERE SEQUENCE_NAME = [pk_seq];
8286
WHILE atk4__pk_seq_last__ <= :NEW.{pk}
8387
LOOP
84-
SELECT {pk_seq}.NEXTVAL + 1 INTO atk4__pk_seq_last__ FROM DUAL;
88+
SELECT {{pk_seq}}.NEXTVAL + 1 INTO atk4__pk_seq_last__ FROM DUAL;
8589
END LOOP;
8690
END IF;
8791
END;
@@ -96,4 +100,21 @@ public function getCreateAutoincrementSql($name, $table, $start = 1)
96100

97101
return $sqls;
98102
}
103+
104+
public function getListDatabasesSQL(): string
105+
{
106+
// ignore Oracle maintained schemas, improve tests performance
107+
return 'SELECT username FROM sys.all_users'
108+
. ' WHERE oracle_maintained = \'N\'';
109+
}
110+
111+
public function getListTablesSQL(): string
112+
{
113+
// ignore Oracle maintained tables, improve tests performance
114+
// self::getListViewsSQL() does not need filtering, as there is no Oracle VIEW by default
115+
return 'SELECT * FROM sys.user_tables'
116+
. ' LEFT JOIN sys.user_objects ON user_objects.object_type = \'TABLE\''
117+
. ' AND user_objects.object_name = user_tables.table_name'
118+
. ' WHERE oracle_maintained = \'N\'';
119+
}
99120
}

src/Persistence/Sql/Postgresql/PlatformTrait.php

+16-7
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,15 @@ public function getClobTypeDeclarationSQL(array $column)
4141
return 'CITEXT';
4242
}
4343

44+
public function getCurrentDatabaseExpression(bool $includeSchema = false): string
45+
{
46+
if ($includeSchema) {
47+
return 'CONCAT(CURRENT_DATABASE(), \'.\', CURRENT_SCHEMA())';
48+
}
49+
50+
return parent::getCurrentDatabaseExpression();
51+
}
52+
4453
// PostgreSQL DBAL platform uses SERIAL column type for autoincrement which does not increment
4554
// when a row with a not-null PK is inserted like Sqlite or MySQL does, unify the behaviour
4655

@@ -65,15 +74,15 @@ protected function getCreateAutoincrementSql(Table $table, Column $pkColumn): ar
6574
// else branch should be maybe (because of concurrency) put into after update trigger
6675
// with pure nextval instead of setval with a loop like in Oracle trigger
6776
str_replace('[pk_seq]', '\'' . $pkSeqName . '\'', <<<'EOF'
68-
CREATE OR REPLACE FUNCTION {trigger_func}()
77+
CREATE OR REPLACE FUNCTION {{trigger_func}}()
6978
RETURNS trigger AS $$
7079
DECLARE
7180
atk4__pk_seq_last__ {table}.{pk}%TYPE;
7281
BEGIN
7382
IF (NEW.{pk} IS NULL) THEN
7483
NEW.{pk} := nextval([pk_seq]);
7584
ELSE
76-
SELECT COALESCE(last_value, 0) INTO atk4__pk_seq_last__ FROM {pk_seq};
85+
SELECT COALESCE(last_value, 0) INTO atk4__pk_seq_last__ FROM {{pk_seq}};
7786
IF (atk4__pk_seq_last__ <= NEW.{pk}) THEN
7887
atk4__pk_seq_last__ := setval([pk_seq], NEW.{pk}, true);
7988
END IF;
@@ -83,24 +92,24 @@ protected function getCreateAutoincrementSql(Table $table, Column $pkColumn): ar
8392
$$ LANGUAGE plpgsql
8493
EOF),
8594
[
86-
'table' => $table->getName(),
95+
'table' => $table->getShortestName($table->getNamespaceName()), // TODO should be probably name /w schema, but it is not supported, get variable type differently
8796
'pk' => $pkColumn->getName(),
8897
'pk_seq' => $pkSeqName,
89-
'trigger_func' => $table->getName() . '_AI_FUNC', // TODO create only one function per schema
98+
'trigger_func' => $table->getName() . '_AI_FUNC',
9099
]
91100
)->render()[0];
92101

93102
$sqls[] = $conn->expr(
94103
<<<'EOF'
95104
CREATE TRIGGER {trigger}
96105
BEFORE INSERT OR UPDATE
97-
ON {table}
106+
ON {{table}}
98107
FOR EACH ROW
99-
EXECUTE PROCEDURE {trigger_func}()
108+
EXECUTE PROCEDURE {{trigger_func}}()
100109
EOF,
101110
[
102111
'table' => $table->getName(),
103-
'trigger' => $table->getName() . '_AI_PK',
112+
'trigger' => $table->getShortestName($table->getNamespaceName()) . '_AI_PK',
104113
'trigger_func' => $table->getName() . '_AI_FUNC',
105114
]
106115
)->render()[0];

src/Persistence/Sql/Query.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -370,7 +370,7 @@ public function join(
370370
}
371371

372372
// Split and deduce fields
373-
// NOTE that this will not allow table names with dots in there !!!
373+
// TODO this will not allow table names with dots in there !!!
374374
[$f1, $f2] = array_pad(explode('.', $foreign_table, 2), 2, null);
375375

376376
if (is_object($master_field)) {

src/Reference/HasMany.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ public function getTheirFieldName(Model $theirModel = null): string
2929
// this is pure guess, verify if such field exist, otherwise throw
3030
// TODO probably remove completely in the future
3131
$ourModel = $this->getOurModel(null);
32-
$theirFieldName = preg_replace('~^.+?\.~', '', $this->getModelTableString($ourModel)) . '_' . $ourModel->id_field;
32+
$theirFieldName = preg_replace('~^.+\.~s', '', $this->getModelTableString($ourModel)) . '_' . $ourModel->id_field;
3333
if (!$this->createTheirModel()->hasField($theirFieldName)) {
3434
throw (new Exception('Their model does not contain fallback field'))
3535
->addMoreInfo('their_fallback_field', $theirFieldName);

src/Schema/Migrator.php

+44-9
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,14 @@
1111
use Atk4\Data\Persistence;
1212
use Atk4\Data\Persistence\Sql\Connection;
1313
use Atk4\Data\Reference\HasOne;
14-
use Doctrine\DBAL\Exception as DbalException;
14+
use Doctrine\DBAL\Driver\Exception as DbalDriverException;
15+
use Doctrine\DBAL\Exception\DatabaseObjectNotFoundException;
16+
use Doctrine\DBAL\Exception\TableNotFoundException;
1517
use Doctrine\DBAL\Platforms\AbstractPlatform;
1618
use Doctrine\DBAL\Platforms\MySQLPlatform;
1719
use Doctrine\DBAL\Platforms\OraclePlatform;
1820
use Doctrine\DBAL\Platforms\SqlitePlatform;
21+
use Doctrine\DBAL\Platforms\SQLServerPlatform;
1922
use Doctrine\DBAL\Schema\AbstractSchemaManager;
2023
use Doctrine\DBAL\Schema\Table;
2124

@@ -79,11 +82,28 @@ protected function createSchemaManager(): AbstractSchemaManager
7982

8083
public function table(string $tableName): self
8184
{
82-
$this->table = new Table($this->getDatabasePlatform()->quoteIdentifier($tableName));
85+
$table = new Table('0.0');
8386
if ($this->getDatabasePlatform() instanceof MySQLPlatform) {
84-
$this->table->addOption('charset', 'utf8mb4');
87+
$table->addOption('charset', 'utf8mb4');
8588
}
8689

90+
// fix namespaced table name split for MSSQL/PostgreSQL
91+
// https://github.com/doctrine/dbal/blob/3.3.7/src/Schema/AbstractAsset.php#L55
92+
// https://github.com/doctrine/dbal/pull/5494
93+
\Closure::bind(function () use ($table, $tableName) {
94+
$table->_quoted = true;
95+
$lastDotPos = strrpos($tableName, '.');
96+
if ($lastDotPos !== false) {
97+
$table->_namespace = substr($tableName, 0, $lastDotPos);
98+
$table->_name = substr($tableName, $lastDotPos + 1);
99+
} else {
100+
$table->_namespace = null;
101+
$table->_name = $tableName;
102+
}
103+
}, null, Table::class)();
104+
105+
$this->table = $table;
106+
87107
return $this;
88108
}
89109

@@ -105,8 +125,19 @@ public function create(): self
105125

106126
public function drop(): self
107127
{
108-
$this->createSchemaManager()
109-
->dropTable($this->getDatabasePlatform()->quoteIdentifier($this->table->getName()));
128+
try {
129+
$this->createSchemaManager()
130+
->dropTable($this->table->getQuotedName($this->getDatabasePlatform()));
131+
} catch (DatabaseObjectNotFoundException $e) {
132+
// fix exception not converted to TableNotFoundException for MSSQL
133+
// https://github.com/doctrine/dbal/pull/5492
134+
if ($this->getDatabasePlatform() instanceof SQLServerPlatform && $e->getPrevious() instanceof DbalDriverException
135+
&& preg_match('~[cC]annot drop the table \'.*\', because it does not exist or you do not have permission\.~', $e->getMessage())) {
136+
throw new TableNotFoundException($e->getPrevious(), $e->getQuery());
137+
}
138+
139+
throw $e;
140+
}
110141

111142
$this->createdTableNames = array_diff($this->createdTableNames, [$this->table->getName()]);
112143

@@ -117,7 +148,7 @@ public function dropIfExists(): self
117148
{
118149
try {
119150
$this->drop();
120-
} catch (DbalException $e) {
151+
} catch (TableNotFoundException $e) {
121152
}
122153

123154
$this->createdTableNames = array_diff($this->createdTableNames, [$this->table->getName()]);
@@ -126,10 +157,14 @@ public function dropIfExists(): self
126157
// but if AI trigger is not present, AI sequence is not dropped
127158
// https://github.com/doctrine/dbal/issues/4997
128159
if ($this->getDatabasePlatform() instanceof OraclePlatform) {
129-
$dropTriggerSql = $this->getDatabasePlatform()->getDropAutoincrementSql($this->table->getName())[1];
160+
$schemaManager = $this->createSchemaManager();
161+
$dropTriggerSql = $this->getDatabasePlatform()
162+
->getDropAutoincrementSql($this->table->getQuotedName($this->getDatabasePlatform()))[1];
130163
try {
131-
$this->getConnection()->expr($dropTriggerSql)->executeStatement();
132-
} catch (Exception $e) {
164+
\Closure::bind(function () use ($schemaManager, $dropTriggerSql) {
165+
$schemaManager->_execSql($dropTriggerSql);
166+
}, null, AbstractSchemaManager::class)();
167+
} catch (DatabaseObjectNotFoundException $e) {
133168
}
134169
}
135170

src/Schema/TestCase.php

+6-1
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,11 @@ protected function setUp(): void
3535

3636
$this->db = Persistence::connect($_ENV['DB_DSN'], $_ENV['DB_USER'], $_ENV['DB_PASSWORD']);
3737

38+
if ($this->db->getDatabasePlatform() instanceof SqlitePlatform) {
39+
$this->db->getConnection()->expr(
40+
'PRAGMA foreign_keys = 1'
41+
)->executeStatement();
42+
}
3843
if ($this->db->getDatabasePlatform() instanceof MySQLPlatform) {
3944
$this->db->getConnection()->expr(
4045
'SET SESSION auto_increment_increment = 1, SESSION auto_increment_offset = 1'
@@ -103,7 +108,7 @@ private function convertSqlFromSqlite(string $sql): string
103108
$platform = $this->getDatabasePlatform();
104109

105110
$convertedSql = preg_replace_callback(
106-
'~\'(?:[^\'\\\\]+|\\\\.)*\'|"(?:[^"\\\\]+|\\\\.)*"|:(\w+)~s',
111+
'~\'(?:[^\'\\\\]+|\\\\.)*+\'|"(?:[^"\\\\]+|\\\\.)*+"|:(\w+)~s',
107112
function ($matches) use ($platform) {
108113
if (isset($matches[1])) {
109114
return ':' . ($platform instanceof OraclePlatform ? 'xxaaa' : '') . $matches[1];

tests/RandomTest.php

+4-13
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,7 @@
1212
use Atk4\Data\Persistence\Sql\Expression;
1313
use Atk4\Data\Persistence\Sql\Query;
1414
use Atk4\Data\Schema\TestCase;
15-
use Doctrine\DBAL\Platforms\OraclePlatform;
16-
use Doctrine\DBAL\Platforms\PostgreSQLPlatform;
1715
use Doctrine\DBAL\Platforms\SqlitePlatform;
18-
use Doctrine\DBAL\Platforms\SQLServerPlatform;
1916

2017
class Model_Rate extends Model
2118
{
@@ -570,19 +567,11 @@ public function testTableWithSchema(): void
570567
$runWithDb = false;
571568
} else {
572569
$dbSchema = $this->db->getConnection()->dsql()
573-
->field(new Expression($this->getDatabasePlatform()->getCurrentDatabaseExpression()))
570+
->field(new Expression($this->getDatabasePlatform()->getCurrentDatabaseExpression(true))) // @phpstan-ignore-line
574571
->getOne();
575572
$userSchema = $dbSchema;
576573
$docSchema = $dbSchema;
577574
$runWithDb = true;
578-
579-
if ($this->getDatabasePlatform() instanceof PostgreSQLPlatform
580-
|| $this->getDatabasePlatform() instanceof SQLServerPlatform
581-
|| $this->getDatabasePlatform() instanceof OraclePlatform) {
582-
$userSchema = 'functional_is_failing_db1';
583-
$docSchema = 'functional_is_failing_db2';
584-
$runWithDb = false;
585-
}
586575
}
587576

588577
$user = new Model($this->db, ['table' => $userSchema . '.user']);
@@ -600,8 +589,10 @@ public function testTableWithSchema(): void
600589
$this->assertSame($render, $selectAction->render());
601590
$this->assertSame($render, $doc->action('select')->render());
602591

592+
$userTableQuoted = '"' . str_replace('.', '"."', $userSchema) . '"."user"';
593+
$docTableQuoted = '"' . str_replace('.', '"."', $docSchema) . '"."doc"';
603594
$this->assertSameSql(
604-
'select "id", "name", "user_id", (select "name" from "' . $userSchema . '"."user" "_u_e8701ad48ba0" where "id" = "' . $docSchema . '"."doc"."user_id") "user" from "' . $docSchema . '"."doc" where (select "name" from "' . $userSchema . '"."user" "_u_e8701ad48ba0" where "id" = "' . $docSchema . '"."doc"."user_id") = :a',
595+
'select "id", "name", "user_id", (select "name" from ' . $userTableQuoted . ' "_u_e8701ad48ba0" where "id" = ' . $docTableQuoted . '."user_id") "user" from ' . $docTableQuoted . ' where (select "name" from ' . $userTableQuoted . ' "_u_e8701ad48ba0" where "id" = ' . $docTableQuoted . '."user_id") = :a',
605596
$render[0]
606597
);
607598

tests/Schema/BasicTest.php

-50
This file was deleted.

0 commit comments

Comments
 (0)