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