Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SQL Schema construction #16

Merged
merged 6 commits into from
Apr 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions DbSeeder/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,18 @@
{
const string sqlScript =
"""
CREATE TABLE users
CREATE TABLE profiles
(
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(122) NOT NULL UNIQUE,
profile_id INT,
FOREIGN KEY (profile_id) REFERENCES profiles(id)
nickname VARCHAR(122) NOT NULL UNIQUE
);

CREATE TABLE profiles
CREATE TABLE users
(
id INT AUTO_INCREMENT PRIMARY KEY,
nickname VARCHAR(122) NOT NULL UNIQUE
name VARCHAR(122) NOT NULL UNIQUE,
profile_id INT,
FOREIGN KEY (profile_id) REFERENCES profiles(id)
);
""";
var lexer = new SqlLexer(sqlScript);
Expand Down Expand Up @@ -56,7 +56,7 @@

private static void PrintSyntaxTree(SyntaxTreeNode? node, string indent = "")
{
Console.WriteLine($"{indent}{node.Value} --> ({node.Type})");

Check warning on line 59 in DbSeeder/Program.cs

View workflow job for this annotation

GitHub Actions / main

Dereference of a possibly null reference.

Check warning on line 59 in DbSeeder/Program.cs

View workflow job for this annotation

GitHub Actions / main

Dereference of a possibly null reference.

Check warning on line 59 in DbSeeder/Program.cs

View workflow job for this annotation

GitHub Actions / main

Dereference of a possibly null reference.

Check warning on line 59 in DbSeeder/Program.cs

View workflow job for this annotation

GitHub Actions / main

Dereference of a possibly null reference.
foreach (var child in node.Children)
{
PrintSyntaxTree(child, indent + " ");
Expand Down
47 changes: 42 additions & 5 deletions DbSeeder/Schema/Column.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,46 @@
namespace DbSeeder.Schema;

public class Column(string name, string dataType, string dataTypeConstraint, string[] constraints)
public record Column
{
public string Name { get; } = name;
public string DataType { get; } = dataType;
public string DataTypeConstraint { get; set; } = dataTypeConstraint;
public string[] Constraints { get; set; } = constraints;
public Column(string name,
string dataType,
string dataTypeConstraint,
string[] constraints)
{
Name = name;
DataType = dataType;
DataTypeConstraint = dataTypeConstraint;

constraints = constraints.Select(c => c.ToLower()).ToArray();
IsAutoIncrement = constraints.Contains("auto_increment", StringComparer.OrdinalIgnoreCase);
IsNotNull = constraints.Contains("not null", StringComparer.OrdinalIgnoreCase);
IsUnique = constraints.Contains("unique", StringComparer.OrdinalIgnoreCase);
IsPrimaryKey = constraints.Contains("primary key", StringComparer.OrdinalIgnoreCase);

var fkConstraint = constraints.FirstOrDefault(

Check warning on line 20 in DbSeeder/Schema/Column.cs

View workflow job for this annotation

GitHub Actions / main

"Array.Find" static method should be used instead of the "FirstOrDefault" extension method. (https://rules.sonarsource.com/csharp/RSPEC-6602)
c => c.StartsWith("foreign key", StringComparison.OrdinalIgnoreCase));
IsForeignKey = fkConstraint is not null;

if (IsForeignKey)
{
var refTableAndCol = fkConstraint![12..].Split("|");

var refTableName = refTableAndCol[0];
var refColumnName = refTableAndCol[1];

ForeignKeyRef = new ForeignKeyRef(refTableName, refColumnName);
}
}

public string Name { get; }
public string DataType { get; }
public string DataTypeConstraint { get; }

public bool IsAutoIncrement { get; }
public bool IsPrimaryKey { get; }
public bool IsForeignKey { get; }
public bool IsNotNull { get; }
public bool IsUnique { get; }

public ForeignKeyRef? ForeignKeyRef { get; }
}
7 changes: 7 additions & 0 deletions DbSeeder/Schema/ForeignKey.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace DbSeeder.Schema;

public record ForeignKey(
Table Table,
Column Column,
Table RefTable,
Column RefColumn);
5 changes: 5 additions & 0 deletions DbSeeder/Schema/ForeignKeyRef.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
namespace DbSeeder.Schema;

public record ForeignKeyRef(
string TableName,
string ColumnName);
5 changes: 5 additions & 0 deletions DbSeeder/Schema/PrimaryKey.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
namespace DbSeeder.Schema;

public record PrimaryKey(
Table Table,
Column Column);
8 changes: 5 additions & 3 deletions DbSeeder/Schema/SqlSchema.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@
public class SqlSchema
{
private readonly List<Table> _tables = [];

public IReadOnlyList<Table> Tables => _tables;

public void AddTable(Table table)
{
_tables.Add(table);
}
=> _tables.Add(table);

public Table? GetTableByName(string tableName)
=> _tables.FirstOrDefault(t => t.Name.Equals(tableName, StringComparison.OrdinalIgnoreCase));

Check warning on line 13 in DbSeeder/Schema/SqlSchema.cs

View workflow job for this annotation

GitHub Actions / main

"Find" method should be used instead of the "FirstOrDefault" extension method. (https://rules.sonarsource.com/csharp/RSPEC-6602)
}
11 changes: 5 additions & 6 deletions DbSeeder/Schema/SqlSchemaBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
return _sqlSchema;
}

public void Visit(SyntaxTreeNode? rootNode)

Check warning on line 16 in DbSeeder/Schema/SqlSchemaBuilder.cs

View workflow job for this annotation

GitHub Actions / main

Rename parameter 'rootNode' to 'node' to match the interface declaration. (https://rules.sonarsource.com/csharp/RSPEC-927)

Check warning on line 16 in DbSeeder/Schema/SqlSchemaBuilder.cs

View workflow job for this annotation

GitHub Actions / main

Rename parameter 'rootNode' to 'node' to match the interface declaration. (https://rules.sonarsource.com/csharp/RSPEC-927)
{
ArgumentNullException.ThrowIfNull(rootNode);

Expand Down Expand Up @@ -42,10 +42,10 @@

private void TraverseCreateStatement(SyntaxTreeNode createStatementNode)
{
var child = createStatementNode.Children.First();

Check warning on line 45 in DbSeeder/Schema/SqlSchemaBuilder.cs

View workflow job for this annotation

GitHub Actions / main

Indexing at 0 should be used instead of the "Enumerable" extension method "First" (https://rules.sonarsource.com/csharp/RSPEC-6608)
if (child!.Type is SyntaxTreeNodeType.CreateTable)
{
var tableRoot = child.Children.First();

Check warning on line 48 in DbSeeder/Schema/SqlSchemaBuilder.cs

View workflow job for this annotation

GitHub Actions / main

Indexing at 0 should be used instead of the "Enumerable" extension method "First" (https://rules.sonarsource.com/csharp/RSPEC-6608)
var table = CreateTable(tableRoot!);
_sqlSchema!.AddTable(table);
}
Expand All @@ -54,21 +54,19 @@
private Table CreateTable(SyntaxTreeNode tableRoot)
{
var tableName = tableRoot.Value;
var table = new Table(tableName);
var table = new Table(tableName, _sqlSchema!);

// Actually, now we expect that table root contains only cols, but in the future more sub-nodes can be added,
// so we have to be sure that we're working exactly with a node that contains cols.
var colsRoot = tableRoot.Children.First(x => x?.Type is SyntaxTreeNodeType.TableColumns);
var cols = ParseColumns(colsRoot!);
table.Columns.AddRange(cols);
table.AddColumns(cols);

return table;
}

private IEnumerable<Column> ParseColumns(SyntaxTreeNode colsRoot)
{
return colsRoot.Children.Select(GetColumn!).ToList();
}
=> colsRoot.Children.Select(GetColumn!).ToList();

private Column GetColumn(SyntaxTreeNode columnNode)
{
Expand Down Expand Up @@ -97,7 +95,8 @@
constraints.Add(node.Value);
break;
case SyntaxTreeNodeType.ForeignKeyDefinition:
// TODO: Implement processing FK constraint correctly
var fkConstraint = $"foreign key {node.Children[1]?.Value}|{node.Children[2]?.Value}";
constraints.Add(fkConstraint);
break;
}
}
Expand Down
71 changes: 69 additions & 2 deletions DbSeeder/Schema/Table.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,74 @@
namespace DbSeeder.Schema;

public class Table(string name)
public class Table(string name, SqlSchema schema)
{
private readonly List<Column> _columns = [];
private readonly List<ForeignKey> _foreignKeys = [];

// There are some rules how to deal if PK is not defined
// ex: find the first attribute with NOT NULL & UNIQUE constraints
public PrimaryKey? PrimaryKey { get; private set; }
public string Name { get; } = name;
public List<Column> Columns { get; } = [];
public IReadOnlyList<Column> Columns => _columns;
public IReadOnlyList<ForeignKey> ForeignKeys => _foreignKeys;

public void AddColumns(IEnumerable<Column> columns)
{
foreach (var column in columns)
{
AddColumn(column);
}
}

private void AddColumn(Column column)
{
ArgumentNullException.ThrowIfNull(column);
_columns.Add(column);

if (column.IsPrimaryKey)
{
SetTablePrimaryKey(column);
}
else if (column.IsForeignKey)
{
SetTableForeignKey(column);
}
}

private void SetTablePrimaryKey(Column column)
{
if (PrimaryKey != null)
{
throw new ArgumentException("Table can not contains more that one PRIMARY KEY");
}

PrimaryKey = new PrimaryKey(this, column);
}

private void SetTableForeignKey(Column column)
{
ArgumentNullException.ThrowIfNull(column.ForeignKeyRef);

var refTable = schema.GetTableByName(column.ForeignKeyRef.TableName);
if (refTable is null)
{
throw new InvalidOperationException($"Referenced table {column.ForeignKeyRef.TableName} is not exists " +
$"in current schema. Validate the order of the create statements, " +
$"it's matter");
}

var refColumn = refTable.GetColumnByName(column.ForeignKeyRef.ColumnName);
if (refColumn is null)
{
throw new InvalidOperationException($"Referenced table {column.ForeignKeyRef.TableName} is not exists " +
$"in current schema. Validate the order of the create statements, " +
$"it's matter");
}

var fk = new ForeignKey(this, column, refTable, refColumn);
_foreignKeys.Add(fk);
}

private Column? GetColumnByName(string columnName)
=> Columns.FirstOrDefault(c => c.Name.Equals(columnName, StringComparison.Ordinal));
}
Loading