Skip to content

Commit d6dd0c4

Browse files
committed
SnowflakeTableReflection now works with views
1 parent 63b07c3 commit d6dd0c4

3 files changed

Lines changed: 160 additions & 61 deletions

File tree

src/Table/Snowflake/SnowflakeTableReflection.php

Lines changed: 94 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,13 @@
77
use Doctrine\DBAL\Connection;
88
use Keboola\TableBackendUtils\Column\ColumnCollection;
99
use Keboola\TableBackendUtils\Column\Snowflake\SnowflakeColumn;
10-
use Keboola\TableBackendUtils\Escaping\Exasol\ExasolQuote;
1110
use Keboola\TableBackendUtils\Escaping\Snowflake\SnowflakeQuote;
1211
use Keboola\TableBackendUtils\Table\TableDefinitionInterface;
1312
use Keboola\TableBackendUtils\Table\TableReflectionInterface;
1413
use Keboola\TableBackendUtils\Table\TableStats;
1514
use Keboola\TableBackendUtils\Table\TableStatsInterface;
1615
use Keboola\TableBackendUtils\TableNotExistsReflectionException;
16+
use RuntimeException;
1717
use Throwable;
1818

1919
final class SnowflakeTableReflection implements TableReflectionInterface
@@ -27,53 +27,87 @@ final class SnowflakeTableReflection implements TableReflectionInterface
2727

2828
private string $tableName;
2929

30+
private ?bool $isView = null;
31+
3032
private ?bool $isTemporary = null;
3133

34+
private ?int $sizeBytes = null;
35+
36+
private ?int $rowCount = null;
37+
3238
public function __construct(Connection $connection, string $schemaName, string $tableName)
3339
{
3440
$this->tableName = $tableName;
3541
$this->schemaName = $schemaName;
3642
$this->connection = $connection;
3743
}
3844

