Lines 54.90% 28 / 51
Methods 50.00% 3 / 6
Classes 0.00% 0 / 1
Covered by tests of size
Name Lines Methods CRAP
 __construct 100.00% 2 / 2 100.00% 1 / 1 1
 getSchema 100.00% 1 / 1 100.00% 1 / 1 1
 createTables 88.88% 16 / 18 0.00% 0 / 1 10.14
 createAndUseSchema 27.77% 5 / 18 0.00% 0 / 1 19.56
 dropTables 0.00% 0 / 8 0.00% 0 / 1 20
 executeSql 100.00% 4 / 4 100.00% 1 / 1 2
33class SchemaInstaller implements DriverInterface
34{
35    private readonly DialectInterface $dialect;
36
37    private readonly DatabaseSchema $schema;
38
39    /** @var string[] Collected SQL for dry-run */
40    public array $collectedSql = [];
41
42    public bool $dryRun = false;
43
44    public function __construct(
45        private readonly Configuration $configuration,
46        ?DialectInterface $dialect = null,
47    ) {
48        $this->dialect = $dialect ?? DialectFactory::create();
49        $this->schema = new DatabaseSchema($this->dialect);
50    }
51
52    /**
53     * Returns the DatabaseSchema instance.
54     */
55    public function getSchema(): DatabaseSchema
56    {
57        return $this->schema;
58    }
59
60    /**
61     * Executes all CREATE TABLE and CREATE INDEX statements.
62     *
63     * @param string $prefix Table prefix to apply. The previous prefix is restored after execution.
64     * @param string|null $schema Schema or database name for schema/database-based tenant isolation.
65     *                            For MySQL: creates and switches to a database.
66     *                            For PostgreSQL: creates and switches to a schema.
67     */
68    public function createTables(string $prefix = '', ?string $schema = null): bool
69    {
70        $previousPrefix = Database::getTablePrefix();
71
72        if ($prefix !== '') {
73            Database::setTablePrefix($prefix);
74        }
75
76        $this->collectedSql = [];
77
78        try {
79            if ($schema !== null && $schema !== '') {
80                if (!$this->createAndUseSchema($schema)) {
81                    return false;
82                }
83            }
84
85            foreach ($this->schema->getAllTables() as $tableBuilder) {
86                $createTableSql = $tableBuilder->build();
87
88                if (!$this->executeSql($createTableSql)) {
89                    return false;
90                }
91
92                foreach ($tableBuilder->buildIndexStatements() as $indexSql) {
93                    if ($this->executeSql($indexSql)) {
94                        continue;
95                    }
96
97                    return false;
98                }
99            }
100
101            return true;
102        } finally {
103            if ($prefix !== '') {
104                Database::setTablePrefix($previousPrefix ?? '');
105            }
106        }
107    }
108
109    /**
110     * Creates a schema/database and switches to it.
111     *
112     * For MySQL: CREATE DATABASE + USE.
113     * For PostgreSQL: CREATE SCHEMA + SET search_path.
114     */
115    private function createAndUseSchema(string $schema): bool
116    {
117        $dialectClass = $this->dialect::class;
118
119        if (str_contains($dialectClass, 'Mysql')) {
120            return (
121                $this->executeSql(sprintf('CREATE DATABASE IF NOT EXISTS `%s`', $schema))
122                && $this->executeSql(sprintf('USE `%s`', $schema))
123            );
124        }
125
126        if (str_contains($dialectClass, 'Pgsql')) {
127            return (
128                $this->executeSql(sprintf('CREATE SCHEMA IF NOT EXISTS "%s"', $schema))
129                && $this->executeSql(sprintf('SET search_path TO "%s"', $schema))
130            );
131        }
132
133        if (str_contains($dialectClass, 'Sqlsrv')) {
134            return $this->executeSql(sprintf(
135                "IF NOT EXISTS (SELECT * FROM sys.schemas WHERE name = '%s') EXEC('CREATE SCHEMA [%s]')",
136                $schema,
137                $schema,
138            ));
139        }
140
141        return true;
142    }
143
144    /**
145     * Executes all DROP TABLE statements for the schema tables.
146     */
147    public function dropTables(string $prefix = ''): bool
148    {
149        if ($prefix === '') {
150            $prefix = Database::getTablePrefix();
151        }
152
153        foreach ($this->schema->getTableNames() as $tableName) {
154            $sql = sprintf('DROP TABLE %s%s', $prefix, $tableName);
155            $result = $this->configuration->getDb()->query($sql);
156
157            if (!$result) {
158                return false;
159            }
160        }
161
162        return true;
163    }
164
165    private function executeSql(string $sql): bool
166    {
167        $this->collectedSql[] = $sql;
168
169        if ($this->dryRun) {
170            return true;
171        }
172
173        return (bool) $this->configuration->getDb()->query($sql);
174    }
175}