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 */
35class MultiTenantVisitor implements Visitor
36{
37    /** @var string[] */
38    private $excludedTables = [];
39
40    /** @var string */
41    private $tenantColumnName;
42
43    /** @var string */
44    private $tenantColumnType = 'integer';
45
46    /**
47     * Name of the federation distribution, defaulting to the tenantColumnName
48     * if not specified.
49     *
50     * @var string
51     */
52    private $distributionName;
53
54    /**
55     * @param string[]    $excludedTables
56     * @param string      $tenantColumnName
57     * @param string|null $distributionName
58     */
59    public function __construct(array $excludedTables = [], $tenantColumnName = 'tenant_id', $distributionName = null)
60    {
61        $this->excludedTables   = $excludedTables;
62        $this->tenantColumnName = $tenantColumnName;
63        $this->distributionName = $distributionName ?: $tenantColumnName;
64    }
65
66    /**
67     * {@inheritdoc}
68     */
69    public function acceptTable(Table $table)
70    {
71        if (in_array($table->getName(), $this->excludedTables)) {
72            return;
73        }
74
75        $table->addColumn($this->tenantColumnName, $this->tenantColumnType, [
76            'default' => "federation_filtering_value('" . $this->distributionName . "')",
77        ]);
78
79        $clusteredIndex = $this->getClusteredIndex($table);
80
81        $indexColumns   = $clusteredIndex->getColumns();
82        $indexColumns[] = $this->tenantColumnName;
83
84        if ($clusteredIndex->isPrimary()) {
85            $table->dropPrimaryKey();
86            $table->setPrimaryKey($indexColumns);
87        } else {
88            $table->dropIndex($clusteredIndex->getName());
89            $table->addIndex($indexColumns, $clusteredIndex->getName());
90            $table->getIndex($clusteredIndex->getName())->addFlag('clustered');
91        }
92    }
93
94    /**
95     * @param Table $table
96     *
97     * @return Index
98     *
99     * @throws RuntimeException
100     */
101    private function getClusteredIndex($table)
102    {
103        foreach ($table->getIndexes() as $index) {
104            if ($index->isPrimary() && ! $index->hasFlag('nonclustered')) {
105                return $index;
106            }
107
108            if ($index->hasFlag('clustered')) {
109                return $index;
110            }
111        }
112
113        throw new RuntimeException('No clustered index found on table ' . $table->getName());
114    }
115
116    /**
117     * {@inheritdoc}
118     */
119    public function acceptSchema(Schema $schema)
120    {
121    }
122
123    /**
124     * {@inheritdoc}
125     */
126    public function acceptColumn(Table $table, Column $column)
127    {
128    }
129
130    /**
131     * {@inheritdoc}
132     */
133    public function acceptForeignKey(Table $localTable, ForeignKeyConstraint $fkConstraint)
134    {
135    }
136
137    /**
138     * {@inheritdoc}
139     */
140    public function acceptIndex(Table $table, Index $index)
141    {
142    }
143
144    /**
145     * {@inheritdoc}
146     */
147    public function acceptSequence(Sequence $sequence)
148    {
149    }
150}
151