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