From 1dff9a759cad02481e3dbe19f4e9e03c9a558338 Mon Sep 17 00:00:00 2001 From: Bardin08 Date: Wed, 17 Apr 2024 01:25:17 +0300 Subject: [PATCH 1/4] Fix test SQL snippet --- DbSeeder/Program.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/DbSeeder/Program.cs b/DbSeeder/Program.cs index e572d34..8b74819 100644 --- a/DbSeeder/Program.cs +++ b/DbSeeder/Program.cs @@ -12,13 +12,13 @@ private static void Main() const string sqlScript = """ CREATE TABLE Users ( - Id INT PRIMARY KEY, + Id INT AUTO_INCREMENT PRIMARY KEY, Name VARCHAR(122) NOT NULL UNIQUE, - ProfileId UUID FOREIGN KEY + ProfileId INT ); CREATE TABLE Profiles ( - Id UUID PRIMARY KEY, + Id INT PRIMARY KEY, Nickname VARCHAR(122) NOT NULL UNIQUE, ); """; From 8a1b2e21435baa825f24796b4374d5b17f3e87c6 Mon Sep 17 00:00:00 2001 From: Bardin08 Date: Wed, 17 Apr 2024 01:29:43 +0300 Subject: [PATCH 2/4] Extend column to expose its constraints as a bool properties Also, now it keeps info required to build the FK --- DbSeeder/Schema/Column.cs | 43 ++++++++++++++++++++++++++++---- DbSeeder/Schema/ForeignKeyRef.cs | 5 ++++ 2 files changed, 43 insertions(+), 5 deletions(-) create mode 100644 DbSeeder/Schema/ForeignKeyRef.cs diff --git a/DbSeeder/Schema/Column.cs b/DbSeeder/Schema/Column.cs index b50bbb5..1ac2068 100644 --- a/DbSeeder/Schema/Column.cs +++ b/DbSeeder/Schema/Column.cs @@ -1,9 +1,42 @@ 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); + IsPrimaryKey = constraints.Contains("primary key", StringComparer.OrdinalIgnoreCase); + IsForeignKey = constraints.Contains("foreign key", StringComparer.OrdinalIgnoreCase); + IsNotNull = constraints.Contains("not null", StringComparer.OrdinalIgnoreCase); + IsUnique = constraints.Contains("unique", StringComparer.OrdinalIgnoreCase); + + if (IsForeignKey) + { + // TODO[#15]: Update when working on FKs support. + // This will cause an exception during table's construction, as 'ref_TableName' won't be in a schema + const string refTableName = "ref_TableName"; + const string refColumnName = "ref_ColumnName"; + 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; } } diff --git a/DbSeeder/Schema/ForeignKeyRef.cs b/DbSeeder/Schema/ForeignKeyRef.cs new file mode 100644 index 0000000..cb156d0 --- /dev/null +++ b/DbSeeder/Schema/ForeignKeyRef.cs @@ -0,0 +1,5 @@ +namespace DbSeeder.Schema; + +public record ForeignKeyRef( + string TableName, + string ColumnName); \ No newline at end of file From 070520a5ef11f6a9e404dfc5fcfdf7cf8be2d2bf Mon Sep 17 00:00:00 2001 From: Bardin08 Date: Wed, 17 Apr 2024 01:31:14 +0300 Subject: [PATCH 3/4] Add `PK` & `FK`s to the table model FKs is not fully functional. Blocked by #15 --- DbSeeder/Schema/ForeignKey.cs | 7 +++ DbSeeder/Schema/PrimaryKey.cs | 5 ++ DbSeeder/Schema/SqlSchema.cs | 8 ++-- DbSeeder/Schema/SqlSchemaBuilder.cs | 8 ++-- DbSeeder/Schema/Table.cs | 71 ++++++++++++++++++++++++++++- 5 files changed, 89 insertions(+), 10 deletions(-) create mode 100644 DbSeeder/Schema/ForeignKey.cs create mode 100644 DbSeeder/Schema/PrimaryKey.cs diff --git a/DbSeeder/Schema/ForeignKey.cs b/DbSeeder/Schema/ForeignKey.cs new file mode 100644 index 0000000..2b9728b --- /dev/null +++ b/DbSeeder/Schema/ForeignKey.cs @@ -0,0 +1,7 @@ +namespace DbSeeder.Schema; + +public record ForeignKey( + Table Table, + Column Column, + Table RefTable, + Column RefColumn); \ No newline at end of file diff --git a/DbSeeder/Schema/PrimaryKey.cs b/DbSeeder/Schema/PrimaryKey.cs new file mode 100644 index 0000000..9f27c86 --- /dev/null +++ b/DbSeeder/Schema/PrimaryKey.cs @@ -0,0 +1,5 @@ +namespace DbSeeder.Schema; + +public record PrimaryKey( + Table Table, + Column Column); \ No newline at end of file diff --git a/DbSeeder/Schema/SqlSchema.cs b/DbSeeder/Schema/SqlSchema.cs index b2460ac..1ee0ba5 100644 --- a/DbSeeder/Schema/SqlSchema.cs +++ b/DbSeeder/Schema/SqlSchema.cs @@ -3,10 +3,12 @@ public class SqlSchema { private readonly List _tables = []; + public IReadOnlyList
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)); } diff --git a/DbSeeder/Schema/SqlSchemaBuilder.cs b/DbSeeder/Schema/SqlSchemaBuilder.cs index 470c867..8ca0ec2 100644 --- a/DbSeeder/Schema/SqlSchemaBuilder.cs +++ b/DbSeeder/Schema/SqlSchemaBuilder.cs @@ -54,21 +54,19 @@ private void TraverseCreateStatement(SyntaxTreeNode createStatementNode) 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 ParseColumns(SyntaxTreeNode colsRoot) - { - return colsRoot.Children.Select(GetColumn!).ToList(); - } + => colsRoot.Children.Select(GetColumn!).ToList(); private Column GetColumn(SyntaxTreeNode columnNode) { diff --git a/DbSeeder/Schema/Table.cs b/DbSeeder/Schema/Table.cs index 06bbb06..3cded09 100644 --- a/DbSeeder/Schema/Table.cs +++ b/DbSeeder/Schema/Table.cs @@ -1,7 +1,74 @@ namespace DbSeeder.Schema; -public class Table(string name) +public class Table(string name, SqlSchema schema) { + private readonly List _columns = []; + private readonly List _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 Columns { get; } = []; + public IReadOnlyList Columns => _columns; + public IReadOnlyList ForeignKeys => _foreignKeys; + + public void AddColumns(IEnumerable 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)); } From 67086edf6b9a58b76e5fed9e396fb480f56ee943 Mon Sep 17 00:00:00 2001 From: Bardin08 Date: Fri, 26 Apr 2024 01:45:39 +0300 Subject: [PATCH 4/4] Add full `FOREIGN KEY` support --- DbSeeder/Program.cs | 12 ++++++------ DbSeeder/Schema/Column.cs | 16 ++++++++++------ DbSeeder/Schema/SqlSchemaBuilder.cs | 3 ++- 3 files changed, 18 insertions(+), 13 deletions(-) diff --git a/DbSeeder/Program.cs b/DbSeeder/Program.cs index 43d28e0..9e09733 100644 --- a/DbSeeder/Program.cs +++ b/DbSeeder/Program.cs @@ -11,18 +11,18 @@ private static void Main() { 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); diff --git a/DbSeeder/Schema/Column.cs b/DbSeeder/Schema/Column.cs index 1ac2068..ed33d9c 100644 --- a/DbSeeder/Schema/Column.cs +++ b/DbSeeder/Schema/Column.cs @@ -13,17 +13,21 @@ public Column(string name, constraints = constraints.Select(c => c.ToLower()).ToArray(); IsAutoIncrement = constraints.Contains("auto_increment", StringComparer.OrdinalIgnoreCase); - IsPrimaryKey = constraints.Contains("primary key", StringComparer.OrdinalIgnoreCase); - IsForeignKey = constraints.Contains("foreign key", 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( + c => c.StartsWith("foreign key", StringComparison.OrdinalIgnoreCase)); + IsForeignKey = fkConstraint is not null; if (IsForeignKey) { - // TODO[#15]: Update when working on FKs support. - // This will cause an exception during table's construction, as 'ref_TableName' won't be in a schema - const string refTableName = "ref_TableName"; - const string refColumnName = "ref_ColumnName"; + var refTableAndCol = fkConstraint![12..].Split("|"); + + var refTableName = refTableAndCol[0]; + var refColumnName = refTableAndCol[1]; + ForeignKeyRef = new ForeignKeyRef(refTableName, refColumnName); } } diff --git a/DbSeeder/Schema/SqlSchemaBuilder.cs b/DbSeeder/Schema/SqlSchemaBuilder.cs index 40160a8..7bfdd54 100644 --- a/DbSeeder/Schema/SqlSchemaBuilder.cs +++ b/DbSeeder/Schema/SqlSchemaBuilder.cs @@ -95,7 +95,8 @@ private Column GetColumn(SyntaxTreeNode columnNode) 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; } }