From 05703b08640158e691bbf31c7da7023f2dd474bc Mon Sep 17 00:00:00 2001 From: Matthew Brown Date: Mon, 8 May 2023 17:22:48 -0400 Subject: [PATCH] Convert table_schema type to TableSchema object (#100) * Convert table_schema type to TableSchema object * Remove unused use --- src/BuildSchemaCLI.php | 130 +++-- src/Column.hack | 13 + src/DataIntegrity.php | 115 +++-- src/Index.hack | 5 + src/Init.php | 2 +- src/Query/FromClause.php | 4 +- src/Query/JoinProcessor.php | 20 +- src/Query/Query.php | 4 +- src/QueryContext.php | 4 +- src/SchemaGenerator.php | 34 +- src/TableSchema.hack | 24 + src/Types.php | 35 -- src/VitessQueryValidator.php | 8 +- src/VitessSharding.hack | 5 + tests/GenerateSchemaTest.php | 143 ++---- tests/InsertQueryTest.php | 2 +- tests/SharedSetup.php | 646 +++++++------------------ tests/fixtures/expected_schema.codegen | 115 +++++ 18 files changed, 556 insertions(+), 753 deletions(-) create mode 100644 src/Column.hack create mode 100644 src/Index.hack create mode 100644 src/TableSchema.hack create mode 100644 src/VitessSharding.hack create mode 100644 tests/fixtures/expected_schema.codegen diff --git a/src/BuildSchemaCLI.php b/src/BuildSchemaCLI.php index dc77193..3a14443 100644 --- a/src/BuildSchemaCLI.php +++ b/src/BuildSchemaCLI.php @@ -5,18 +5,17 @@ use namespace HH\Lib\{C, Regex}; use type Facebook\CLILib\CLIWithArguments; use namespace Facebook\CLILib\CLIOptions; -use type Facebook\HackCodegen\{HackBuilderKeys, HackBuilderValues, HackCodegenConfig, HackCodegenFactory}; final class BuildSchemaCLI extends CLIWithArguments { - private string $constName = 'DB_SCHEMA'; + private string $functionName = 'get_db_schema'; <<__Override>> protected function getSupportedOptions(): vec { return vec[CLIOptions\with_required_string( $name ==> { - $this->constName = $name; + $this->functionName = $name; }, - 'The name of the constant to generate. Defaults to DB_SCHEMA', + 'The name of the function name to generate. Defaults to get_db_schema', '--name', )]; } @@ -67,43 +66,96 @@ protected function getSupportedOptions(): vec { $generated[$db] = $schema; } - $cg = new HackCodegenFactory(new HackCodegenConfig()); - - $generated = $cg->codegenConstant($this->constName) - ->setType('dict>') - ->setValue($generated, HackBuilderValues::dict(HackBuilderKeys::export(), HackBuilderValues::dict( - HackBuilderKeys::export(), - // special exporters are required to make shapes and enum values, ::export would turn them into arrays and strings - HackBuilderValues::shapeWithPerKeyRendering( - shape( - 'name' => HackBuilderValues::export(), - 'indexes' => - HackBuilderValues::vec(HackBuilderValues::shapeWithUniformRendering(HackBuilderValues::export())), - 'fields' => HackBuilderValues::vec(HackBuilderValues::shapeWithPerKeyRendering( - shape( - 'name' => HackBuilderValues::export(), - 'type' => HackBuilderValues::lambda(($_cfg, $str) ==> 'DataType::'.$str), - 'length' => HackBuilderValues::export(), - 'null' => HackBuilderValues::export(), - 'hack_type' => HackBuilderValues::export(), - 'default' => HackBuilderValues::export(), - 'unsigned' => HackBuilderValues::export(), - ), - )), - ), - ), - ))) - ->render(); - - $generated = <<functionName, $generated); await $terminal->getStdout()->writeAllAsync($generated); return 0; } + + // + // Write out a top level import target containing all of our generated db files. + // + // This also contains a memoized function that returns a lookup for each field in our DB tables and the type of those fields. + // + + public static function getRenderedHackTableSchemaWithClusters( + string $function_name, + dict> $table_schemas, + ): string { + $file_contents = ''; + + $file_contents .= "use type Slack\\SQLFake\\{Column, DataType, Index, TableSchema};\n"; + $file_contents .= "\n"; + $file_contents .= "<<__Memoize>>\n"; + $file_contents .= "function {$function_name}(): dict> {\n"; + $file_contents .= "\treturn dict[\n"; + foreach ($table_schemas as $cluster => $tables) { + $file_contents .= "\t\t'{$cluster}' => ".self::getRenderedHackTableSchema($tables, "\t\t"); + } + $file_contents .= "\t];\n"; + $file_contents .= "}\n"; + + return $file_contents; + } + + public static function getRenderedHackTableSchema( + dict $table_schemas, + string $indentation, + ): string { + $file_contents = "dict[\n"; + foreach ($table_schemas as $table_schema) { + $table_name = $table_schema->name; + $file_contents .= $indentation."\t'{$table_name}' => new TableSchema(\n"; + + // + // Write out the fields + // + + $file_contents .= $indentation."\t\t'{$table_name}',\n"; + $file_contents .= $indentation."\t\tvec[\n"; + foreach ($table_schema->fields as $field) { + $file_contents .= $indentation."\t\t\tnew Column(\n"; + $file_contents .= $indentation."\t\t\t\t'{$field->name}',\n"; + $file_contents .= $indentation."\t\t\t\tDataType::{$field->type},\n"; + $file_contents .= $indentation."\t\t\t\t{$field->length},\n"; + $file_contents .= $indentation."\t\t\t\t" . ($field->null ? 'true' : 'false') . ",\n"; + $file_contents .= $indentation."\t\t\t\t'{$field->hack_type}',\n"; + if ($field->unsigned is nonnull || $field->default is nonnull) { + if ($field->unsigned is nonnull) { + $file_contents .= $indentation."\t\t\t\t" . ($field->unsigned ? 'true' : 'false') . ",\n"; + } else { + $file_contents .= $indentation."\t\t\t\tnull,\n"; + } + if ($field->default is nonnull) { + $file_contents .= $indentation."\t\t\t\t'{$field->default}',\n"; + } + } + $file_contents .= $indentation."\t\t\t),\n"; + } + $file_contents .= $indentation."\t\t],\n"; + + // + // Write out the indexes + // + + $file_contents .= $indentation."\t\tvec[\n"; + foreach ($table_schema->indexes as $index) { + $file_contents .= $indentation."\t\t\tnew Index(\n"; + $file_contents .= $indentation."\t\t\t\t'{$index->name}',\n"; + $file_contents .= $indentation."\t\t\t\t'{$index->type}',\n"; + $fields = 'keyset[\''.\implode('\', \'', $index->fields).'\']'; + $file_contents .= $indentation."\t\t\t\t{$fields},\n"; + $file_contents .= $indentation."\t\t\t),\n"; + } + $file_contents .= $indentation."\t\t],\n"; + $file_contents .= $indentation."\t),\n"; + } + + $file_contents .= $indentation."],\n"; + return $file_contents; + } + + private static function varExportStringArray(Container $array): string { + return C\is_empty($array) ? 'vec[]' : 'vec[\''.\HH\Lib\Str\join($array, '\', \'').'\']'; + } } diff --git a/src/Column.hack b/src/Column.hack new file mode 100644 index 0000000..dc319d8 --- /dev/null +++ b/src/Column.hack @@ -0,0 +1,13 @@ +namespace Slack\SQLFake; + +final class Column { + public function __construct( + public string $name, + public DataType $type, + public int $length, + public bool $null, + public string $hack_type, + public ?bool $unsigned = null, + public ?string $default = null, + ) {} +} diff --git a/src/DataIntegrity.php b/src/DataIntegrity.php index 6758f13..c1d24bd 100644 --- a/src/DataIntegrity.php +++ b/src/DataIntegrity.php @@ -15,8 +15,8 @@ abstract final class DataIntegrity { <<__Memoize>> - public static function namesForSchema(table_schema $schema): keyset { - return Keyset\map($schema['fields'], $field ==> $field['name']); + public static function namesForSchema(TableSchema $schema): keyset { + return Keyset\map($schema->fields, $field ==> $field->name); } protected static function getDefaultValueForField( @@ -66,20 +66,25 @@ protected static function getDefaultValueForField( * Ensure all fields from the table schema are present in the row * Applies default values based on either DEFAULTs, nullable fields, or data types */ - public static function ensureFieldsPresent(dict $row, table_schema $schema): dict { + public static function ensureFieldsPresent(dict $row, TableSchema $schema): dict { - foreach ($schema['fields'] as $field) { - $field_name = $field['name']; - $field_type = $field['hack_type']; - $field_length = $field['length']; - $field_mysql_type = $field['type']; - $field_nullable = $field['null'] ?? false; - $field_default = $field['default'] ?? null; - $field_unsigned = $field['unsigned'] ?? false; + foreach ($schema->fields as $field) { + $field_name = $field->name; + $field_type = $field->hack_type; + $field_length = $field->length; + $field_mysql_type = $field->type; + $field_nullable = $field->null ?? false; + $field_default = $field->default; + $field_unsigned = $field->unsigned ?? false; if (!C\contains_key($row, $field_name)) { - $row[$field_name] = - self::getDefaultValueForField($field_type, $field_nullable, $field_default, $field_name, $schema['name']); + $row[$field_name] = self::getDefaultValueForField( + $field_type, + $field_nullable, + $field_default, + $field_name, + $schema->name, + ); } else if ($row[$field_name] === null) { if ($field_nullable) { // explicit null value and nulls are allowed, let it through @@ -87,10 +92,17 @@ public static function ensureFieldsPresent(dict $row, table_schem } else if (QueryContext::$strictSQLMode) { // if we got this far the column has no default and isn't nullable, strict would throw // but default MySQL mode would coerce to a valid value - throw new SQLFakeRuntimeException("Column '{$field_name}' on '{$schema['name']}' does not allow null values"); + throw new SQLFakeRuntimeException( + "Column '{$field_name}' on '{$schema->name}' does not allow null values", + ); } else { - $row[$field_name] = - self::getDefaultValueForField($field_type, $field_nullable, $field_default, $field_name, $schema['name']); + $row[$field_name] = self::getDefaultValueForField( + $field_type, + $field_nullable, + $field_default, + $field_name, + $schema->name, + ); } } else { // TODO more integrity constraints, check field length for varchars, check timestamps @@ -102,7 +114,8 @@ public static function ensureFieldsPresent(dict $row, table_schem if (QueryContext::$strictSQLMode) { $field_str = \var_export($row[$field_name], true); throw new SQLFakeRuntimeException( - "Invalid value {$field_str} for column '{$field_name}' on '{$schema['name']}', expected int", + "Invalid value {$field_str} for column '{$field_name}' on '{$schema + ->name}', expected int", ); } else { $row[$field_name] = (int)$row[$field_name]; @@ -118,7 +131,8 @@ public static function ensureFieldsPresent(dict $row, table_schem $field_value >= (($signed) ? \pow(2, 7) : \pow(2, 8)) ) { throw new SQLFakeRuntimeException( - "Column '{$field_name}' on '{$schema['name']}' expects a valid '{$field_mysql_type}'", + "Column '{$field_name}' on '{$schema + ->name}' expects a valid '{$field_mysql_type}'", ); } break; @@ -128,7 +142,8 @@ public static function ensureFieldsPresent(dict $row, table_schem $field_value >= (($signed) ? \pow(2, 15) : \pow(2, 16)) ) { throw new SQLFakeRuntimeException( - "Column '{$field_name}' on '{$schema['name']}' expects a valid '{$field_mysql_type}'", + "Column '{$field_name}' on '{$schema + ->name}' expects a valid '{$field_mysql_type}'", ); } break; @@ -138,7 +153,8 @@ public static function ensureFieldsPresent(dict $row, table_schem $field_value >= (($signed) ? \pow(2, 23) : \pow(2, 24)) ) { throw new SQLFakeRuntimeException( - "Column '{$field_name}' on '{$schema['name']}' expects a valid '{$field_mysql_type}'", + "Column '{$field_name}' on '{$schema + ->name}' expects a valid '{$field_mysql_type}'", ); } break; @@ -148,7 +164,8 @@ public static function ensureFieldsPresent(dict $row, table_schem $field_value >= (($signed) ? \pow(2, 31) : \pow(2, 32)) ) { throw new SQLFakeRuntimeException( - "Column '{$field_name}' on '{$schema['name']}' expects a valid '{$field_mysql_type}'", + "Column '{$field_name}' on '{$schema + ->name}' expects a valid '{$field_mysql_type}'", ); } break; @@ -158,13 +175,15 @@ public static function ensureFieldsPresent(dict $row, table_schem $field_value >= (($signed) ? \pow(2, 63) : \pow(2, 64)) ) { throw new SQLFakeRuntimeException( - "Column '{$field_name}' on '{$schema['name']}' expects a valid '{$field_mysql_type}'", + "Column '{$field_name}' on '{$schema + ->name}' expects a valid '{$field_mysql_type}'", ); } break; default: throw new SQLFakeRuntimeException( - "Column '{$field_name}' on '{$schema['name']}' expects a valid '{$field_mysql_type}'", + "Column '{$field_name}' on '{$schema + ->name}' expects a valid '{$field_mysql_type}'", ); break; } @@ -175,7 +194,8 @@ public static function ensureFieldsPresent(dict $row, table_schem if (QueryContext::$strictSQLMode) { $field_str = \var_export($row[$field_name], true); throw new SQLFakeRuntimeException( - "Invalid value '{$field_str}' for column '{$field_name}' on '{$schema['name']}', expected float", + "Invalid value '{$field_str}' for column '{$field_name}' on '{$schema + ->name}', expected float", ); } else { $row[$field_name] = (float)$row[$field_name]; @@ -187,7 +207,8 @@ public static function ensureFieldsPresent(dict $row, table_schem if (QueryContext::$strictSQLMode) { $field_str = \var_export($row[$field_name], true); throw new SQLFakeRuntimeException( - "Invalid value '{$field_str}' for column '{$field_name}' on '{$schema['name']}', expected string", + "Invalid value '{$field_str}' for column '{$field_name}' on '{$schema + ->name}', expected string", ); } else { $row[$field_name] = (string)$row[$field_name]; @@ -211,21 +232,24 @@ public static function ensureFieldsPresent(dict $row, table_schem } else { // invalid json throw new SQLFakeRuntimeException( - "Invalid value '{$field_value}' for column '{$field_name}' on '{$schema['name']}', expected json", + "Invalid value '{$field_value}' for column '{$field_name}' on '{$schema + ->name}', expected json", ); } } } else { // empty strings are not valid for json columns throw new SQLFakeRuntimeException( - "Invalid value '{$field_value}' for column '{$field_name}' on '{$schema['name']}', expected json", + "Invalid value '{$field_value}' for column '{$field_name}' on '{$schema + ->name}', expected json", ); } } } else if ($field_length > 0 && \mb_strlen($field_value) > $field_length) { $field_str = \var_export($row[$field_name], true); throw new SQLFakeRuntimeException( - "Invalid value '{$field_str}' for column '{$field_name}' on '{$schema['name']}', expected string of size {$field_length}", + "Invalid value '{$field_str}' for column '{$field_name}' on '{$schema + ->name}', expected string of size {$field_length}", ); } } @@ -240,23 +264,23 @@ public static function ensureFieldsPresent(dict $row, table_schem /** * Ensure default values are present, coerce data types as MySQL would */ - public static function coerceToSchema(dict $row, table_schema $schema): dict { + public static function coerceToSchema(dict $row, TableSchema $schema): dict { $fields = self::namesForSchema($schema); $bad_fields = Keyset\keys($row) |> Keyset\diff($$, $fields); if (!C\is_empty($bad_fields)) { $bad_fields = Str\join($bad_fields, ', '); - throw new SQLFakeRuntimeException("Column(s) '{$bad_fields}' not found on '{$schema['name']}'"); + throw new SQLFakeRuntimeException("Column(s) '{$bad_fields}' not found on '{$schema->name}'"); } $row = self::ensureFieldsPresent($row, $schema); - foreach ($schema['fields'] as $field) { - $field_name = $field['name']; - $field_type = $field['hack_type']; + foreach ($schema->fields as $field) { + $field_name = $field->name; + $field_type = $field->hack_type; // don't coerce null values on nullable fields - if ($field['null'] && $row[$field_name] === null) { + if ($field->null && $row[$field_name] === null) { continue; } @@ -289,17 +313,17 @@ public static function coerceToSchema(dict $row, table_schema $sc public static function checkUniqueConstraints( dataset $table, dict $row, - table_schema $schema, - ?int $update_row_id = null, + TableSchema $schema, + ?arraykey $update_row_id = null, ): ?(string, int) { // gather all unique keys $unique_keys = dict[]; - foreach ($schema['indexes'] as $index) { - if ($index['type'] === 'PRIMARY') { - $unique_keys['PRIMARY'] = keyset($index['fields']); - } else if ($index['type'] === 'UNIQUE') { - $unique_keys[$index['name']] = keyset($index['fields']); + foreach ($schema->indexes as $index) { + if ($index->type === 'PRIMARY') { + $unique_keys['PRIMARY'] = $index->fields; + } else if ($index->type === 'UNIQUE') { + $unique_keys[$index->name] = $index->fields; } } @@ -317,9 +341,12 @@ public static function checkUniqueConstraints( continue; } if (C\every($unique_key, $field ==> $r[$field] === $row[$field])) { - $dupe_unique_key_value = Vec\map($unique_key, $field ==> (string)$row[$field]) |> Str\join($$, ', '); - return - tuple("Duplicate entry '{$dupe_unique_key_value}' for key '{$name}' in table '{$schema['name']}'", $row_id); + $dupe_unique_key_value = Vec\map($unique_key, $field ==> (string)$row[$field]) + |> Str\join($$, ', '); + return tuple( + "Duplicate entry '{$dupe_unique_key_value}' for key '{$name}' in table '{$schema->name}'", + $row_id, + ); } } } diff --git a/src/Index.hack b/src/Index.hack new file mode 100644 index 0000000..9a8ffc1 --- /dev/null +++ b/src/Index.hack @@ -0,0 +1,5 @@ +namespace Slack\SQLFake; + +final class Index { + public function __construct(public string $name, public string $type, public keyset $fields) {} +} diff --git a/src/Init.php b/src/Init.php index fe65ffe..bb50b0d 100644 --- a/src/Init.php +++ b/src/Init.php @@ -17,7 +17,7 @@ * If strict mode is provided (recommended), SQLFake will throw an exception on any query referencing tables not in the schema. */ function init( - dict> $schema = dict[], + dict> $schema = dict[], bool $strict_sql = false, bool $strict_schema = false, ): void { diff --git a/src/Query/FromClause.php b/src/Query/FromClause.php index aa98c78..f09d77a 100644 --- a/src/Query/FromClause.php +++ b/src/Query/FromClause.php @@ -78,8 +78,8 @@ public function process(AsyncMysqlConnection $conn, string $sql): dataset { } else if ($schema is nonnull) { // if schema is set, order the fields in the right order on each row $ordered_fields = keyset[]; - foreach ($schema['fields'] as $field) { - $ordered_fields[] = $field['name']; + foreach ($schema->fields as $field) { + $ordered_fields[] = $field->name; } foreach ($res as $row) { diff --git a/src/Query/JoinProcessor.php b/src/Query/JoinProcessor.php index 169fc4f..3736a24 100644 --- a/src/Query/JoinProcessor.php +++ b/src/Query/JoinProcessor.php @@ -20,7 +20,7 @@ public static function process( JoinType $join_type, ?JoinOperator $_ref_type, ?Expression $ref_clause, - ?table_schema $right_schema, + ?TableSchema $right_schema, ): dataset { // MySQL supports JOIN (inner), LEFT OUTER JOIN, RIGHT OUTER JOIN, and implicitly CROSS JOIN (which uses commas), NATURAL @@ -76,8 +76,8 @@ public static function process( // this null placeholder row is merged into the data set for that row $null_placeholder = dict[]; if ($right_schema !== null) { - foreach ($right_schema['fields'] as $field) { - $null_placeholder["{$right_table_name}.{$field['name']}"] = null; + foreach ($right_schema->fields as $field) { + $null_placeholder["{$right_table_name}.{$field->name}"] = null; } } @@ -109,8 +109,8 @@ public static function process( $null_placeholder = dict[]; if ($right_schema !== null) { - foreach ($right_schema['fields'] as $field) { - $null_placeholder["{$right_table_name}.{$field['name']}"] = null; + foreach ($right_schema->fields as $field) { + $null_placeholder["{$right_table_name}.{$field->name}"] = null; } } @@ -180,7 +180,9 @@ protected static function buildNaturalJoinFilter(dataset $left_dataset, dataset // MySQL actually doesn't throw if there's no matching columns, but I think we can take the liberty to assume it's not what you meant to do and throw here if ($filter === null) { - throw new SQLFakeParseException('NATURAL join keyword was used with tables that do not share any column names'); + throw new SQLFakeParseException( + 'NATURAL join keyword was used with tables that do not share any column names', + ); } return $filter; @@ -235,7 +237,7 @@ private static function processHashJoin( JoinType $join_type, ?JoinOperator $_ref_type, BinaryOperatorExpression $filter, - ?table_schema $right_schema, + ?TableSchema $right_schema, ): dataset { $left = $filter->left as ColumnExpression; $right = $filter->right as ColumnExpression; @@ -273,8 +275,8 @@ private static function processHashJoin( // this null placeholder row is merged into the data set for that row $null_placeholder = dict[]; if ($right_schema !== null) { - foreach ($right_schema['fields'] as $field) { - $null_placeholder["{$right_table_name}.{$field['name']}"] = null; + foreach ($right_schema->fields as $field) { + $null_placeholder["{$right_table_name}.{$field->name}"] = null; } } diff --git a/src/Query/Query.php b/src/Query/Query.php index f4c6e38..8e7a0ea 100644 --- a/src/Query/Query.php +++ b/src/Query/Query.php @@ -149,7 +149,7 @@ protected function applySet( dataset $filtered_rows, dataset $original_table, vec $set_clause, - ?table_schema $table_schema, + ?TableSchema $table_schema, /* for dupe inserts only */ ?row $values = null, ): (int, vec>) { @@ -158,7 +158,7 @@ protected function applySet( $valid_fields = null; if ($table_schema !== null) { - $valid_fields = Keyset\map($table_schema['fields'], $field ==> $field['name']); + $valid_fields = Keyset\map($table_schema->fields, $field ==> $field->name); } $set_clauses = vec[]; diff --git a/src/QueryContext.php b/src/QueryContext.php index da1ec22..e4a7d9f 100644 --- a/src/QueryContext.php +++ b/src/QueryContext.php @@ -45,9 +45,9 @@ * servers with the same name but different schemas. We don't include server hostnames here * because it's common to have sharded databases with the same names on different hosts */ - public static dict> $schema = dict[]; + public static dict> $schema = dict[]; - public static function getSchema(string $database, string $table): ?table_schema { + public static function getSchema(string $database, string $table): ?TableSchema { return self::$schema[$database][$table] ?? null; } } diff --git a/src/SchemaGenerator.php b/src/SchemaGenerator.php index 647e903..0c53f25 100644 --- a/src/SchemaGenerator.php +++ b/src/SchemaGenerator.php @@ -9,44 +9,40 @@ final class SchemaGenerator { /** * Pass SQL schema as a string */ - public function generateFromString(string $sql): dict { + public function generateFromString(string $sql): dict { $parser = new CreateTableParser(); $schema = $parser->parse($sql); $tables = dict[]; foreach ($schema as $table => $s) { - $table_generated_schema = shape( - 'name' => $s['name'], - 'fields' => vec[], - 'indexes' => vec[], - ); + $table_generated_schema = new TableSchema($s['name']); foreach ($s['fields'] as $field) { - $f = shape( - 'name' => $field['name'], - 'type' => $field['type'] as DataType, - 'length' => (int)($field['length'] ?? 0), - 'null' => $field['null'] ?? true, - 'hack_type' => $this->sqlToHackFieldType($field), + $f = new Column( + $field['name'], + $field['type'] as DataType, + (int)($field['length'] ?? 0), + $field['null'] ?? true, + $this->sqlToHackFieldType($field), ); $default = ($field['default'] ?? null); if ($default is nonnull && $default !== 'NULL') { - $f['default'] = $default; + $f->default = $default; } $unsigned = ($field['unsigned'] ?? null); if ($unsigned is nonnull) { - $f['unsigned'] = $unsigned; + $f->unsigned = $unsigned; } - $table_generated_schema['fields'][] = $f; + $table_generated_schema->fields[] = $f; } foreach ($s['indexes'] as $index) { - $table_generated_schema['indexes'][] = shape( - 'name' => $index['name'] ?? $index['type'], - 'type' => $index['type'], - 'fields' => Keyset\map($index['cols'], $col ==> $col['name']), + $table_generated_schema->indexes[] = new Index( + $index['name'] ?? $index['type'], + $index['type'], + Keyset\map($index['cols'], $col ==> $col['name']), ); } diff --git a/src/TableSchema.hack b/src/TableSchema.hack new file mode 100644 index 0000000..4890707 --- /dev/null +++ b/src/TableSchema.hack @@ -0,0 +1,24 @@ +namespace Slack\SQLFake; + +/** + * A simple representation of a table schema, used to make the application smarter. + * This allows SQL Fake to provide fully typed rows, validate that columns exist, + * enforce primary key constraints, check if indexes would be used, and more + */ +final class TableSchema implements IMemoizeParam { + public function __construct( + public string $name, + public vec $fields = vec[], + public vec $indexes = vec[], + public ?VitessSharding $vitess_sharding = null, + ) {} + + public function getInstanceKey(): string { + return $this->name; + } + + public function getPrimaryKeyColumns(): keyset { + $primary = \HH\Lib\Vec\filter($this->indexes, $index ==> $index->name === 'PRIMARY')[0]; + return $primary->fields; + } +} diff --git a/src/Types.php b/src/Types.php index 645e06b..7d880e5 100644 --- a/src/Types.php +++ b/src/Types.php @@ -91,41 +91,6 @@ enum MultiOperand: string { type order_by_clause = vec Expression, 'direction' => SortDirection)>; -/** - * A simple representation of a table schema, used to make the application smarter. - * This allows SQL Fake to provide fully typed rows, validate that columns exist, - * enforce primary key constraints, check if indexes would be used, and more - */ -type table_schema = shape( - - /** - * Table name as it exists in the database - */ - 'name' => string, - 'fields' => Container< - shape( - 'name' => string, - 'type' => DataType, - 'length' => int, - 'null' => bool, - 'hack_type' => string, - ?'unsigned' => bool, - ?'default' => string, - ), - >, - 'indexes' => Container< - shape( - 'name' => string, - 'type' => string, - 'fields' => Container, - ), - >, - ?'vitess_sharding' => shape( - 'keyspace' => string, - 'sharding_key' => string, - ), -); - enum DataType: string { TINYINT = 'TINYINT'; SMALLINT = 'SMALLINT'; diff --git a/src/VitessQueryValidator.php b/src/VitessQueryValidator.php index e8e69c6..2b9df06 100644 --- a/src/VitessQueryValidator.php +++ b/src/VitessQueryValidator.php @@ -86,7 +86,7 @@ public function getHandlers(): dict)> { list($database, $table_name) = Query::parseTableName($this->conn, $this->query->updateClause['name']); $table_schema = QueryContext::getSchema($database, $table_name); - $vitess_sharding = $table_schema['vitess_sharding'] ?? null; + $vitess_sharding = $table_schema?->vitess_sharding; if ($vitess_sharding === null) { // This could either be an unsharded table or a misconfiguration. @@ -96,7 +96,7 @@ public function getHandlers(): dict)> { $columns = VitessQueryValidator::extractColumnExprNames($set); - if (C\contains_key($columns, $vitess_sharding['sharding_key'])) { + if (C\contains_key($columns, $vitess_sharding->sharding_key)) { throw new SQLFakeVitessQueryViolation( Str\format('Vitess query validation error: %s', UnsupportedCases::PRIMARY_VINDEX_COLUMN), ); @@ -132,7 +132,7 @@ private function isCrossShardQuery(): bool { $where = $this->query->whereClause; list($database, $table_name) = Query::parseTableName($this->conn, $table['name']); $table_schema = QueryContext::getSchema($database, $table_name); - $vitess_sharding = $table_schema['vitess_sharding'] ?? null; + $vitess_sharding = $table_schema?->vitess_sharding ?? null; // if we don't have a sharding scheme defined, assume it isn't cross shard if ($vitess_sharding === null) { $is_scatter_query = false; @@ -142,7 +142,7 @@ private function isCrossShardQuery(): bool { if ($where is BinaryOperatorExpression) { $columns = VitessQueryValidator::extractColumnExprNames($where->traverse()); // if the where clause selects on the sharding key, we're good to go - if (C\contains_key($columns, $vitess_sharding['sharding_key'])) { + if (C\contains_key($columns, $vitess_sharding->sharding_key)) { // TODO: for now this just returns but we need to eventually // handle cases like JOINS where multiple tables are involved return false; diff --git a/src/VitessSharding.hack b/src/VitessSharding.hack new file mode 100644 index 0000000..3d922a3 --- /dev/null +++ b/src/VitessSharding.hack @@ -0,0 +1,5 @@ +namespace Slack\SQLFake; + +class VitessSharding { + public function __construct(public string $keyspace, public string $sharding_key) {} +} diff --git a/tests/GenerateSchemaTest.php b/tests/GenerateSchemaTest.php index e5994b7..bcb3c2e 100644 --- a/tests/GenerateSchemaTest.php +++ b/tests/GenerateSchemaTest.php @@ -9,121 +9,38 @@ final class GenerateSchemaTest extends HackTest { public async function testGenerateSchema(): Awaitable { $expected = dict[ - 'test' => shape( - 'name' => 'test', - 'fields' => vec[ - shape( - 'name' => 'id', - 'type' => 'VARCHAR', - 'length' => 255, - 'null' => false, - 'hack_type' => 'string', - ), - shape( - 'name' => 'value', - 'type' => 'VARCHAR', - 'length' => 255, - 'null' => false, - 'hack_type' => 'string', - ), + 'test' => new TableSchema( + 'test', + vec[ + new Column('id', DataType::VARCHAR, 255, false, 'string'), + new Column('value', DataType::VARCHAR, 255, false, 'string'), ], - 'indexes' => vec[ - shape( - 'name' => 'PRIMARY', - 'type' => 'PRIMARY', - 'fields' => dict[ - 'id' => 'id', - ], - ), + vec[ + new Index('PRIMARY', 'PRIMARY', keyset['id']), ], ), - 'test2' => shape( - 'name' => 'test2', - 'fields' => vec[ - shape( - 'name' => 'id', - 'type' => 'BIGINT', - 'length' => 20, - 'null' => false, - 'hack_type' => 'int', - 'unsigned' => true, - ), - shape( - 'name' => 'name', - 'type' => 'VARCHAR', - 'length' => 100, - 'null' => false, - 'hack_type' => 'string', - ), + 'test2' => new TableSchema( + 'test2', + vec[ + new Column('id', DataType::BIGINT, 20, false, 'int', true), + new Column('name', DataType::VARCHAR, 100, false, 'string'), ], - 'indexes' => vec[ - shape( - 'name' => 'PRIMARY', - 'type' => 'PRIMARY', - 'fields' => dict[ - 'id' => 'id', - 'name' => 'name', - ], - ), - shape( - 'name' => 'name', - 'type' => 'INDEX', - 'fields' => dict[ - 'name' => 'name', - ], - ), + vec[ + new Index('PRIMARY', 'PRIMARY', keyset['id', 'name']), + new Index('name', 'INDEX', keyset['name']), ], ), - 'test3' => shape( - 'name' => 'test3', - 'fields' => vec[ - shape( - 'name' => 'id', - 'type' => 'BIGINT', - 'length' => 20, - 'null' => false, - 'hack_type' => 'int', - 'unsigned' => true, - ), - shape( - 'name' => 'ch', - 'type' => 'CHAR', - 'length' => 64, - 'null' => true, - 'hack_type' => 'string', - ), - shape( - 'name' => 'deleted', - 'type' => 'TINYINT', - 'length' => 3, - 'null' => false, - 'hack_type' => 'int', - 'default' => '0', - 'unsigned' => true, - ), - shape( - 'name' => 'name', - 'type' => 'VARCHAR', - 'length' => 100, - 'null' => false, - 'hack_type' => 'string', - ), + 'test3' => new TableSchema( + 'test3', + vec[ + new Column('id', DataType::BIGINT, 20, false, 'int', true), + new Column('ch', DataType::CHAR, 64, true, 'string'), + new Column('deleted', DataType::TINYINT, 3, false, 'int', true, '0'), + new Column('name', DataType::VARCHAR, 100, false, 'string'), ], - 'indexes' => vec[ - shape( - 'name' => 'PRIMARY', - 'type' => 'PRIMARY', - 'fields' => dict[ - 'id' => 'id', - ], - ), - shape( - 'name' => 'name', - 'type' => 'UNIQUE', - 'fields' => dict[ - 'name' => 'name', - ], - ), + vec[ + new Index('PRIMARY', 'PRIMARY', keyset['id']), + new Index('name', 'UNIQUE', keyset['name']), ], ), ]; @@ -131,8 +48,12 @@ final class GenerateSchemaTest extends HackTest { $generator = new SchemaGenerator(); $sql = \file_get_contents(__DIR__.'/fixtures/SchemaExample.sql'); $schema = $generator->generateFromString($sql); - expect($schema['test'])->toHaveSameShapeAs($expected['test']); - expect($schema['test2'])->toHaveSameShapeAs($expected['test2']); - expect($schema['test3'])->toHaveSameShapeAs($expected['test3']); + expect(\var_export($schema['test'], true))->toEqual(\var_export($expected['test'], true)); + expect(\var_export($schema['test2'], true))->toEqual(\var_export($expected['test2'], true)); + expect(\var_export($schema['test3'], true))->toEqual(\var_export($expected['test3'], true)); + $expected_schema = \file_get_contents(__DIR__.'/fixtures/expected_schema.codegen'); + $string_schema = + BuildSchemaCLI::getRenderedHackTableSchemaWithClusters('get_my_table_schemas', dict['prod' => $schema]); + expect($string_schema)->toBeSame($expected_schema); } } diff --git a/tests/InsertQueryTest.php b/tests/InsertQueryTest.php index 2782131..678f836 100644 --- a/tests/InsertQueryTest.php +++ b/tests/InsertQueryTest.php @@ -11,7 +11,7 @@ final class InsertQueryTest extends HackTest { <<__Override>> public static async function beforeFirstTestAsync(): Awaitable { - init(TEST_SCHEMA, true); + init(get_test_schema(), true); $pool = new AsyncMysqlConnectionPool(darray[]); static::$conn = await $pool->connect('example', 1, 'db1', '', ''); // black hole logging diff --git a/tests/SharedSetup.php b/tests/SharedSetup.php index c8b91fb..43a29ea 100644 --- a/tests/SharedSetup.php +++ b/tests/SharedSetup.php @@ -4,7 +4,7 @@ final class SharedSetup { public static async function initAsync(): Awaitable { - $schema = TEST_SCHEMA; + $schema = get_test_schema(); init($schema, true); $pool = new AsyncMysqlConnectionPool(darray[]); @@ -55,7 +55,7 @@ final class SharedSetup { } public static async function initVitessAsync(): Awaitable { - $schema = VITESS_TEST_SCHEMA; + $schema = get_vitess_test_schema(); init($schema, true); $pool = new AsyncMysqlConnectionPool(darray[]); @@ -91,491 +91,169 @@ final class SharedSetup { } -const dict> TEST_SCHEMA = dict[ - 'db1' => dict[ - 'table1' => shape( - 'name' => 'table1', - 'fields' => vec[ - shape( - 'name' => 'id', - 'type' => DataType::BIGINT, - 'length' => 20, - 'null' => false, - 'hack_type' => 'int', - ), - shape( - 'name' => 'name', - 'type' => DataType::VARCHAR, - 'length' => 255, - 'null' => false, - 'hack_type' => 'string', - ), - ], - 'indexes' => vec[ - shape( - 'name' => 'PRIMARY', - 'type' => 'PRIMARY', - 'fields' => keyset['id'], - ), - shape( - 'name' => 'name_uniq', - 'type' => 'UNIQUE', - 'fields' => keyset['name'], - ), - ], - ), - 'table2' => shape( - 'name' => 'table2', - 'fields' => vec[ - shape( - 'name' => 'id', - 'type' => DataType::BIGINT, - 'length' => 20, - 'null' => false, - 'hack_type' => 'int', - ), - shape( - 'name' => 'table_1_id', - 'type' => DataType::BIGINT, - 'length' => 20, - 'null' => false, - 'hack_type' => 'int', - ), - shape( - 'name' => 'description', - 'type' => DataType::VARCHAR, - 'length' => 255, - 'null' => false, - 'hack_type' => 'string', - ), - ], - 'indexes' => vec[ - shape( - 'name' => 'PRIMARY', - 'type' => 'PRIMARY', - 'fields' => keyset['id'], - ), - shape( - 'name' => 'table_1_id', - 'type' => 'INDEX', - 'fields' => keyset['table_1_id'], - ), - ], - ), - 'table_with_json' => shape( - 'name' => 'table_with_json', - 'fields' => vec[ - shape( - 'name' => 'id', - 'type' => DataType::BIGINT, - 'length' => 20, - 'null' => false, - 'hack_type' => 'int', - ), - shape( - 'name' => 'data', - 'type' => DataType::JSON, - 'length' => 255, - 'null' => true, - 'hack_type' => 'string', - ), - ], - 'indexes' => vec[ - shape( - 'name' => 'PRIMARY', - 'type' => 'PRIMARY', - 'fields' => keyset['id'], - ), - ], - ), - 'table_with_more_fields' => shape( - 'name' => 'table_with_more_fields', - 'fields' => vec[ - shape( - 'name' => 'id', - 'type' => DataType::BIGINT, - 'length' => 20, - 'null' => false, - 'hack_type' => 'int', - ), - shape( - 'name' => 'name', - 'type' => DataType::VARCHAR, - 'length' => 255, - 'null' => false, - 'hack_type' => 'string', - ), - shape( - 'name' => 'nullable_unique', - 'type' => DataType::VARCHAR, - 'length' => 255, - 'null' => true, - 'hack_type' => 'string', - ), - shape( - 'name' => 'nullable_default', - 'type' => DataType::INT, - 'length' => 20, - 'null' => true, - 'hack_type' => 'int', - 'default' => '1', - ), - shape( - 'name' => 'not_null_default', - 'type' => DataType::INT, - 'length' => 20, - 'null' => false, - 'hack_type' => 'int', - 'default' => '2', - ), - ], - 'indexes' => vec[ - shape( - 'name' => 'PRIMARY', - 'type' => 'PRIMARY', - 'fields' => keyset['id', 'name'], - ), - shape( - 'name' => 'nullable_unique', - 'type' => 'UNIQUE', - 'fields' => keyset['nullable_unique'], - ), - ], - ), - ], - 'db2' => dict[ - 'table3' => shape( - 'name' => 'table3', - 'fields' => vec[ - shape( - 'name' => 'id', - 'type' => DataType::BIGINT, - 'length' => 20, - 'null' => false, - 'hack_type' => 'int', - ), - shape( - 'name' => 'group_id', - 'type' => DataType::BIGINT, - 'length' => 20, - 'null' => false, - 'hack_type' => 'int', - ), - shape( - 'name' => 'name', - 'type' => DataType::VARCHAR, - 'length' => 255, - 'null' => false, - 'hack_type' => 'string', - ), - ], - 'indexes' => vec[ - shape( - 'name' => 'PRIMARY', - 'type' => 'PRIMARY', - 'fields' => keyset['id'], - ), - shape( - 'name' => 'name_uniq', - 'type' => 'UNIQUE', - 'fields' => keyset['name'], - ), - shape( - 'name' => 'group_id', - 'type' => 'INDEX', - 'fields' => keyset['group_id'], - ), - ], - ), - 'table4' => shape( - 'name' => 'table4', - 'fields' => vec[ - shape( - 'name' => 'id', - 'type' => DataType::BIGINT, - 'length' => 20, - 'null' => false, - 'hack_type' => 'int', - ), - shape( - 'name' => 'group_id', - 'type' => DataType::BIGINT, - 'length' => 20, - 'null' => false, - 'hack_type' => 'int', - ), - shape( - 'name' => 'description', - 'type' => DataType::VARCHAR, - 'length' => 255, - 'null' => false, - 'hack_type' => 'string', - ), - ], - 'indexes' => vec[ - shape( - 'name' => 'PRIMARY', - 'type' => 'PRIMARY', - 'fields' => keyset['id'], - ), - shape( - 'name' => 'group_id', - 'type' => 'INDEX', - 'fields' => keyset['group_id'], - ), - ], - ), - 'table5' => shape( - 'name' => 'table5', - 'fields' => vec[ - shape( - 'name' => 'id', - 'type' => DataType::BIGINT, - 'length' => 20, - 'null' => false, - 'hack_type' => 'int', - ), - shape( - 'name' => 'test_type', - 'type' => DataType::INT, - 'length' => 16, - 'null' => false, - 'hack_type' => 'int', - ), - shape( - 'name' => 'description', - 'type' => DataType::VARCHAR, - 'length' => 255, - 'null' => false, - 'hack_type' => 'string', - ), - ], - 'indexes' => vec[ - shape( - 'name' => 'PRIMARY', - 'type' => 'PRIMARY', - 'fields' => keyset['id'], - ), - shape( - 'name' => 'test_type', - 'type' => 'INDEX', - 'fields' => keyset['test_type'], - ), - ], - ), - 'table6' => shape( - 'name' => 'table6', - 'fields' => vec[ - shape( - 'name' => 'id', - 'type' => DataType::BIGINT, - 'length' => 20, - 'null' => false, - 'hack_type' => 'int', - ), - shape( - 'name' => 'position', - 'type' => DataType::VARCHAR, - 'length' => 255, - 'null' => false, - 'hack_type' => 'string', - ), - ], - 'indexes' => vec[ - shape( - 'name' => 'PRIMARY', - 'type' => 'PRIMARY', - 'fields' => keyset['id'], - ), - ], - ), - 'association_table' => shape( - 'name' => 'association_table', - 'fields' => vec[ - shape( - 'name' => 'table_3_id', - 'type' => DataType::BIGINT, - 'length' => 20, - 'null' => false, - 'hack_type' => 'int', - ), - shape( - 'name' => 'table_4_id', - 'type' => DataType::BIGINT, - 'length' => 20, - 'null' => false, - 'hack_type' => 'int', - ), - shape( - 'name' => 'description', - 'type' => DataType::VARCHAR, - 'length' => 255, - 'null' => false, - 'hack_type' => 'string', - ), - shape( - 'name' => 'group_id', - 'type' => DataType::BIGINT, - 'length' => 20, - 'null' => false, - 'hack_type' => 'int', - ), - ], - 'indexes' => vec[ - shape( - 'name' => 'PRIMARY', - 'type' => 'PRIMARY', - 'fields' => keyset['table_3_id', 'table_4_id'], - ), - shape( - 'name' => 'table_4_id', - 'type' => 'INDEX', - 'fields' => keyset['table_4_id'], - ), - ], - ), - ], +<<__Memoize>> +function get_test_schema(): dict> { + return dict[ + 'db1' => dict[ + 'table1' => new TableSchema( + 'table1', + vec[ + new Column('id', DataType::BIGINT, 20, false, 'int'), + new Column('name', DataType::VARCHAR, 255, false, 'string'), + ], + vec[ + new Index('PRIMARY', 'PRIMARY', keyset['id']), + new Index('name_uniq', 'UNIQUE', keyset['name']), + ], + ), + 'table2' => new TableSchema( + 'table2', + vec[ + new Column('id', DataType::BIGINT, 20, false, 'int'), + new Column('table_1_id', DataType::BIGINT, 20, false, 'int'), + new Column('description', DataType::VARCHAR, 255, false, 'string'), + ], + vec[ + new Index('PRIMARY', 'PRIMARY', keyset['id']), + new Index('table_1_id', 'INDEX', keyset['table_1_id']), + ], + ), + 'table_with_json' => new TableSchema( + 'table_with_json', + vec[ + new Column('id', DataType::BIGINT, 20, false, 'int'), + new Column('data', DataType::JSON, 255, true, 'string'), + ], + vec[ + new Index('PRIMARY', 'PRIMARY', keyset['id']), + ], + ), + 'table_with_more_fields' => new TableSchema( + 'table_with_more_fields', + vec[ + new Column('id', DataType::BIGINT, 20, false, 'int'), + new Column('name', DataType::VARCHAR, 255, false, 'string'), + new Column('nullable_unique', DataType::VARCHAR, 255, true, 'string'), + new Column('nullable_default', DataType::INT, 20, true, 'int', null, '1'), + new Column('not_null_default', DataType::INT, 20, false, 'int', null, '2'), + ], + vec[ + new Index('PRIMARY', 'PRIMARY', keyset['id', 'name']), + new Index('nullable_unique', 'UNIQUE', keyset['nullable_unique']), + ], + ), + ], + 'db2' => dict[ + 'table3' => new TableSchema( + 'table3', + vec[ + new Column('id', DataType::BIGINT, 20, false, 'int'), + new Column('group_id', DataType::BIGINT, 20, false, 'int'), + new Column('name', DataType::VARCHAR, 255, false, 'string'), + ], + vec[ + new Index('PRIMARY', 'PRIMARY', keyset['id']), + new Index('name_uniq', 'UNIQUE', keyset['name']), + new Index('group_id', 'INDEX', keyset['group_id']), + ], + ), + 'table4' => new TableSchema( + 'table4', + vec[ + new Column('id', DataType::BIGINT, 20, false, 'int'), + new Column('group_id', DataType::BIGINT, 20, false, 'int'), + new Column('description', DataType::VARCHAR, 255, false, 'string'), + ], + vec[ + new Index('PRIMARY', 'PRIMARY', keyset['id']), + new Index('group_id', 'INDEX', keyset['group_id']), + ], + ), + 'table5' => new TableSchema( + 'table5', + vec[ + new Column('id', DataType::BIGINT, 20, false, 'int'), + new Column('test_type', DataType::INT, 16, false, 'int'), + new Column('description', DataType::VARCHAR, 255, false, 'string'), + ], + vec[ + new Index('PRIMARY', 'PRIMARY', keyset['id']), + new Index('test_type', 'INDEX', keyset['test_type']), + ], + ), + 'table6' => new TableSchema( + 'table6', + vec[ + new Column('id', DataType::BIGINT, 20, false, 'int'), + new Column('position', DataType::VARCHAR, 255, false, 'string'), + ], + vec[ + new Index('PRIMARY', 'PRIMARY', keyset['id']), + ], + ), + 'association_table' => new TableSchema( + 'association_table', + vec[ + new Column('table_3_id', DataType::BIGINT, 20, false, 'int'), + new Column('table_4_id', DataType::BIGINT, 20, false, 'int'), + new Column('description', DataType::VARCHAR, 255, false, 'string'), + new Column('group_id', DataType::BIGINT, 20, false, 'int'), + ], + vec[ + new Index('PRIMARY', 'PRIMARY', keyset['table_3_id', 'table_4_id']), + new Index('table_4_id', 'INDEX', keyset['table_4_id']), + ], + ), + ], -]; + ]; +} -const dict> VITESS_TEST_SCHEMA = dict[ - 'vitess' => dict[ - 'vt_table1' => shape( - 'name' => 'vt_table1', - 'fields' => vec[ - shape( - 'name' => 'id', - 'type' => DataType::BIGINT, - 'length' => 20, - 'null' => false, - 'hack_type' => 'int', - ), - shape( - 'name' => 'name', - 'type' => DataType::VARCHAR, - 'length' => 255, - 'null' => false, - 'hack_type' => 'string', - ), - ], - 'indexes' => vec[ - shape( - 'name' => 'PRIMARY', - 'type' => 'PRIMARY', - 'fields' => keyset['id'], - ), - shape( - 'name' => 'name_uniq', - 'type' => 'UNIQUE', - 'fields' => keyset['name'], - ), - ], - 'vitess_sharding' => shape( - 'keyspace' => 'test_keyspace_one', - 'sharding_key' => 'id', - ), - ), - 'vt_table2' => shape( - 'name' => 'vt_table2', - 'fields' => vec[ - shape( - 'name' => 'id', - 'type' => DataType::BIGINT, - 'length' => 20, - 'null' => false, - 'hack_type' => 'int', - ), - shape( - 'name' => 'vt_table1_id', - 'type' => DataType::BIGINT, - 'length' => 20, - 'null' => false, - 'hack_type' => 'int', - ), - shape( - 'name' => 'description', - 'type' => DataType::VARCHAR, - 'length' => 255, - 'null' => false, - 'hack_type' => 'string', - ), - ], - 'indexes' => vec[ - shape( - 'name' => 'PRIMARY', - 'type' => 'PRIMARY', - 'fields' => keyset['id'], - ), - shape( - 'name' => 'table_1_id', - 'type' => 'INDEX', - 'fields' => keyset['vt_table1_id'], - ), - ], - 'vitess_sharding' => shape( - 'keyspace' => 'test_keyspace_one', - 'sharding_key' => 'vt_table1_id', +function get_vitess_test_schema(): dict> { + return dict[ + 'vitess' => dict[ + 'vt_table1' => new TableSchema( + 'vt_table1', + vec[ + new Column('id', DataType::BIGINT, 20, false, 'int'), + new Column('name', DataType::VARCHAR, 255, false, 'string'), + ], + vec[ + new Index('PRIMARY', 'PRIMARY', keyset['id']), + new Index('name_uniq', 'UNIQUE', keyset['name']), + ], + new VitessSharding('test_keyspace_one', 'id'), ), + 'vt_table2' => new TableSchema( + 'vt_table2', + vec[ + new Column('id', DataType::BIGINT, 20, false, 'int'), + new Column('vt_table1_id', DataType::BIGINT, 20, false, 'int'), + new Column('description', DataType::VARCHAR, 255, false, 'string'), + ], + vec[ + new Index('PRIMARY', 'PRIMARY', keyset['id']), + new Index('table_1_id', 'INDEX', keyset['vt_table1_id']), + ], + new VitessSharding('test_keyspace_one', 'vt_table1_id'), - ), - 'vt_table_with_more_fields' => shape( - 'name' => 'vt_table_with_more_fields', - 'fields' => vec[ - shape( - 'name' => 'id', - 'type' => DataType::BIGINT, - 'length' => 20, - 'null' => false, - 'hack_type' => 'int', - ), - shape( - 'name' => 'name', - 'type' => DataType::VARCHAR, - 'length' => 255, - 'null' => false, - 'hack_type' => 'string', - ), - shape( - 'name' => 'nullable_unique', - 'type' => DataType::VARCHAR, - 'length' => 255, - 'null' => true, - 'hack_type' => 'string', - ), - shape( - 'name' => 'nullable_default', - 'type' => DataType::INT, - 'length' => 20, - 'null' => true, - 'hack_type' => 'int', - 'default' => '1', - ), - shape( - 'name' => 'not_null_default', - 'type' => DataType::INT, - 'length' => 20, - 'null' => false, - 'hack_type' => 'int', - 'default' => '2', - ), - ], - 'indexes' => vec[ - shape( - 'name' => 'PRIMARY', - 'type' => 'PRIMARY', - 'fields' => keyset['id', 'name'], - ), - shape( - 'name' => 'nullable_unique', - 'type' => 'UNIQUE', - 'fields' => keyset['nullable_unique'], - ), - ], - 'vitess_sharding' => shape( - 'keyspace' => 'test_keyspace_two', - 'sharding_key' => 'name', ), + 'vt_table_with_more_fields' => new TableSchema( + 'vt_table_with_more_fields', + vec[ + new Column('id', DataType::BIGINT, 20, false, 'int'), + new Column('name', DataType::VARCHAR, 255, false, 'string'), + new Column('nullable_unique', DataType::VARCHAR, 255, true, 'string'), + new Column('nullable_default', DataType::INT, 20, true, 'int', null, '1'), + new Column('not_null_default', DataType::INT, 20, false, 'int', null, '2'), + ], + vec[ + new Index('PRIMARY', 'PRIMARY', keyset['id', 'name']), + new Index('nullable_unique', 'UNIQUE', keyset['nullable_unique']), + ], + new VitessSharding('test_keyspace_two', 'name'), - ), - ], -]; + ), + ], + ]; +} diff --git a/tests/fixtures/expected_schema.codegen b/tests/fixtures/expected_schema.codegen new file mode 100644 index 0000000..9a52238 --- /dev/null +++ b/tests/fixtures/expected_schema.codegen @@ -0,0 +1,115 @@ +use type Slack\SQLFake\{Column, DataType, Index, TableSchema}; + +<<__Memoize>> +function get_my_table_schemas(): dict> { + return dict[ + 'prod' => dict[ + 'test' => new TableSchema( + 'test', + vec[ + new Column( + 'id', + DataType::VARCHAR, + 255, + false, + 'string', + ), + new Column( + 'value', + DataType::VARCHAR, + 255, + false, + 'string', + ), + ], + vec[ + new Index( + 'PRIMARY', + 'PRIMARY', + keyset['id'], + ), + ], + ), + 'test2' => new TableSchema( + 'test2', + vec[ + new Column( + 'id', + DataType::BIGINT, + 20, + false, + 'int', + true, + ), + new Column( + 'name', + DataType::VARCHAR, + 100, + false, + 'string', + ), + ], + vec[ + new Index( + 'PRIMARY', + 'PRIMARY', + keyset['id', 'name'], + ), + new Index( + 'name', + 'INDEX', + keyset['name'], + ), + ], + ), + 'test3' => new TableSchema( + 'test3', + vec[ + new Column( + 'id', + DataType::BIGINT, + 20, + false, + 'int', + true, + ), + new Column( + 'ch', + DataType::CHAR, + 64, + true, + 'string', + ), + new Column( + 'deleted', + DataType::TINYINT, + 3, + false, + 'int', + true, + '0', + ), + new Column( + 'name', + DataType::VARCHAR, + 100, + false, + 'string', + ), + ], + vec[ + new Index( + 'PRIMARY', + 'PRIMARY', + keyset['id'], + ), + new Index( + 'name', + 'UNIQUE', + keyset['name'], + ), + ], + ), + ], + ]; +}