39-
private function setIsTemporary(): bool
45+
/**
46+
* @throws TableNotExistsReflectionException
47+
*/
48+
private function cacheTableProps(bool $force = false): void
4049
{
41-
$row = $this->connection->fetchAssociative(
50+
if ($force === false && $this->isView !== null && $this->isTemporary !== null) {
51+
return;
52+
}
53+
/** @var array<array{TABLE_TYPE:string,BYTES:string,ROW_COUNT:string}> $row */
54+
$row = $this->connection->fetchAllAssociative(
4255
sprintf(
43-
// STARTS WITH is added because it is case-sensitive
44-
'SHOW TABLES LIKE %s IN %s STARTS WITH %s ',
45-
SnowflakeQuote::quote($this->tableName),
46-
SnowflakeQuote::quoteSingleIdentifier($this->schemaName),
56+
//phpcs:ignore
57+
'SELECT TABLE_TYPE,BYTES,ROW_COUNT FROM information_schema.tables WHERE TABLE_SCHEMA = %s AND TABLE_NAME = %s;',
58+
SnowflakeQuote::quote($this->schemaName),
4759
SnowflakeQuote::quote($this->tableName)
4860
)
4961
);
50-
51-
if ($row) {
52-
return $row['kind'] === 'TEMPORARY';
62+
if (count($row) === 0) {
63+
throw TableNotExistsReflectionException::createForTable([$this->schemaName, $this->tableName]);
64+
}
65+
$this->sizeBytes = (int) $row[0]['BYTES'];
66+
$this->rowCount = (int) $row[0]['ROW_COUNT'];
67+
switch (strtoupper($row[0]['TABLE_TYPE'])) {
68+
case 'BASE TABLE':
69+
$this->isTemporary = false;
70+
$this->isView = false;
71+
return;
72+
case 'LOCAL TEMPORARY':
73+
case 'TEMPORARY TABLE':
74+
$this->isTemporary = true;
75+
$this->isView = false;
76+
return;
77+
case 'VIEW':
78+
$this->isTemporary = false;
79+
$this->isView = true;
80+
return;
81+
default:
82+
throw new RuntimeException(sprintf(
83+
'Table type "%s" is not known.',
84+
$row[0]['TABLE_TYPE']
85+
));
5386
}
54-
55-
throw new TableNotExistsReflectionException('Cannot detect if table is temporary or not. Table does not exist');
5687
}
5788

5889
/**
5990
* @return string[]
91+
* @throws TableNotExistsReflectionException
6092
*/
6193
public function getColumnsNames(): array
6294
{
63-
/** @var array<array{column_name: string}> $columnsData */
64-
$columnsData = $this->connection->fetchAllAssociative(
65-
sprintf(
66-
// case-sensitive
67-
'SHOW COLUMNS IN %s',
68-
SnowflakeQuote::createQuotedIdentifierFromParts([$this->schemaName, $this->tableName,])
69-
)
70-
);
95+
$columns = $this->getColumnsDefinitions();
7196

72-
return array_values(array_map(fn($column) => $column['column_name'], $columnsData));
97+
$names = [];
98+
/** @var SnowflakeColumn $col */
99+
foreach ($columns as $col) {
100+
$names[] = $col->getColumnName();
101+
}
102+
return $names;
73103
}
74104

105+
/**
106+
* @throws TableNotExistsReflectionException
107+
*/
75108
public function getColumnsDefinitions(): ColumnCollection
76109
{
110+
$this->cacheTableProps();
77111
/** @var array<array{
78112
* name: string,
79113
* kind: string,
@@ -99,28 +133,25 @@ public function getColumnsDefinitions(): ColumnCollection
99133
return new ColumnCollection($columns);
100134
}
101135

102-
103-
136+
/**
137+
* @throws TableNotExistsReflectionException
138+
*/
104139
public function getRowsCount(): int
105140
{
106-
/** @var int|string $result */
107-
$result = $this->connection->fetchOne(sprintf(
108-
'SELECT COUNT(*) AS NumberOfRows FROM %s',
109-
SnowflakeQuote::createQuotedIdentifierFromParts([
110-
$this->schemaName,
111-
$this->tableName,
112-
])
113-
));
114-
return (int) $result;
141+
$this->cacheTableProps(true);
142+
assert($this->rowCount !== null);
143+
return $this->rowCount;
115144
}
116145

117146
/**
118147
* returns list of column names where PK is defined on
119148
*
120149
* @return string[]
150+
* @throws TableNotExistsReflectionException
121151
*/
122152
public function getPrimaryKeysNames(): array
123153
{
154+
$this->cacheTableProps();
124155
/** @var array<array{column_name:string}> $columnsMeta */
125156
$columnsMeta = $this->connection->fetchAllAssociative(
126157
sprintf(
@@ -132,28 +163,24 @@ public function getPrimaryKeysNames(): array
132163
return array_map(fn($pkRow) => $pkRow['column_name'], $columnsMeta);
133164
}
134165

166+
/**
167+
* @throws TableNotExistsReflectionException
168+
*/
135169
public function getTableStats(): TableStatsInterface
136170
{
137-
$sql = sprintf(
138-
'SHOW TABLES LIKE %s IN SCHEMA %s STARTS WITH %s',
139-
ExasolQuote::quote($this->tableName),
140-
ExasolQuote::quoteSingleIdentifier($this->schemaName),
141-
ExasolQuote::quote($this->tableName)
142-
);
143-
/** @var array{bytes:int|string}|null $result */
144-
$result = $this->connection->fetchAssociative($sql);
145-
if (!$result) {
146-
throw new TableNotExistsReflectionException('Table does not exist');
147-
}
148-
149-
return new TableStats((int) $result['bytes'], $this->getRowsCount());
171+
$this->cacheTableProps(true);
172+
assert($this->sizeBytes !== null);
173+
assert($this->rowCount !== null);
174+
return new TableStats($this->sizeBytes, $this->rowCount);
150175
}
151176

177+
/**
178+
* @throws TableNotExistsReflectionException
179+
*/
152180
public function isTemporary(): bool
153181
{
154-
if ($this->isTemporary === null) {
155-
$this->isTemporary = $this->setIsTemporary();
156-
}
182+
$this->cacheTableProps();
183+
assert($this->isTemporary !== null);
157184
return $this->isTemporary;
158185
}
159186

@@ -224,14 +251,20 @@ public static function getDependentViewsForObject(
224251
* schema_name: string,
225252
* name: string
226253
* }[]
254+
* @throws TableNotExistsReflectionException
227255
*/
228256
public function getDependentViews(): array
229257
{
258+
$this->cacheTableProps();
230259
return self::getDependentViewsForObject($this->connection, $this->tableName, $this->schemaName, 'TABLE');
231260
}
232261

262+
/**
263+
* @throws TableNotExistsReflectionException
264+
*/
233265
public function getTableDefinition(): TableDefinitionInterface
234266
{
267+
$this->cacheTableProps();
235268
return new SnowflakeTableDefinition(
236269
$this->schemaName,
237270
$this->tableName,
@@ -243,18 +276,19 @@ public function getTableDefinition(): TableDefinitionInterface
243276

244277
public function exists(): bool
245278
{
246-
$row = $this->connection->fetchAssociative(
247-
sprintf(
248-
"SELECT *
249-
FROM information_schema.tables
250-
WHERE TABLE_TYPE = 'BASE TABLE'
251-
AND TABLE_NAME = %s AND TABLE_SCHEMA = %s
252-
",
253-
SnowflakeQuote::quote($this->tableName),
254-
SnowflakeQuote::quote($this->schemaName),
255-
)
256-
);
279+
try {
280+
$this->cacheTableProps(true);
281+
} catch (TableNotExistsReflectionException $e) {
282+
return false;
283+
}
284+
285+
return true;
286+
}
257287

258-
return $row !== false;
288+
public function isView(): bool
289+
{
290+
$this->cacheTableProps();
291+
assert($this->isView !== null);
292+
return $this->isView;
259293
}
260294
}

src/TableNotExistsReflectionException.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,20 @@
66

77
class TableNotExistsReflectionException extends ReflectionException
88
{
9+
/**
10+
* @param string[] $path
11+
*/
12+
public static function createForTable(array $path): self
13+
{
14+
return new self(sprintf(
15+
'Table %s does not exists.',
16+
implode(
17+
',',
18+
array_map(
19+
fn(string $item) => sprintf('"%s"', $item),
20+
$path
21+
)
22+
)
23+
));
24+
}
925
}

tests/Functional/Snowflake/Table/SnowflakeTableReflectionTest.php

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,15 @@ public function testGetTableColumnsNames(): void
3030
{
3131
$this->initTable();
3232
$ref = new SnowflakeTableReflection($this->connection, self::TEST_SCHEMA, self::TABLE_GENERIC);
33+
self::assertSame([
34+
'id',
35+
'first_name',
36+
'last_name',
37+
], $ref->getColumnsNames());
38+
39+
// same test on view
40+
$this->initView();
41+
$ref = new SnowflakeTableReflection($this->connection, self::TEST_SCHEMA, self::VIEW_GENERIC);
3342

3443
self::assertSame([
3544
'id',
@@ -42,7 +51,15 @@ public function testGetTableColumnsNamesCase(): void
4251
{
4352
$this->initTable();
4453
$ref = new SnowflakeTableReflection($this->connection, self::TEST_SCHEMA, self::TABLE_GENERIC);
54+
self::assertSame([
55+
'id',
56+
'first_name',
57+
'last_name',
58+
], $ref->getColumnsNames());
4559

60+
// same test on view
61+
$this->initView();
62+
$ref = new SnowflakeTableReflection($this->connection, self::TEST_SCHEMA, self::VIEW_GENERIC);
4663
self::assertSame([
4764
'id',
4865
'first_name',
@@ -62,6 +79,11 @@ public function testGetPrimaryKeysNames(): void
6279
);
6380
$ref = new SnowflakeTableReflection($this->connection, self::TEST_SCHEMA, self::TABLE_GENERIC);
6481
self::assertEquals(['id'], $ref->getPrimaryKeysNames());
82+
83+
// same test on view, view has no primary keys
84+
$this->initView();
85+
$ref = new SnowflakeTableReflection($this->connection, self::TEST_SCHEMA, self::VIEW_GENERIC);
86+
self::assertEquals([], $ref->getPrimaryKeysNames());
6587
}
6688

6789
public function testGetRowsCount(): void
@@ -77,6 +99,12 @@ public function testGetRowsCount(): void
7799
$this->insertRowToTable(self::TEST_SCHEMA, self::TABLE_GENERIC, ...$item);
78100
}
79101
self::assertEquals(2, $ref->getRowsCount());
102+
103+
// same test on view
104+
$this->initView();
105+
$ref = new SnowflakeTableReflection($this->connection, self::TEST_SCHEMA, self::VIEW_GENERIC);
106+
// view doesn't have rows count
107+
self::assertEquals(0, $ref->getRowsCount());
80108
}
81109

82110
public function testGetTableStatsWithWrongCase(): void
@@ -103,6 +131,15 @@ public function testGetTableStats(): void
103131
$stats2 = $ref->getTableStats();
104132
self::assertEquals(2, $stats2->getRowsCount());
105133
self::assertGreaterThan($stats1->getDataSizeBytes(), $stats2->getDataSizeBytes());
134+
135+
// same test on view
136+
$this->initView();
137+
$ref = new SnowflakeTableReflection($this->connection, self::TEST_SCHEMA, self::VIEW_GENERIC);
138+
/** @var TableStats $stats2 */
139+
$stats2 = $ref->getTableStats();
140+
// view doesn't have size or row count
141+
self::assertEquals(0, $stats2->getRowsCount());
142+
self::assertEquals(0, $stats2->getDataSizeBytes());
106143
}
107144

108145
/**
@@ -397,7 +434,6 @@ public function testGetDependentViews(): void
397434
], $dependentViews[0]);
398435
}
399436

400-
401437
public function testDependenciesWithCaseSensitivity(): void
402438
{
403439
$this->cleanSchema('TEST_UTIL_SCHEMA');
@@ -488,6 +524,7 @@ public function testDetectTempTable(): void
488524
// check temp table on normal table
489525
$ref = new SnowflakeTableReflection($this->connection, self::TEST_SCHEMA, self::TABLE_GENERIC);
490526
self::assertFalse($ref->isTemporary());
527+
self::assertFalse($ref->isView());
491528

492529
// check temp table on TEMP table
493530
$tableName = 'tableWhichDoesntLookLikeTemp';
@@ -500,6 +537,11 @@ public function testDetectTempTable(): void
500537
);
501538
$refTemp = new SnowflakeTableReflection($this->connection, self::TEST_SCHEMA, $tableName);
502539
self::assertTrue($refTemp->isTemporary());
540+
541+
$this->initView(self::VIEW_GENERIC, $tableName);
542+
$ref = new SnowflakeTableReflection($this->connection, self::TEST_SCHEMA, self::VIEW_GENERIC);
543+
self::assertFalse($ref->isTemporary());
544+
self::assertTrue($ref->isView());
503545
}
504546

505547
public function testDetectTempTableWithWrongCase(): void
@@ -536,6 +578,13 @@ public function testIfTableExists(): void
536578

537579
$ref = new SnowflakeTableReflection($this->connection, self::TEST_SCHEMA, self::TABLE_GENERIC);
538580
self::assertTrue($ref->exists());
581+
self::assertFalse($ref->isView());
582+
583+
// same test on view
584+
$this->initView();
585+
$ref = new SnowflakeTableReflection($this->connection, self::TEST_SCHEMA, self::VIEW_GENERIC);
586+
self::assertTrue($ref->exists());
587+
self::assertTrue($ref->isView());
539588
}
540589

541590
public function testIfSchemaDoesNotExists(): void

0 commit comments

Comments
 (0)