1: <?php
2:
3: declare(strict_types=1);
4:
5: namespace Atk4\Data\Persistence\Sql\Oracle;
6:
7: use Doctrine\DBAL\Platforms\OraclePlatform;
8: use Doctrine\DBAL\Schema\AbstractAsset;
9: use Doctrine\DBAL\Schema\Index;
10: use Doctrine\DBAL\Schema\Sequence;
11: use Doctrine\DBAL\Schema\Table;
12: use Doctrine\DBAL\Schema\UniqueConstraint;
13:
14: trait PlatformTrait
15: {
16: #[\Override]
17: public function getVarcharTypeDeclarationSQL(array $column)
18: {
19: $column['length'] = ($column['length'] ?? 255) * 4;
20:
21: return parent::getVarcharTypeDeclarationSQL($column);
22: }
23:
24: // Oracle database requires explicit conversion when using binary column,
25: // workaround by using a standard non-binary column with custom encoding/typecast
26:
27: #[\Override]
28: public function getBinaryTypeDeclarationSQL(array $column)
29: {
30: $lengthEncodedAscii = ($column['length'] ?? 255) * 2 + strlen('atk__binary__u5f8mzx4vsm8g2c9__' . hash('crc32b', ''));
31: $column['length'] = intdiv($lengthEncodedAscii + 3, 4);
32:
33: return $this->getVarcharTypeDeclarationSQL($column);
34: }
35:
36: #[\Override]
37: public function getBlobTypeDeclarationSQL(array $column)
38: {
39: return $this->getClobTypeDeclarationSQL($column);
40: }
41:
42: // TODO create DBAL PR
43: #[\Override]
44: public function getFloatDeclarationSQL(array $column)
45: {
46: return 'BINARY_DOUBLE';
47: }
48:
49: // TODO test DBAL DB diff for each supported Field type
50: // then fix using https://github.com/doctrine/dbal/issues/5194#issuecomment-1018790220
51: /* protected function initializeCommentedDoctrineTypes()
52: {
53: parent::initializeCommentedDoctrineTypes();
54:
55: $this->markDoctrineTypeCommented('binary');
56: $this->markDoctrineTypeCommented('blob');
57: } */
58:
59: // Oracle DBAL platform autoincrement implementation does not increment like
60: // Sqlite or MySQL does, unify the behaviour
61:
62: #[\Override]
63: public function getCreateSequenceSQL(Sequence $sequence)
64: {
65: $sequence->setCache(1);
66:
67: return parent::getCreateSequenceSQL($sequence);
68: }
69:
70: #[\Override]
71: public function getCreateAutoincrementSql($name, $table, $start = 1)
72: {
73: $sqls = parent::getCreateAutoincrementSql($name, $table, $start);
74:
75: // fix table name when name /w schema is used
76: // TODO submit a PR with fixed OraclePlatform to DBAL
77: $sqls[0] = preg_replace('~(?<=WHERE TABLE_NAME = \').+\.(?=.+?\')~', '', $sqls[0]);
78:
79: // replace trigger from https://github.com/doctrine/dbal/blob/3.1.3/src/Platforms/OraclePlatform.php#L526-L546
80: $tableIdentifier = \Closure::bind(fn () => $this->normalizeIdentifier($table), $this, OraclePlatform::class)();
81: $nameIdentifier = \Closure::bind(fn () => $this->normalizeIdentifier($name), $this, OraclePlatform::class)();
82: $aiTriggerName = \Closure::bind(fn () => $this->getAutoincrementIdentifierName($tableIdentifier), $this, OraclePlatform::class)();
83: $aiSequenceName = $this->getIdentitySequenceName($tableIdentifier->getQuotedName($this), $nameIdentifier->getQuotedName($this));
84: assert(str_starts_with($sqls[count($sqls) - 1], 'CREATE TRIGGER ' . $aiTriggerName . "\n"));
85:
86: $pkSeq = \Closure::bind(fn () => $this->normalizeIdentifier($aiSequenceName), $this, OraclePlatform::class)()->getName();
87: $sqls[count($sqls) - 1] = (new Expression(
88: // else branch should be maybe (because of concurrency) put into after update trigger
89: str_replace('[pk_seq]', '\'' . str_replace('\'', '\'\'', $pkSeq) . '\'', <<<'EOF'
90: CREATE TRIGGER {{trigger}}
91: BEFORE INSERT OR UPDATE
92: ON {{table}}
93: FOR EACH ROW
94: DECLARE
95: atk4__pk_seq_last__ {{table}}.{pk}%TYPE;
96: BEGIN
97: IF (:NEW.{pk} IS NULL) THEN
98: SELECT {{pk_seq}}.NEXTVAL INTO :NEW.{pk} FROM DUAL;
99: ELSE
100: SELECT LAST_NUMBER INTO atk4__pk_seq_last__ FROM USER_SEQUENCES WHERE SEQUENCE_NAME = [pk_seq];
101: WHILE atk4__pk_seq_last__ <= :NEW.{pk}
102: LOOP
103: SELECT {{pk_seq}}.NEXTVAL + 1 INTO atk4__pk_seq_last__ FROM DUAL;
104: END LOOP;
105: END IF;
106: END;
107: EOF),
108: [
109: 'trigger' => \Closure::bind(fn () => $this->normalizeIdentifier($aiTriggerName), $this, OraclePlatform::class)()->getName(),
110: 'table' => $tableIdentifier->getName(),
111: 'pk' => $nameIdentifier->getName(),
112: 'pk_seq' => $pkSeq,
113: ]
114: ))->render()[0];
115:
116: return $sqls;
117: }
118:
119: #[\Override]
120: public function getCreateIndexSQL(Index $index, $table)
121: {
122: // workaround https://github.com/doctrine/dbal/issues/5508
123: // no side effect on multiple null values or DBAL index list observed
124: if ($index->isUnique()) {
125: $uniqueConstraint = new UniqueConstraint(
126: '0.0',
127: ['0.0'],
128: $index->getFlags(),
129: $index->getOptions()
130: );
131: \Closure::bind(static function () use ($index, $uniqueConstraint) {
132: $uniqueConstraint->_name = $index->_name;
133: $uniqueConstraint->_namespace = $index->_namespace;
134: $uniqueConstraint->_quoted = $index->_quoted;
135: $uniqueConstraint->columns = $index->_columns;
136: }, null, AbstractAsset::class)();
137:
138: $tableName = $table instanceof Table ? $table->getQuotedName($this) : $table;
139:
140: return $this->getCreateUniqueConstraintSQL($uniqueConstraint, $tableName);
141: }
142:
143: return parent::getCreateIndexSQL($index, $table);
144: }
145:
146: #[\Override]
147: public function getListDatabasesSQL(): string
148: {
149: // ignore Oracle maintained schemas, improve tests performance
150: // self::getListTablesSQL() is never used, thanks to https://github.com/doctrine/dbal/pull/5268 replaced by OracleSchemaManager::selectTableNames()
151: // self::getListViewsSQL() does not need filtering, as there is no Oracle VIEW by default
152: return 'SELECT username FROM sys.all_users'
153: . ' WHERE oracle_maintained = \'N\'';
154: }
155: }
156: