diff --git a/DbSeeder.Tests/DbSeeder.Tests.csproj b/DbSeeder.Tests/DbSeeder.Tests.csproj index 085bddd..cb354b8 100644 --- a/DbSeeder.Tests/DbSeeder.Tests.csproj +++ b/DbSeeder.Tests/DbSeeder.Tests.csproj @@ -10,10 +10,10 @@ - - - - + + + + diff --git a/DbSeeder/DbSeeder.csproj b/DbSeeder/DbSeeder.csproj index 2f4fc77..8f67ab3 100644 --- a/DbSeeder/DbSeeder.csproj +++ b/DbSeeder/DbSeeder.csproj @@ -7,4 +7,8 @@ enable + + + + diff --git a/DbSeeder/Program.cs b/DbSeeder/Program.cs index 855ea14..43d28e0 100644 --- a/DbSeeder/Program.cs +++ b/DbSeeder/Program.cs @@ -13,10 +13,10 @@ private static void Main() """ CREATE TABLE users ( - id INT AUTO_INCREMENT, + id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(122) NOT NULL UNIQUE, profile_id INT, - PRIMARY KEY (id) + FOREIGN KEY (profile_id) REFERENCES profiles(id) ); CREATE TABLE profiles diff --git a/DbSeeder/Schema/SqlSchemaBuilder.cs b/DbSeeder/Schema/SqlSchemaBuilder.cs index 8ca0ec2..40160a8 100644 --- a/DbSeeder/Schema/SqlSchemaBuilder.cs +++ b/DbSeeder/Schema/SqlSchemaBuilder.cs @@ -94,6 +94,9 @@ private Column GetColumn(SyntaxTreeNode columnNode) case SyntaxTreeNodeType.ColumnConstraint: constraints.Add(node.Value); break; + case SyntaxTreeNodeType.ForeignKeyDefinition: + // TODO: Implement processing FK constraint correctly + break; } } diff --git a/DbSeeder/SqlParser/SyntaxTree/AstBuilder.cs b/DbSeeder/SqlParser/SyntaxTree/AstBuilder.cs index b9575a6..f2ccfbe 100644 --- a/DbSeeder/SqlParser/SyntaxTree/AstBuilder.cs +++ b/DbSeeder/SqlParser/SyntaxTree/AstBuilder.cs @@ -2,23 +2,54 @@ namespace DbSeeder.SqlParser.SyntaxTree; public class AstBuilder(List tokens) { + private ForeignKeyConstraintBuilder? _fkBuilder; + + + private readonly SyntaxTreeNode _root = new(SyntaxTreeNodeType.Root, "SQL_Script", null!); + + private SyntaxTreeNode? _localRoot; + public SyntaxTreeNode BuildSyntaxTree() { - var root = new SyntaxTreeNode(SyntaxTreeNodeType.Root, "SQL_Script", null!); - var localRoot = root; + _localRoot = _root; + _fkBuilder = new ForeignKeyConstraintBuilder(); foreach (var token in tokens) { - HandleToken(token, root, ref localRoot); + HandleToken(token, ref _localRoot); } - return root; + return _root; } - private void HandleToken(SqlToken token, SyntaxTreeNode root, ref SyntaxTreeNode? localRoot) + private void HandleToken(SqlToken token, ref SyntaxTreeNode? localRoot) { ArgumentNullException.ThrowIfNull(token); + if (localRoot.Type is SyntaxTreeNodeType.KeyDefinition) + { + HandleKeyConstraint(token, ref localRoot); + return; + } + else if (localRoot.Type is SyntaxTreeNodeType.ForeignKeyDefinition) + { + _fkBuilder!.Handle(token, ref localRoot); + + if (_fkBuilder.IsComplete) + { + // we have to move FK from the cols' definition to specific col which owns this FK + var fkDefinition = _localRoot!.Children.Single(n => n!.Type is SyntaxTreeNodeType.ForeignKeyDefinition); + var fkOwner = fkDefinition!.Children.First(); // According to the FK subtree struct owner is the first child of the FK root + + var fkOwnerColRoot = localRoot.Children.Single(c => + c!.Value.Equals(fkOwner!.Value, StringComparison.OrdinalIgnoreCase)); + + localRoot.Children.Remove(fkDefinition); + fkOwnerColRoot!.Children.Add(fkDefinition); + } + return; + } + switch (token.Type) { case SqlTokenType.Keyword: @@ -31,7 +62,7 @@ private void HandleToken(SqlToken token, SyntaxTreeNode root, ref SyntaxTreeNode HandleNumber(token, ref localRoot!); break; case SqlTokenType.Punctuation: - HandlePunctuation(token, root, ref localRoot!); + HandlePunctuation(token, ref localRoot!); break; case SqlTokenType.StringLiteral: case SqlTokenType.Operator: @@ -90,8 +121,10 @@ private void HandleIdentifier(SqlToken token, ref SyntaxTreeNode localRoot) } else { - BuildConstraint(token, ref localRoot); + // Here can be another constraints created with a CONSTRAINT keyword, but they're not supported yet + BuildKeyConstraint(token, ref localRoot); } + break; case SyntaxTreeNodeType.Column: AddNode(SyntaxTreeNodeType.ColumnDataType, token.Value, ref localRoot!); @@ -102,11 +135,58 @@ private void HandleIdentifier(SqlToken token, ref SyntaxTreeNode localRoot) case SyntaxTreeNodeType.ColumnConstraint: HandleColumnConstraintIdentifier(token, ref localRoot); break; + case SyntaxTreeNodeType.KeyDefinition: + HandleKeyConstraint(token, ref localRoot); + break; + case SyntaxTreeNodeType.ForeignKeyDefinition: + HandleForeignKeyToken(token); + break; + case SyntaxTreeNodeType.PrimaryKeyDefinition: + HandlePrimaryKeyToken(token, ref localRoot); + break; default: throw new NotImplementedException($"Unhandled localRoot type for identifier: {localRoot.Type}"); } } + private void HandlePrimaryKeyToken(SqlToken token, ref SyntaxTreeNode localRoot) + { + throw new NotImplementedException(); + } + + private void HandleForeignKeyToken(SqlToken token) + { + _fkBuilder!.Handle(token, ref _localRoot!); + } + + private void HandleKeyConstraint(SqlToken token, ref SyntaxTreeNode localRoot) + { + if (localRoot.Value.Contains("FOREIGN", StringComparison.OrdinalIgnoreCase)) + { + _fkBuilder!.Handle(token, ref localRoot); + } + + // if (token.Value.Equals("KEY", StringComparison.OrdinalIgnoreCase) && + // !localRoot.Value.Contains("KEY", StringComparison.OrdinalIgnoreCase)) + // { + // var isPrimaryKey = localRoot.Value.Contains("PRIMARY", StringComparison.OrdinalIgnoreCase); + // var nodeType = isPrimaryKey + // ? SyntaxTreeNodeType.PrimaryKeyDefinition + // : SyntaxTreeNodeType.ForeignKeyDefinition; + // var nodeValue = string.Concat(localRoot.Value.Trim(), " ", token.Value); + // var newNode = new SyntaxTreeNode(nodeType, nodeValue, localRoot.Parent); + // newNode.Children.AddRange(localRoot.Children); + // + // var tempNode = localRoot; + // localRoot = newNode; + // + // // Update parent with a new sub node + // var parent = localRoot.Parent!; + // parent.Children.Remove(tempNode); + // parent.Children.Add(newNode); + // } + } + private bool IsConstraint(SqlToken token) { var isConstraintKeyWord = token.Value.Equals("constraint", StringComparison.OrdinalIgnoreCase); @@ -116,14 +196,10 @@ private bool IsConstraint(SqlToken token) return isConstraintKeyWord || isIdentifier; } - private void BuildConstraint(SqlToken token, ref SyntaxTreeNode localRoot) + private void BuildKeyConstraint(SqlToken token, ref SyntaxTreeNode localRoot) { - Console.WriteLine($"Detected Constraint: {token.Value}"); - - AddNode(SyntaxTreeNodeType.ColumnConstraint, token.Value, ref localRoot!); - - - + // We temporarily add this constraint to the cols' root + AddNode(SyntaxTreeNodeType.KeyDefinition, token.Value, ref localRoot!); } private void HandleColumnConstraintIdentifier(SqlToken token, ref SyntaxTreeNode localRoot) @@ -137,14 +213,34 @@ private void HandleColumnConstraintIdentifier(SqlToken token, ref SyntaxTreeNode else if (localRoot.Parent?.Type is SyntaxTreeNodeType.TableColumns) { // PRIMARY KEY (id). If we're here, we are looking at the col name. - var colsContainerNode = localRoot.Parent; - colsContainerNode.Children.Remove(localRoot); - var constraintOwner = colsContainerNode.Children.First(x => x!.Value.Equals(token.Value))!; + if (localRoot.Value.Equals("FOREIGN KEY", StringComparison.OrdinalIgnoreCase)) + { + // This is a FOREIGN KEY CONSTRAINT, so we have to handle it appropriately. + // FOREIGN KEY definition statement looks like: + // + // FOREIGN KEY id REFERENCES profiles(user_id) + + if (token.Type is SqlTokenType.Identifier) + { + localRoot.AddChild(new SyntaxTreeNode(SyntaxTreeNodeType.KeyColumnIdentifier, token.Value, + localRoot)); + } + + Console.WriteLine("We are parsing FK constraint"); + } + + else if (localRoot.Value.Equals("PRIMARY KEY", StringComparison.OrdinalIgnoreCase)) + { + var colsContainerNode = localRoot.Parent; + + colsContainerNode.Children.Remove(localRoot); + var constraintOwner = colsContainerNode.Children.First(x => x!.Value.Equals(token.Value))!; - var temp = new SyntaxTreeNode(localRoot.Type, localRoot.Value, constraintOwner); - localRoot = temp; - constraintOwner.Children.Add(temp); + var temp = new SyntaxTreeNode(localRoot.Type, localRoot.Value, constraintOwner); + localRoot = temp; + constraintOwner.Children.Add(temp); + } } else { @@ -174,21 +270,22 @@ private void AddColumnConstraintNode(SqlToken token, ref SyntaxTreeNode localRoo #region Punctuation - private void HandlePunctuation(SqlToken token, SyntaxTreeNode root, ref SyntaxTreeNode localRoot) + private void HandlePunctuation(SqlToken token, ref SyntaxTreeNode localRoot) { - ArgumentNullException.ThrowIfNull(root); + ArgumentNullException.ThrowIfNull(_root); ArgumentNullException.ThrowIfNull(localRoot); var isStatementClosingBracket = token.Value.Equals(")") && localRoot.Type is SyntaxTreeNodeType.TableColumns; if (token.Value.Equals(";") || isStatementClosingBracket) { // Reset to root on statement end - localRoot = root; + localRoot = _root; } else { switch (localRoot.Type) { + // TODO: Add Key Token Type Constraint case SyntaxTreeNodeType.TableRoot: HandleTableRootPunctuation(ref localRoot); break; @@ -201,6 +298,11 @@ private void HandlePunctuation(SqlToken token, SyntaxTreeNode root, ref SyntaxTr // Move back to the direct parent (likely ColumnDataType) localRoot = localRoot.Parent!; break; + case SyntaxTreeNodeType.KeyDefinition: + case SyntaxTreeNodeType.ForeignKeyDefinition: + case SyntaxTreeNodeType.PrimaryKeyDefinition: + HandleKeyConstraintPunctuation(token, ref localRoot); + break; case SyntaxTreeNodeType.Root: case SyntaxTreeNodeType.CreateStatement: case SyntaxTreeNodeType.CreateTable: @@ -211,6 +313,14 @@ private void HandlePunctuation(SqlToken token, SyntaxTreeNode root, ref SyntaxTr } } + private void HandleKeyConstraintPunctuation(SqlToken token, ref SyntaxTreeNode localRoot) + { + if (token.Value.Equals("(")) + { + // just skip + } + } + private void HandleTableRootPunctuation(ref SyntaxTreeNode localRoot) { ArgumentNullException.ThrowIfNull(localRoot); @@ -276,7 +386,7 @@ private void HandleNumber(SqlToken token, ref SyntaxTreeNode localRoot) } #endregion - + private void AddNode(SyntaxTreeNodeType type, string value, ref SyntaxTreeNode? currentRootNode) { var newNode = new SyntaxTreeNode(type, value, currentRootNode); diff --git a/DbSeeder/SqlParser/SyntaxTree/ForeignKeyConstraintBuilder.cs b/DbSeeder/SqlParser/SyntaxTree/ForeignKeyConstraintBuilder.cs new file mode 100644 index 0000000..d09ebce --- /dev/null +++ b/DbSeeder/SqlParser/SyntaxTree/ForeignKeyConstraintBuilder.cs @@ -0,0 +1,119 @@ +namespace DbSeeder.SqlParser.SyntaxTree; + +public class ForeignKeyConstraintBuilder +{ + private SyntaxTreeNode? _constraintLocalRoot; + private SyntaxTreeNode? _lastAddedNode; + + private bool _referenceKeywordMet; + public bool IsComplete { get; private set; } + + // FOREIGN KEY subtree structure: + // FOREIGN KEY -- ForeignKeyDefinition + // profile_id -- KeyColumnIdentifier + // profiles -- KeyReferencedTable + // id -- KeyReferencedColumn + public void Handle(SqlToken? token, ref SyntaxTreeNode treeLocalRoot) + { + ArgumentNullException.ThrowIfNull(token); + + if (_constraintLocalRoot is null) + { + // This is a first node and here we have to create a ForeignKeyDefinition node + CreateConstraintRoot(ref treeLocalRoot); + } + else if (token.Type is SqlTokenType.Punctuation) + { + HandlePunctuationToken(token, ref treeLocalRoot); + } + + else if (token.Type is SqlTokenType.Identifier) + { + HandleIdentifierToken(token); + } + + else if (token.Type is SqlTokenType.Keyword + && token.Value.Equals("REFERENCES", StringComparison.OrdinalIgnoreCase)) + { + _referenceKeywordMet = true; + } + } + + private void HandleIdentifierToken(SqlToken token) + { + ArgumentNullException.ThrowIfNull(_lastAddedNode); + ArgumentNullException.ThrowIfNull(_constraintLocalRoot); + + if (token.Value.Equals("REFERENCES", StringComparison.OrdinalIgnoreCase)) + { + _referenceKeywordMet = true; + return; + } + + if (_lastAddedNode.Type is SyntaxTreeNodeType.ForeignKeyDefinition) + { + // This is a pointer to the constraint owner column + var node = new SyntaxTreeNode( + SyntaxTreeNodeType.KeyColumnIdentifier, + token.Value, + _constraintLocalRoot); + + _constraintLocalRoot.Children.Add(node); + _lastAddedNode = node; + } + else if (_lastAddedNode.Type is SyntaxTreeNodeType.KeyColumnIdentifier && + _referenceKeywordMet) + { + var node = new SyntaxTreeNode( + SyntaxTreeNodeType.KeyReferencedTable, + token.Value, + _constraintLocalRoot); + + _constraintLocalRoot.Children.Add(node); + _lastAddedNode = node; + } + else if (_lastAddedNode.Type is SyntaxTreeNodeType.KeyReferencedTable) + { + var node = new SyntaxTreeNode( + SyntaxTreeNodeType.KeyReferencedColumn, + token.Value, + _constraintLocalRoot); + + _constraintLocalRoot.Children.Add(node); + _lastAddedNode = node; + } + } + + private void HandlePunctuationToken(SqlToken token, ref SyntaxTreeNode treeLocalRoot) + { + ArgumentNullException.ThrowIfNull(_lastAddedNode); + + if (_lastAddedNode.Type == SyntaxTreeNodeType.KeyReferencedColumn && + token.Value is ")") + { + // FOREIGN KEY statement is complete + IsComplete = true; + + // move a pointer back to the cols + // then this one should be moved to the appropriate column, + // but this should be managed by the ast builder + treeLocalRoot = _constraintLocalRoot!.Parent!; + } + } + + private void CreateConstraintRoot(ref SyntaxTreeNode treeLocalRoot) + { + const string nodeValue = "FOREIGN KEY"; + _constraintLocalRoot = new SyntaxTreeNode( + SyntaxTreeNodeType.ForeignKeyDefinition, nodeValue, treeLocalRoot.Parent); + + var tempNode = treeLocalRoot; + treeLocalRoot = _constraintLocalRoot; + + var parent = treeLocalRoot.Parent!; + parent.Children.Remove(tempNode); + parent.Children.Add(_constraintLocalRoot); + + _lastAddedNode = _constraintLocalRoot; + } +} diff --git a/DbSeeder/SqlParser/SyntaxTree/SyntaxTreeNodeType.cs b/DbSeeder/SqlParser/SyntaxTree/SyntaxTreeNodeType.cs index 4393fe6..2677aee 100644 --- a/DbSeeder/SqlParser/SyntaxTree/SyntaxTreeNodeType.cs +++ b/DbSeeder/SqlParser/SyntaxTree/SyntaxTreeNodeType.cs @@ -14,5 +14,13 @@ public enum SyntaxTreeNodeType ColumnDataType, ColumnConstraint, - DataTypeConstraint + DataTypeConstraint, + + KeyDefinition, + KeyReferencedTable, + KeyColumnIdentifier, + KeyReferencedColumn, + + PrimaryKeyDefinition, + ForeignKeyDefinition } diff --git a/DbSeeder/SqlParser/SyntaxTree/Validation/Internal/TreeStructureValidator.cs b/DbSeeder/SqlParser/SyntaxTree/Validation/Internal/TreeStructureValidator.cs index 93648fe..d9782d9 100644 --- a/DbSeeder/SqlParser/SyntaxTree/Validation/Internal/TreeStructureValidator.cs +++ b/DbSeeder/SqlParser/SyntaxTree/Validation/Internal/TreeStructureValidator.cs @@ -11,10 +11,23 @@ public class TreeStructureValidator : INodeValidator { SyntaxTreeNodeType.CreateTable, [SyntaxTreeNodeType.TableRoot] }, { SyntaxTreeNodeType.TableRoot, [SyntaxTreeNodeType.TableColumns] }, { SyntaxTreeNodeType.TableColumns, [SyntaxTreeNodeType.Column] }, - { SyntaxTreeNodeType.Column, [SyntaxTreeNodeType.ColumnDataType, SyntaxTreeNodeType.ColumnConstraint] }, + { SyntaxTreeNodeType.Column, [ + SyntaxTreeNodeType.ColumnDataType, + SyntaxTreeNodeType.ColumnConstraint, + SyntaxTreeNodeType.ForeignKeyDefinition] }, { SyntaxTreeNodeType.ColumnDataType, [SyntaxTreeNodeType.DataTypeConstraint] }, { SyntaxTreeNodeType.DataTypeConstraint, [] }, { SyntaxTreeNodeType.ColumnConstraint, [] }, + { + SyntaxTreeNodeType.ForeignKeyDefinition, [ + SyntaxTreeNodeType.KeyColumnIdentifier, + SyntaxTreeNodeType.KeyReferencedTable, + SyntaxTreeNodeType.KeyReferencedColumn + ] + }, + { SyntaxTreeNodeType.KeyColumnIdentifier, [] }, + { SyntaxTreeNodeType.KeyReferencedTable, [] }, + { SyntaxTreeNodeType.KeyReferencedColumn, [] } }; public void Validate(ValidationContext validationContext, SyntaxTreeNode node)