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)