Completed
Pull Request — develop (#3570)
by Jonathan
155:39 queued 152:58
created

MultiTenantVisitor   A

Complexity

Total Complexity 15

Size/Duplication

Total Lines 113
Duplicated Lines 0 %

Test Coverage

Coverage 86.11%

Importance

Changes 0
Metric Value
wmc 15
eloc 27
dl 0
loc 113
ccs 31
cts 36
cp 0.8611
rs 10
c 0
b 0
f 0

8 Methods

Rating   Name   Duplication   Size   Complexity  
A acceptColumn() 0 2 1
A acceptTable() 0 22 3
A __construct() 0 5 2
A acceptIndex() 0 2 1
A acceptForeignKey() 0 2 1
A acceptSequence() 0 2 1
A acceptSchema() 0 2 1
A getClusteredIndex() 0 12 5
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Doctrine\DBAL\Sharding\SQLAzure\Schema;
6
7
use Doctrine\DBAL\Schema\Column;
8
use Doctrine\DBAL\Schema\ForeignKeyConstraint;
9
use Doctrine\DBAL\Schema\Index;
10
use Doctrine\DBAL\Schema\Schema;
11
use Doctrine\DBAL\Schema\Sequence;
12
use Doctrine\DBAL\Schema\Table;
13
use Doctrine\DBAL\Schema\Visitor\Visitor;
14
use RuntimeException;
15
use function in_array;
16
use function sprintf;
17
18
/**
19
 * Converts a single tenant schema into a multi-tenant schema for SQL Azure
20
 * Federations under the following assumptions:
21
 *
22
 * - Every table is part of the multi-tenant application, only explicitly
23
 *   excluded tables are non-federated. The behavior of the tables being in
24
 *   global or federated database is undefined. It depends on you selecting a
25
 *   federation before DDL statements or not.
26
 * - Every Primary key of a federated table is extended by another column
27
 *   'tenant_id' with a default value of the SQLAzure function
28
 *   `federation_filtering_value('tenant_id')`.
29
 * - You always have to work with `filtering=On` when using federations with this
30
 *   multi-tenant approach.
31
 * - Primary keys are either using globally unique ids (GUID, Table Generator)
32
 *   or you explicitly add the tenant_id in every UPDATE or DELETE statement
33
 *   (otherwise they will affect the same-id rows from other tenants as well).
34
 *   SQLAzure throws errors when you try to create IDENTIY columns on federated
35
 *   tables.
36
 */
37
class MultiTenantVisitor implements Visitor
38
{
39
    /** @var string[] */
40
    private $excludedTables = [];
41
42
    /** @var string */
43
    private $tenantColumnName;
44
45
    /** @var string */
46
    private $tenantColumnType = 'integer';
47
48
    /**
49
     * Name of the federation distribution, defaulting to the tenantColumnName
50
     * if not specified.
51
     *
52
     * @var string
53
     */
54
    private $distributionName;
55
56
    /**
57
     * @param string[]    $excludedTables
58
     * @param string      $tenantColumnName
59
     * @param string|null $distributionName
60
     */
61 46
    public function __construct(array $excludedTables = [], $tenantColumnName = 'tenant_id', $distributionName = null)
62
    {
63 46
        $this->excludedTables   = $excludedTables;
64 46
        $this->tenantColumnName = $tenantColumnName;
65 46
        $this->distributionName = $distributionName ?: $tenantColumnName;
66 46
    }
67
68
    /**
69
     * {@inheritdoc}
70
     */
71 46
    public function acceptTable(Table $table) : void
72
    {
73 46
        if (in_array($table->getName(), $this->excludedTables)) {
74
            return;
75
        }
76
77 46
        $table->addColumn($this->tenantColumnName, $this->tenantColumnType, [
78 46
            'default' => "federation_filtering_value('" . $this->distributionName . "')",
79
        ]);
80
81 46
        $clusteredIndex = $this->getClusteredIndex($table);
82
83 46
        $indexColumns   = $clusteredIndex->getColumns();
84 46
        $indexColumns[] = $this->tenantColumnName;
85
86 46
        if ($clusteredIndex->isPrimary()) {
87 46
            $table->dropPrimaryKey();
88 46
            $table->setPrimaryKey($indexColumns);
89
        } else {
90 23
            $table->dropIndex($clusteredIndex->getName());
91 23
            $table->addIndex($indexColumns, $clusteredIndex->getName());
92 23
            $table->getIndex($clusteredIndex->getName())->addFlag('clustered');
93
        }
94 46
    }
95
96
    /**
97
     * @param Table $table
98
     *
99
     * @return Index
100
     *
101
     * @throws RuntimeException
102
     */
103 46
    private function getClusteredIndex($table)
104
    {
105 46
        foreach ($table->getIndexes() as $index) {
106 46
            if ($index->isPrimary() && ! $index->hasFlag('nonclustered')) {
107 46
                return $index;
108
            }
109
110 23
            if ($index->hasFlag('clustered')) {
111 23
                return $index;
112
            }
113
        }
114
        throw new RuntimeException(sprintf('No clustered index found on table "%s".', $table->getName()));
115
    }
116
117
    /**
118
     * {@inheritdoc}
119
     */
120 46
    public function acceptSchema(Schema $schema) : void
121
    {
122 46
    }
123
124
    /**
125
     * {@inheritdoc}
126
     */
127 46
    public function acceptColumn(Table $table, Column $column) : void
128
    {
129 46
    }
130
131
    /**
132
     * {@inheritdoc}
133
     */
134
    public function acceptForeignKey(Table $localTable, ForeignKeyConstraint $fkConstraint) : void
135
    {
136
    }
137
138
    /**
139
     * {@inheritdoc}
140
     */
141 46
    public function acceptIndex(Table $table, Index $index) : void
142
    {
143 46
    }
144
145
    /**
146
     * {@inheritdoc}
147
     */
148
    public function acceptSequence(Sequence $sequence) : void
149
    {
150
    }
151
}
152