├── .editorconfig ├── .github ├── FUNDING.yml └── workflows │ └── dotnet.yml ├── .gitignore ├── .vscode ├── launch.json └── tasks.json ├── Benchmarks ├── Benchmarks.csproj ├── Program.cs ├── QueryBenchmark.cs └── SqlSchema.sql ├── Icon ├── SqlSrcGen.svg ├── SqlSrcGen256.png └── SqlSrcGen64.png ├── IntegrationTests ├── AutoIncrementTests.cs ├── CompositePrimaryKeyTests.cs ├── ContactJob_GetJobCombos_Void.sql ├── ContactTableTests.cs ├── Contact_GetAllContacts_Void.sql ├── IntegrationTests.csproj ├── JoinTests.cs ├── NullableTests.cs ├── PrimaryKeyTests.cs ├── SqlSchema.sql ├── TransactionTests.cs ├── UniqueConstraintTests.cs └── Usings.cs ├── LICENSE ├── Playground ├── Contact_GetOldContacts_Void.sql ├── Playground.csproj ├── Program.cs ├── SqlSchema.sql └── database.db ├── PlaygroundNuget ├── PlaygroundNuget.csproj ├── Program.cs └── SqlSchema.sql ├── README.md ├── SqlSrcGen.Generator ├── CSharp.cs ├── CollationParser.cs ├── DatabaseAccessGenerator.cs ├── DatabaseInfo.cs ├── DiagnosticsReporter.cs ├── ErrorCode.cs ├── ExpressionParser.cs ├── InvalidSqlException.cs ├── LiteralValueParser.cs ├── NumberParser.cs ├── Praser.cs ├── Query.cs ├── QueryInfo.cs ├── SelectParser.cs ├── SourceBuilder.cs ├── SpanTokenExtensions.cs ├── SqlGenerator.cs ├── SqlSrcGen.Generator.csproj ├── Token.cs ├── Tokenizer.cs └── TypeNameParser.cs ├── SqlSrcGen.sln ├── SqlSrcGen ├── Contact.cs ├── Numeric.cs ├── SqlSrcGen.csproj ├── Sqlite.cs ├── SqliteException.cs └── SqliteNativeMethods.cs └── Tests ├── CheckConstraintTests.cs ├── CollateConstraintTests.cs ├── DefaultConstraintTests.cs ├── GeneratedConstraintTests.cs ├── NamedConstraintTests.cs ├── NotNullConstraintTests.cs ├── NunitAttribures.cs ├── ParseExprTests.cs ├── ReadBlobLiteralTests.cs ├── ReadNumericLiteralTests.cs ├── ReferencesConstraintTests.cs ├── SelectParserTests └── StarColumnsTests.cs ├── SqlGeneratorTests.cs ├── SquareBacketNameTests.cs ├── TableConstraintTests ├── TableCheckConstraintTests.cs ├── TableForeignKeyConstraintTests.cs ├── TableNamedConstraintTests.cs ├── TablePrimaryKeyConstraintTests.cs └── TableUniqueConstraintTests.cs ├── TableOptionsTests.cs ├── Tests.csproj ├── UniqueConstraintTests.cs └── Usings.cs /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 4 -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [trampster] 4 | -------------------------------------------------------------------------------- /.github/workflows/dotnet.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a .NET project 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-net 3 | 4 | name: .NET 5 | 6 | on: 7 | push: 8 | branches: [ "main" ] 9 | pull_request: 10 | branches: [ "main" ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v3 19 | - name: Setup .NET 20 | uses: actions/setup-dotnet@v3 21 | with: 22 | dotnet-version: 6.0.x 23 | - name: Restore dependencies 24 | run: dotnet restore 25 | - name: Build 26 | run: dotnet build --no-restore 27 | - name: Test 28 | run: dotnet test --no-build --verbosity normal 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | Benchmarks/BenchmarkDotNet.Artifacts/* 2 | bin 3 | obj 4 | database.sql 5 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | // Use IntelliSense to find out which attributes exist for C# debugging 6 | // Use hover for the description of the existing attributes 7 | // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md 8 | "name": ".NET Core Launch (console)", 9 | "type": "coreclr", 10 | "request": "launch", 11 | "preLaunchTask": "build", 12 | // If you have changed target frameworks, make sure to update the program path. 13 | "program": "${workspaceFolder}/Benchmarks/bin/Debug/net6.0/Benchmarks.dll", 14 | "args": [], 15 | "cwd": "${workspaceFolder}/Benchmarks", 16 | // For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console 17 | "console": "internalConsole", 18 | "stopAtEntry": false 19 | }, 20 | { 21 | // Use IntelliSense to find out which attributes exist for C# debugging 22 | // Use hover for the description of the existing attributes 23 | // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md 24 | "name": "Playground", 25 | "type": "coreclr", 26 | "request": "launch", 27 | "preLaunchTask": "build", 28 | // If you have changed target frameworks, make sure to update the program path. 29 | "program": "${workspaceFolder}/Playground/bin/Debug/net6.0/Playground.dll", 30 | "args": [], 31 | "cwd": "${workspaceFolder}/Playground", 32 | // For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console 33 | "console": "internalConsole", 34 | "stopAtEntry": false 35 | }, 36 | { 37 | "name": ".NET Core Attach", 38 | "type": "coreclr", 39 | "request": "attach" 40 | } 41 | ] 42 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "build", 6 | "command": "dotnet", 7 | "type": "process", 8 | "args": [ 9 | "build", 10 | "${workspaceFolder}/Benchmarks/Benchmarks.csproj", 11 | "/property:GenerateFullPaths=true", 12 | "/consoleloggerparameters:NoSummary" 13 | ], 14 | "problemMatcher": "$msCompile" 15 | }, 16 | { 17 | "label": "publish", 18 | "command": "dotnet", 19 | "type": "process", 20 | "args": [ 21 | "publish", 22 | "${workspaceFolder}/Benchmarks/Benchmarks.csproj", 23 | "/property:GenerateFullPaths=true", 24 | "/consoleloggerparameters:NoSummary" 25 | ], 26 | "problemMatcher": "$msCompile" 27 | }, 28 | { 29 | "label": "watch", 30 | "command": "dotnet", 31 | "type": "process", 32 | "args": [ 33 | "watch", 34 | "run", 35 | "--project", 36 | "${workspaceFolder}/Benchmarks/Benchmarks.csproj" 37 | ], 38 | "problemMatcher": "$msCompile" 39 | } 40 | ] 41 | } -------------------------------------------------------------------------------- /Benchmarks/Benchmarks.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net6.0 6 | enable 7 | enable 8 | 9 | 10 | 11 | 12 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /Benchmarks/Program.cs: -------------------------------------------------------------------------------- 1 | using BenchmarkDotNet.Running; 2 | using Benchmarks; 3 | 4 | var summary = BenchmarkRunner.Run(); 5 | -------------------------------------------------------------------------------- /Benchmarks/QueryBenchmark.cs: -------------------------------------------------------------------------------- 1 | namespace Benchmarks; 2 | 3 | using BenchmarkDotNet.Attributes; 4 | using SqlSrcGen; 5 | using SQLite; 6 | 7 | public class QueryBenchmark 8 | { 9 | readonly Database _database; 10 | readonly List _list = new(); 11 | readonly SQLiteConnection _sqliteNetConnection; 12 | 13 | public QueryBenchmark() 14 | { 15 | string databaseName = "database1.sql"; 16 | 17 | if (File.Exists(databaseName)) 18 | { 19 | File.Delete(databaseName); 20 | } 21 | 22 | _database = new Database(databaseName); 23 | 24 | _database.CreateContactTable(); 25 | 26 | _database.InsertContact(new Contact() { Name = "Luke", Email = "luke@rebels.com" }); 27 | 28 | _sqliteNetConnection = new SQLiteConnection(databaseName); 29 | 30 | } 31 | 32 | [Benchmark] 33 | public void SqlSrcGen() 34 | { 35 | _database.AllContacts(_list); 36 | } 37 | 38 | [Benchmark] 39 | public void SqliteNet() 40 | { 41 | _sqliteNetConnection.Query("SELECT * FROM contact;"); 42 | } 43 | } -------------------------------------------------------------------------------- /Benchmarks/SqlSchema.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE contact (name Text, email Text); -------------------------------------------------------------------------------- /Icon/SqlSrcGen.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 23 | 25 | 28 | 32 | 36 | 37 | 46 | 55 | 64 | 65 | 84 | 86 | 87 | 89 | image/svg+xml 90 | 92 | 93 | 94 | 95 | 96 | 101 | 108 | 113 | 118 | 123 | 129 | 135 | 136 | 137 | -------------------------------------------------------------------------------- /Icon/SqlSrcGen256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trampster/SqlSrcGen/f2980a0ae5890d8d4af2b0d0a10d07b18807ecad/Icon/SqlSrcGen256.png -------------------------------------------------------------------------------- /Icon/SqlSrcGen64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trampster/SqlSrcGen/f2980a0ae5890d8d4af2b0d0a10d07b18807ecad/Icon/SqlSrcGen64.png -------------------------------------------------------------------------------- /IntegrationTests/AutoIncrementTests.cs: -------------------------------------------------------------------------------- 1 | using SqlSrcGen; 2 | using SQLite; 3 | 4 | namespace Tests; 5 | 6 | public class AutoIncrementTests 7 | { 8 | const string DatabaseName = "database.sql"; 9 | 10 | Database? _database; 11 | SQLiteConnection? _sqliteNetConnection; 12 | 13 | [OneTimeTearDown] 14 | public void FixtureTearDown() 15 | { 16 | _database?.Dispose(); 17 | } 18 | 19 | [SetUp] 20 | public void Setup() 21 | { 22 | if (File.Exists(DatabaseName)) 23 | { 24 | File.Delete(DatabaseName); 25 | } 26 | 27 | _database = new Database(DatabaseName); 28 | 29 | _sqliteNetConnection = new SQLiteConnection(DatabaseName); 30 | } 31 | 32 | public class TableDetails 33 | { 34 | public string? Name { get; set; } 35 | } 36 | 37 | [Test] 38 | public void CreateTable() 39 | { 40 | // arrange 41 | // act 42 | _database!.CreateAutoincrementTableTable(); 43 | 44 | // assert 45 | var tableDetails = _sqliteNetConnection?.Query("SELECT name FROM sqlite_master WHERE type='table' AND name='autoincrement_table';"); 46 | Assert.That(tableDetails?.Any() ?? false); 47 | } 48 | 49 | record SqliteNetAutoincrementTable 50 | { 51 | public long? Id { get; set; } 52 | public string? Email { get; set; } 53 | } 54 | 55 | [Test] 56 | public void Get_Exists_SetsValuesReturnsTrue() 57 | { 58 | // arrange 59 | _database!.CreateAutoincrementTableTable(); 60 | 61 | _sqliteNetConnection?.Execute("INSERT INTO autoincrement_table (email) VALUES (\"steve.rogers@avengers.com\")"); 62 | _sqliteNetConnection?.Execute("INSERT INTO autoincrement_table (email) VALUES (\"bruce.banner@avengers.com\")"); 63 | 64 | AutoincrementTable contact = new(); 65 | 66 | // act 67 | bool found = _database!.GetAutoincrementTable(contact, 2); 68 | 69 | // assert 70 | Assert.That(found, Is.True); 71 | Assert.That(contact.Id, Is.EqualTo(2)); 72 | Assert.That(contact.Email, Is.EqualTo("bruce.banner@avengers.com")); 73 | } 74 | 75 | [Test] 76 | public void Insert_AllowsNull_IdAutoSet() 77 | { 78 | // arrange 79 | _database!.CreateAutoincrementTableTable(); 80 | AutoincrementTable row = new() 81 | { 82 | Email = "luke.skywalker@jedi.com" 83 | }; 84 | 85 | // act 86 | _database!.InsertAutoincrementTable(row); 87 | 88 | // assert 89 | var queriedRow = _sqliteNetConnection?.Query("Select * from autoincrement_table;").First()!; 90 | Assert.That(queriedRow.Id, Is.EqualTo(1)); 91 | Assert.That(row.Id, Is.EqualTo(1)); 92 | Assert.That(row.Email, Is.EqualTo("luke.skywalker@jedi.com")); 93 | } 94 | 95 | record SqliteNetAutoincrementNotNullTable 96 | { 97 | public long? Id { get; set; } 98 | public string? Email { get; set; } 99 | } 100 | 101 | [Test] 102 | public void Insert_NotNull_IdAutoSet() 103 | { 104 | // arrange 105 | _database!.CreateAutoincrementNotNullTableTable(); 106 | AutoincrementNotNullTable row = new(); 107 | 108 | // act 109 | _database!.InsertAutoincrementNotNullTable(row); 110 | 111 | // assert 112 | var queriedRow = _sqliteNetConnection?.Query("Select * from autoincrement_not_null_table;").First()!; 113 | Assert.That(queriedRow.Id, Is.EqualTo(1)); 114 | Assert.That(row.Id, Is.EqualTo(1)); 115 | } 116 | } -------------------------------------------------------------------------------- /IntegrationTests/CompositePrimaryKeyTests.cs: -------------------------------------------------------------------------------- 1 | using SqlSrcGen; 2 | using SQLite; 3 | 4 | namespace Tests; 5 | 6 | public class CompositePrimaryKeyTests 7 | { 8 | const string DatabaseName = "database.sql"; 9 | 10 | Database? _database; 11 | SQLiteConnection? _sqliteNetConnection; 12 | 13 | [OneTimeTearDown] 14 | public void FixtureTearDown() 15 | { 16 | _database?.Dispose(); 17 | } 18 | 19 | [SetUp] 20 | public void Setup() 21 | { 22 | if (File.Exists(DatabaseName)) 23 | { 24 | File.Delete(DatabaseName); 25 | } 26 | 27 | _database = new Database(DatabaseName); 28 | 29 | _sqliteNetConnection = new SQLiteConnection(DatabaseName); 30 | } 31 | 32 | public class TableDetails 33 | { 34 | public string? Name { get; set; } 35 | } 36 | 37 | [Test] 38 | public void CreateTable() 39 | { 40 | // arrange 41 | // act 42 | _database!.CreateCompositPrimaryKeyTable(); 43 | 44 | // assert 45 | var tableDetails = _sqliteNetConnection?.Query("SELECT name FROM sqlite_master WHERE type='table' AND name='composit_primary_key';"); 46 | Assert.That(tableDetails?.Any() ?? false); 47 | } 48 | 49 | record SqliteNetCompositPrimaryKey 50 | { 51 | public string? Name { get; set; } 52 | public string? Email { get; set; } 53 | } 54 | 55 | [Test] 56 | public void Get_Exists_SetsValuesReturnsTrue() 57 | { 58 | // arrange 59 | _database!.CreateCompositPrimaryKeyTable(); 60 | 61 | _sqliteNetConnection?.Execute("INSERT INTO composit_primary_key (name, email) VALUES (\"Steve\", \"steve.rogers@avengers.com\")"); 62 | _sqliteNetConnection?.Execute("INSERT INTO composit_primary_key (name, email) VALUES (\"Bruce\", \"bruce.banner@avengers.com\")"); 63 | 64 | CompositPrimaryKey contact = new(); 65 | 66 | // act 67 | bool found = _database!.GetCompositPrimaryKey(contact, "Bruce", "bruce.banner@avengers.com"); 68 | 69 | // assert 70 | Assert.That(found, Is.True); 71 | Assert.That(contact.Name, Is.EqualTo("Bruce")); 72 | Assert.That(contact.Email, Is.EqualTo("bruce.banner@avengers.com")); 73 | } 74 | 75 | [Test] 76 | public void Get_DoesntExist_ReturnsFalse() 77 | { 78 | // arrange 79 | _database!.CreateCompositPrimaryKeyTable(); 80 | 81 | _sqliteNetConnection?.Execute("INSERT INTO composit_primary_key (name, email) VALUES (\"Steve\", \"steve.rogers@avengers.com\")"); 82 | _sqliteNetConnection?.Execute("INSERT INTO composit_primary_key (name, email) VALUES (\"Bruce\", \"bruce.banner@avengers.com\")"); 83 | 84 | CompositPrimaryKey contact = new(); 85 | 86 | // act 87 | bool found = _database!.GetCompositPrimaryKey(contact, "Bruce", "steve.rogers@avengers.com"); 88 | 89 | // assert 90 | Assert.That(found, Is.False); 91 | } 92 | 93 | [Test] 94 | public void Delete_Exists_RowDeleted() 95 | { 96 | // arrange 97 | _database!.CreateCompositPrimaryKeyTable(); 98 | 99 | _sqliteNetConnection?.Execute("INSERT INTO composit_primary_key (name, email) VALUES (\"Steve\", \"steve.rogers@avengers.com\")"); 100 | _sqliteNetConnection?.Execute("INSERT INTO composit_primary_key (name, email) VALUES (\"Bruce\", \"bruce.banner@avengers.com\")"); 101 | 102 | PrimaryKeyTable contact = new(); 103 | 104 | // act 105 | _database!.DeleteCompositPrimaryKey("Bruce", "bruce.banner@avengers.com"); 106 | 107 | // assert 108 | var rows = _sqliteNetConnection?.Query("Select * from composit_primary_key;")!; 109 | Assert.That(rows.Count(), Is.EqualTo(1)); 110 | Assert.That(rows.First()!.Name, Is.EqualTo("Steve")); 111 | } 112 | 113 | [Test] 114 | public void Delete_DoesntExists_NoRowDeleted() 115 | { 116 | // arrange 117 | _database!.CreateCompositPrimaryKeyTable(); 118 | 119 | _sqliteNetConnection?.Execute("INSERT INTO composit_primary_key (name, email) VALUES (\"Steve\", \"steve.rogers@avengers.com\")"); 120 | _sqliteNetConnection?.Execute("INSERT INTO composit_primary_key (name, email) VALUES (\"Bruce\", \"bruce.banner@avengers.com\")"); 121 | 122 | PrimaryKeyTable contact = new(); 123 | 124 | // act 125 | _database!.DeleteCompositPrimaryKey("Bruce", "steve.rogers@avengers.com"); 126 | 127 | // assert 128 | var rows = _sqliteNetConnection?.Query("Select * from composit_primary_key;")!; 129 | Assert.That(rows.Count(), Is.EqualTo(2)); 130 | } 131 | } -------------------------------------------------------------------------------- /IntegrationTests/ContactJob_GetJobCombos_Void.sql: -------------------------------------------------------------------------------- 1 | SELECT * FROM contact, job; -------------------------------------------------------------------------------- /IntegrationTests/ContactTableTests.cs: -------------------------------------------------------------------------------- 1 | using SqlSrcGen; 2 | using SQLite; 3 | 4 | namespace Tests; 5 | 6 | public class ContactTableTests 7 | { 8 | const string DatabaseName = "database.sql"; 9 | 10 | Database? _database; 11 | SQLiteConnection? _sqliteNetConnection; 12 | 13 | [OneTimeTearDown] 14 | public void FixtureTearDown() 15 | { 16 | _database?.Dispose(); 17 | } 18 | 19 | [SetUp] 20 | public void Setup() 21 | { 22 | if (File.Exists(DatabaseName)) 23 | { 24 | File.Delete(DatabaseName); 25 | } 26 | 27 | _database = new Database(DatabaseName); 28 | 29 | _sqliteNetConnection = new SQLiteConnection(DatabaseName); 30 | } 31 | 32 | public class TableDetails 33 | { 34 | public string? Name { get; set; } 35 | } 36 | 37 | [Test] 38 | public void CreateTable() 39 | { 40 | // arrange 41 | // act 42 | _database!.CreateContactTable(); 43 | 44 | // assert 45 | var tableDetails = _sqliteNetConnection?.Query("SELECT name FROM sqlite_master WHERE type='table' AND name='contact';"); 46 | Assert.That(tableDetails?.Any() ?? false); 47 | } 48 | 49 | record SqliteNetContact 50 | { 51 | public string Name { get; set; } = ""; 52 | public string Email { get; set; } = ""; 53 | public long Age { get; set; } 54 | public double Height { get; set; } 55 | public byte[] PrivateKey { get; set; } = new byte[0]; 56 | public double Mana { get; set; } 57 | } 58 | 59 | [Test] 60 | public void Insert() 61 | { 62 | // arrange 63 | _database!.CreateContactTable(); 64 | 65 | var contact = new Contact() 66 | { 67 | Name = "Steve", 68 | Email = "steve.rogers@superhero.com", 69 | Age = 35, 70 | Height = 170.5, 71 | PrivateKey = new byte[] { 1, 2, 3 }, 72 | Mana = new Numeric(24.5) 73 | }; 74 | 75 | // act 76 | _database!.InsertContact(contact); 77 | 78 | // assert 79 | var actualContact = _sqliteNetConnection?.Query("Select * from contact;").First()!; 80 | Assert.That(contact.Name, Is.EqualTo(actualContact.Name)); 81 | Assert.That(contact.Age, Is.EqualTo(actualContact.Age)); 82 | Assert.That(contact.Email, Is.EqualTo(actualContact.Email)); 83 | Assert.That(contact.Height, Is.EqualTo(actualContact.Height)); 84 | Assert.That(contact.PrivateKey, Is.EqualTo(actualContact.PrivateKey)); 85 | Assert.That(contact.Mana.GetReal(), Is.EqualTo(actualContact.Mana)); 86 | } 87 | 88 | [Test] 89 | public void All() 90 | { 91 | // arrange 92 | _database!.CreateContactTable(); 93 | 94 | var steveContact = new Contact() 95 | { 96 | Name = "Steve", 97 | Email = "steve.rogers@avengers.com", 98 | Age = 35, 99 | Height = 170.5, 100 | PrivateKey = new byte[] { 1, 2, 3 }, 101 | Mana = new Numeric(24.5) 102 | }; 103 | var tonyContact = new Contact() 104 | { 105 | Name = "Tony", 106 | Email = "tony.stark@avengers.com", 107 | Age = 40, 108 | Height = 185.42, 109 | PrivateKey = new byte[] { 1, 2, 3 }, 110 | Mana = new Numeric(24.5) 111 | }; 112 | _sqliteNetConnection?.Execute("INSERT INTO contact (name, email, age, height, privateKey, mana) VALUES (\"Steve\", \"steve.rogers@avengers.com\", 35, 170.5, X'010203', 24.5)"); 113 | _sqliteNetConnection?.Execute("INSERT INTO contact (name, email, age, height, privateKey, mana) VALUES (\"Tony\", \"tony.stark@avengers.com\", 40, 185.42, X'010203', \"24.5\")"); 114 | 115 | List contacts = new(); 116 | 117 | // act 118 | _database!.AllContacts(contacts); 119 | 120 | // assert 121 | Assert.That(contacts.Count, Is.EqualTo(2)); 122 | Assert.That(contacts[0].Name, Is.EqualTo(steveContact.Name)); 123 | Assert.That(contacts[0].Age, Is.EqualTo(steveContact.Age)); 124 | Assert.That(contacts[0].Email, Is.EqualTo(steveContact.Email)); 125 | Assert.That(contacts[0].PrivateKey, Is.EqualTo(steveContact.PrivateKey)); 126 | Assert.That(contacts[0].Mana, Is.EqualTo(steveContact.Mana)); 127 | Assert.That(contacts[1].Name, Is.EqualTo(tonyContact.Name)); 128 | Assert.That(contacts[1].Age, Is.EqualTo(tonyContact.Age)); 129 | Assert.That(contacts[1].Email, Is.EqualTo(tonyContact.Email)); 130 | Assert.That(contacts[1].PrivateKey, Is.EqualTo(tonyContact.PrivateKey)); 131 | Assert.That(contacts[1].Mana, Is.EqualTo(tonyContact.Mana)); 132 | } 133 | 134 | [Test] 135 | public void CustomQuery_GetAllContactsTest() 136 | { 137 | // arrange 138 | _database!.CreateContactTable(); 139 | 140 | var steveContact = new Contact() 141 | { 142 | Name = "Steve", 143 | Email = "steve.rogers@avengers.com", 144 | Age = 35, 145 | Height = 170.5, 146 | PrivateKey = new byte[] { 1, 2, 3 }, 147 | Mana = new Numeric(24.5) 148 | }; 149 | var tonyContact = new Contact() 150 | { 151 | Name = "Tony", 152 | Email = "tony.stark@avengers.com", 153 | Age = 40, 154 | Height = 185.42, 155 | PrivateKey = new byte[] { 1, 2, 3 }, 156 | Mana = new Numeric(24.5) 157 | }; 158 | _sqliteNetConnection?.Execute("INSERT INTO contact (name, email, age, height, privateKey, mana) VALUES (\"Steve\", \"steve.rogers@avengers.com\", 35, 170.5, X'010203', 24.5)"); 159 | _sqliteNetConnection?.Execute("INSERT INTO contact (name, email, age, height, privateKey, mana) VALUES (\"Tony\", \"tony.stark@avengers.com\", 40, 185.42, X'010203', \"24.5\")"); 160 | 161 | List contacts = new(); 162 | 163 | // act 164 | _database!.GetAllContacts(contacts); 165 | 166 | // assert 167 | Assert.That(contacts.Count, Is.EqualTo(2)); 168 | Assert.That(contacts[0].Name, Is.EqualTo(steveContact.Name)); 169 | Assert.That(contacts[0].Age, Is.EqualTo(steveContact.Age)); 170 | Assert.That(contacts[0].Email, Is.EqualTo(steveContact.Email)); 171 | Assert.That(contacts[0].PrivateKey, Is.EqualTo(steveContact.PrivateKey)); 172 | Assert.That(contacts[0].Mana, Is.EqualTo(steveContact.Mana)); 173 | Assert.That(contacts[1].Name, Is.EqualTo(tonyContact.Name)); 174 | Assert.That(contacts[1].Age, Is.EqualTo(tonyContact.Age)); 175 | Assert.That(contacts[1].Email, Is.EqualTo(tonyContact.Email)); 176 | Assert.That(contacts[1].PrivateKey, Is.EqualTo(tonyContact.PrivateKey)); 177 | Assert.That(contacts[1].Mana, Is.EqualTo(tonyContact.Mana)); 178 | } 179 | 180 | [Test] 181 | public void DeleteAll() 182 | { 183 | // arrange 184 | _database!.CreateContactTable(); 185 | 186 | _sqliteNetConnection?.Execute("INSERT INTO contact (name, email, age, height, privateKey, mana) VALUES (\"Steve\", \"steve.rogers@avengers.com\", 35, 170.5, X'010203', 24.5)"); 187 | _sqliteNetConnection?.Execute("INSERT INTO contact (name, email, age, height, privateKey, mana) VALUES (\"Tony\", \"tony.stark@avengers.com\", 40, 185.42, X'010203', \"24.5\")"); 188 | 189 | // act 190 | _database!.DeleteAllContacts(); 191 | 192 | // assert 193 | var contacts = _sqliteNetConnection?.Query("Select * from contact;"); 194 | 195 | Assert.That(contacts!.Count, Is.EqualTo(0)); 196 | } 197 | } -------------------------------------------------------------------------------- /IntegrationTests/Contact_GetAllContacts_Void.sql: -------------------------------------------------------------------------------- 1 | SELECT * FROM contact; -------------------------------------------------------------------------------- /IntegrationTests/IntegrationTests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | enable 6 | enable 7 | 8 | false 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /IntegrationTests/JoinTests.cs: -------------------------------------------------------------------------------- 1 | using SqlSrcGen; 2 | using SQLite; 3 | using Microsoft.VisualStudio.TestPlatform.Utilities; 4 | using System.Reflection.Metadata.Ecma335; 5 | 6 | namespace Tests; 7 | 8 | public class JoinTests 9 | { 10 | const string DatabaseName = "database.sql"; 11 | 12 | Database? _database; 13 | SQLiteConnection? _sqliteNetConnection; 14 | 15 | [OneTimeTearDown] 16 | public void FixtureTearDown() 17 | { 18 | _database?.Dispose(); 19 | } 20 | 21 | [SetUp] 22 | public void Setup() 23 | { 24 | if (File.Exists(DatabaseName)) 25 | { 26 | File.Delete(DatabaseName); 27 | } 28 | 29 | _database = new Database(DatabaseName); 30 | 31 | _sqliteNetConnection = new SQLiteConnection(DatabaseName); 32 | } 33 | 34 | public class TableDetails 35 | { 36 | public string? Name { get; set; } 37 | } 38 | 39 | record SqliteNetContact 40 | { 41 | public string Name { get; set; } = ""; 42 | public string Email { get; set; } = ""; 43 | public long Age { get; set; } 44 | public double Height { get; set; } 45 | public byte[] PrivateKey { get; set; } = new byte[0]; 46 | public double Mana { get; set; } 47 | } 48 | 49 | record SqliteNetJob 50 | { 51 | public string Name { get; set; } = ""; 52 | public long Salary { get; set; } 53 | } 54 | 55 | 56 | [Test] 57 | public void CustomQuery_GetAllContactsTest() 58 | { 59 | // arrange 60 | _database!.CreateContactTable(); 61 | _database!.CreateJobTable(); 62 | 63 | var steveContact = new Contact() 64 | { 65 | Name = "Steve", 66 | Email = "steve.rogers@avengers.com", 67 | Age = 35, 68 | Height = 170.5, 69 | PrivateKey = new byte[] { 1, 2, 3 }, 70 | Mana = new Numeric(24.5) 71 | }; 72 | var tonyContact = new Contact() 73 | { 74 | Name = "Tony", 75 | Email = "tony.stark@avengers.com", 76 | Age = 40, 77 | Height = 185.42, 78 | PrivateKey = new byte[] { 1, 2, 3 }, 79 | Mana = new Numeric(24.5) 80 | }; 81 | var teamLead = new Job() 82 | { 83 | Name = "Team Lead", 84 | Salary = 100000 85 | }; 86 | 87 | var ceo = new Job() 88 | { 89 | Name = "CEO", 90 | Salary = 200000 91 | }; 92 | 93 | _sqliteNetConnection?.Execute("INSERT INTO contact (name, email, age, height, privateKey, mana) VALUES (\"Steve\", \"steve.rogers@avengers.com\", 35, 170.5, X'010203', 24.5)"); 94 | _sqliteNetConnection?.Execute("INSERT INTO contact (name, email, age, height, privateKey, mana) VALUES (\"Tony\", \"tony.stark@avengers.com\", 40, 185.42, X'010203', \"24.5\")"); 95 | _sqliteNetConnection?.Execute("INSERT INTO job (name, salary) VALUES (\"Team Lead\", 100000)"); 96 | _sqliteNetConnection?.Execute("INSERT INTO job (name, salary) VALUES (\"CEO\", 200000)"); 97 | 98 | List contactJobs = new(); 99 | 100 | // act 101 | _database!.GetJobCombos(contactJobs); 102 | 103 | // assert 104 | Assert.That(contactJobs.Count, Is.EqualTo(4)); 105 | Assert.That(contactJobs[0].ContactName, Is.EqualTo(steveContact.Name)); 106 | Assert.That(contactJobs[0].Age, Is.EqualTo(steveContact.Age)); 107 | Assert.That(contactJobs[0].Email, Is.EqualTo(steveContact.Email)); 108 | Assert.That(contactJobs[0].PrivateKey, Is.EqualTo(steveContact.PrivateKey)); 109 | Assert.That(contactJobs[0].JobName, Is.EqualTo(teamLead.Name)); 110 | Assert.That(contactJobs[0].Salary, Is.EqualTo(teamLead.Salary)); 111 | 112 | Assert.That(contactJobs[1].ContactName, Is.EqualTo(steveContact.Name)); 113 | Assert.That(contactJobs[1].Age, Is.EqualTo(steveContact.Age)); 114 | Assert.That(contactJobs[1].Email, Is.EqualTo(steveContact.Email)); 115 | Assert.That(contactJobs[1].PrivateKey, Is.EqualTo(steveContact.PrivateKey)); 116 | Assert.That(contactJobs[1].JobName, Is.EqualTo(ceo.Name)); 117 | Assert.That(contactJobs[1].Salary, Is.EqualTo(ceo.Salary)); 118 | 119 | Assert.That(contactJobs[2].ContactName, Is.EqualTo(tonyContact.Name)); 120 | Assert.That(contactJobs[2].Age, Is.EqualTo(tonyContact.Age)); 121 | Assert.That(contactJobs[2].Email, Is.EqualTo(tonyContact.Email)); 122 | Assert.That(contactJobs[2].PrivateKey, Is.EqualTo(tonyContact.PrivateKey)); 123 | Assert.That(contactJobs[2].Mana, Is.EqualTo(tonyContact.Mana)); 124 | Assert.That(contactJobs[2].JobName, Is.EqualTo(teamLead.Name)); 125 | Assert.That(contactJobs[2].Salary, Is.EqualTo(teamLead.Salary)); 126 | 127 | Assert.That(contactJobs[3].ContactName, Is.EqualTo(tonyContact.Name)); 128 | Assert.That(contactJobs[3].Age, Is.EqualTo(tonyContact.Age)); 129 | Assert.That(contactJobs[3].Email, Is.EqualTo(tonyContact.Email)); 130 | Assert.That(contactJobs[3].PrivateKey, Is.EqualTo(tonyContact.PrivateKey)); 131 | Assert.That(contactJobs[3].Mana, Is.EqualTo(tonyContact.Mana)); 132 | Assert.That(contactJobs[3].JobName, Is.EqualTo(ceo.Name)); 133 | Assert.That(contactJobs[3].Salary, Is.EqualTo(ceo.Salary)); 134 | } 135 | } -------------------------------------------------------------------------------- /IntegrationTests/NullableTests.cs: -------------------------------------------------------------------------------- 1 | using SqlSrcGen; 2 | using SQLite; 3 | 4 | namespace Tests; 5 | 6 | public class NullableTests 7 | { 8 | const string DatabaseName = "database.sql"; 9 | 10 | Database? _database; 11 | SQLiteConnection? _sqliteNetConnection; 12 | 13 | [OneTimeTearDown] 14 | public void FixtureTearDown() 15 | { 16 | _database?.Dispose(); 17 | } 18 | 19 | [SetUp] 20 | public void Setup() 21 | { 22 | if (File.Exists(DatabaseName)) 23 | { 24 | File.Delete(DatabaseName); 25 | } 26 | 27 | _database = new Database(DatabaseName); 28 | 29 | _sqliteNetConnection = new SQLiteConnection(DatabaseName); 30 | } 31 | 32 | public class TableDetails 33 | { 34 | public string? Name { get; set; } 35 | } 36 | 37 | [Test] 38 | public void CreateTable() 39 | { 40 | // arrange 41 | // act 42 | _database!.CreateNullableContactTable(); 43 | 44 | // assert 45 | var tableDetails = _sqliteNetConnection?.Query("SELECT name FROM sqlite_master WHERE type='table' AND name='nullable_contact';"); 46 | Assert.That(tableDetails?.Any() ?? false); 47 | } 48 | 49 | record SqliteNetContact 50 | { 51 | public string? Name { get; set; } 52 | public string? Email { get; set; } 53 | public long? Age { get; set; } 54 | public double? Height { get; set; } 55 | public byte[]? PrivateKey { get; set; } 56 | public double? Mana { get; set; } 57 | } 58 | 59 | [Test] 60 | public void Insert_NotNull() 61 | { 62 | // arrange 63 | _database!.CreateNullableContactTable(); 64 | 65 | var contact = new NullableContact() 66 | { 67 | Name = "Steve", 68 | Email = "steve.rogers@superhero.com", 69 | Age = 35, 70 | Height = 170.5, 71 | PrivateKey = new byte[] { 1, 2, 3 }, 72 | Mana = new Numeric(24.5) 73 | }; 74 | 75 | // act 76 | _database!.InsertNullableContact(contact); 77 | 78 | // assert 79 | var actualContact = _sqliteNetConnection?.Query("Select * from nullable_contact;").First()!; 80 | Assert.That(contact.Name, Is.EqualTo(actualContact.Name)); 81 | Assert.That(contact.Age, Is.EqualTo(actualContact.Age)); 82 | Assert.That(contact.Email, Is.EqualTo(actualContact.Email)); 83 | Assert.That(contact.Height, Is.EqualTo(actualContact.Height)); 84 | Assert.That(contact.PrivateKey, Is.EqualTo(actualContact.PrivateKey)); 85 | Assert.That(contact.Mana.Value.GetReal(), Is.EqualTo(actualContact.Mana)); 86 | } 87 | 88 | [Test] 89 | public void Insert_Null() 90 | { 91 | // arrange 92 | _database!.CreateNullableContactTable(); 93 | 94 | var contact = new NullableContact() 95 | { 96 | Name = null, 97 | Email = null, 98 | Age = null, 99 | Height = null, 100 | PrivateKey = null, 101 | Mana = null 102 | }; 103 | 104 | // act 105 | _database!.InsertNullableContact(contact); 106 | 107 | // assert 108 | var actualContact = _sqliteNetConnection?.Query("Select * from nullable_contact;").First()!; 109 | Assert.That(actualContact.Name, Is.Null); 110 | Assert.That(actualContact.Age, Is.Null); 111 | Assert.That(actualContact.Email, Is.Null); 112 | Assert.That(actualContact.Height, Is.Null); 113 | Assert.That(actualContact.PrivateKey, Is.Null); 114 | Assert.That(actualContact.Mana, Is.Null); 115 | } 116 | 117 | [Test] 118 | public void All() 119 | { 120 | // arrange 121 | _database!.CreateNullableContactTable(); 122 | 123 | var steveContact = new NullableContact() 124 | { 125 | Name = "Steve", 126 | Email = "steve.rogers@avengers.com", 127 | Age = 35, 128 | Height = 170.5, 129 | PrivateKey = new byte[] { 1, 2, 3 }, 130 | Mana = new Numeric(24.5) 131 | }; 132 | var tonyContact = new NullableContact() 133 | { 134 | Name = null, 135 | Email = null, 136 | Age = null, 137 | Height = null, 138 | PrivateKey = null, 139 | Mana = null 140 | }; 141 | _sqliteNetConnection?.Execute("INSERT INTO nullable_contact (name, email, age, height, privateKey, mana) VALUES (\"Steve\", \"steve.rogers@avengers.com\", 35, 170.5, X'010203', 24.5)"); 142 | _sqliteNetConnection?.Execute("INSERT INTO nullable_contact (name, email, age, height, privateKey, mana) VALUES (null, null, null, null, null, null)"); 143 | 144 | List contacts = new(); 145 | 146 | // act 147 | _database!.AllNullableContacts(contacts); 148 | 149 | // assert 150 | Assert.That(contacts.Count, Is.EqualTo(2)); 151 | Assert.That(contacts[0].Name, Is.EqualTo(steveContact.Name)); 152 | Assert.That(contacts[0].Age, Is.EqualTo(steveContact.Age)); 153 | Assert.That(contacts[0].Email, Is.EqualTo(steveContact.Email)); 154 | Assert.That(contacts[0].PrivateKey, Is.EqualTo(steveContact.PrivateKey)); 155 | Assert.That(contacts[0].Mana, Is.EqualTo(steveContact.Mana)); 156 | Assert.That(contacts[1].Name, Is.EqualTo(tonyContact.Name)); 157 | Assert.That(contacts[1].Age, Is.EqualTo(tonyContact.Age)); 158 | Assert.That(contacts[1].Email, Is.EqualTo(tonyContact.Email)); 159 | Assert.That(contacts[1].PrivateKey, Is.EqualTo(tonyContact.PrivateKey)); 160 | Assert.That(contacts[1].Mana, Is.EqualTo(tonyContact.Mana)); 161 | } 162 | } -------------------------------------------------------------------------------- /IntegrationTests/PrimaryKeyTests.cs: -------------------------------------------------------------------------------- 1 | using SqlSrcGen; 2 | using SQLite; 3 | 4 | namespace Tests; 5 | 6 | public class PrimaryKeyTests 7 | { 8 | const string DatabaseName = "database.sql"; 9 | 10 | Database? _database; 11 | SQLiteConnection? _sqliteNetConnection; 12 | 13 | [OneTimeTearDown] 14 | public void FixtureTearDown() 15 | { 16 | _database?.Dispose(); 17 | } 18 | 19 | [SetUp] 20 | public void Setup() 21 | { 22 | if (File.Exists(DatabaseName)) 23 | { 24 | File.Delete(DatabaseName); 25 | } 26 | 27 | _database = new Database(DatabaseName); 28 | 29 | _sqliteNetConnection = new SQLiteConnection(DatabaseName); 30 | } 31 | 32 | public class TableDetails 33 | { 34 | public string? Name { get; set; } 35 | } 36 | 37 | [Test] 38 | public void CreateTable() 39 | { 40 | // arrange 41 | // act 42 | _database!.CreatePrimaryKeyTableTable(); 43 | 44 | // assert 45 | var tableDetails = _sqliteNetConnection?.Query("SELECT name FROM sqlite_master WHERE type='table' AND name='primary_key_table';"); 46 | Assert.That(tableDetails?.Any() ?? false); 47 | } 48 | 49 | record SqliteNetPrimaryKeyTable 50 | { 51 | public string? Name { get; set; } 52 | public string? Email { get; set; } 53 | } 54 | 55 | [Test] 56 | public void Get_Exists_SetsValuesReturnsTrue() 57 | { 58 | // arrange 59 | _database!.CreatePrimaryKeyTableTable(); 60 | 61 | _sqliteNetConnection?.Execute("INSERT INTO primary_key_table (name, email) VALUES (\"Steve\", \"steve.rogers@avengers.com\")"); 62 | _sqliteNetConnection?.Execute("INSERT INTO primary_key_table (name, email) VALUES (\"Bruce\", \"bruce.banner@avengers.com\")"); 63 | 64 | PrimaryKeyTable contact = new(); 65 | 66 | // act 67 | bool found = _database!.GetPrimaryKeyTable(contact, "Bruce"); 68 | 69 | // assert 70 | Assert.That(found, Is.True); 71 | Assert.That(contact.Name, Is.EqualTo("Bruce")); 72 | Assert.That(contact.Email, Is.EqualTo("bruce.banner@avengers.com")); 73 | } 74 | 75 | [Test] 76 | public void Get_DoesntExist_ReturnsFalse() 77 | { 78 | // arrange 79 | _database!.CreatePrimaryKeyTableTable(); 80 | 81 | _sqliteNetConnection?.Execute("INSERT INTO primary_key_table (name, email) VALUES (\"Steve\", \"steve.rogers@avengers.com\")"); 82 | _sqliteNetConnection?.Execute("INSERT INTO primary_key_table (name, email) VALUES (\"Bruce\", \"bruce.banner@avengers.com\")"); 83 | 84 | PrimaryKeyTable contact = new(); 85 | 86 | // act 87 | bool found = _database!.GetPrimaryKeyTable(contact, "Thanos"); 88 | 89 | // assert 90 | Assert.That(found, Is.False); 91 | } 92 | 93 | [Test] 94 | public void Delete_Exists_RowDeleted() 95 | { 96 | // arrange 97 | _database!.CreatePrimaryKeyTableTable(); 98 | 99 | _sqliteNetConnection?.Execute("INSERT INTO primary_key_table (name, email) VALUES (\"Steve\", \"steve.rogers@avengers.com\")"); 100 | _sqliteNetConnection?.Execute("INSERT INTO primary_key_table (name, email) VALUES (\"Bruce\", \"bruce.banner@avengers.com\")"); 101 | 102 | PrimaryKeyTable contact = new(); 103 | 104 | // act 105 | _database!.DeletePrimaryKeyTable("Bruce"); 106 | 107 | // assert 108 | var rows = _sqliteNetConnection?.Query("Select * from primary_key_table;")!; 109 | Assert.That(rows.Count(), Is.EqualTo(1)); 110 | Assert.That(rows.First()!.Name, Is.EqualTo("Steve")); 111 | } 112 | } -------------------------------------------------------------------------------- /IntegrationTests/SqlSchema.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE contact (name Text not null, email Text not null, age Integer not null, height Real not null, privateKey Blob not null, mana Numeric not null); 2 | 3 | CREATE TABLE job (name Text not null, salary Integer not null); 4 | 5 | CREATE TABLE nullable_contact (name Text, email Text, age Integer, height Real, privateKey Blob, mana Numeric); 6 | 7 | CREATE TABLE primary_key_table (name Text primary key, email Text); 8 | 9 | CREATE TABLE autoincrement_table (id INTEGER PRIMARY KEY AUTOINCREMENT, email TEXT); 10 | 11 | CREATE TABLE autoincrement_not_null_table (id INTEGER PRIMARY KEY AUTOINCREMENT, email TEXT); 12 | 13 | CREATE TABLE composit_primary_key (name Text, email Text, PRIMARY KEY (name, email)); 14 | 15 | CREATE TABLE unique_constraint (name Text UNIQUE, email Text, UNIQUE (name, email)); 16 | 17 | -------------------------------------------------------------------------------- /IntegrationTests/TransactionTests.cs: -------------------------------------------------------------------------------- 1 | using SqlSrcGen; 2 | using SQLite; 3 | 4 | namespace Tests; 5 | 6 | public class TransactionTests 7 | { 8 | const string DatabaseName = "database.sql"; 9 | 10 | Database? _database; 11 | SQLiteConnection? _sqliteNetConnection; 12 | 13 | [OneTimeTearDown] 14 | public void FixtureTearDown() 15 | { 16 | _database?.Dispose(); 17 | } 18 | 19 | [SetUp] 20 | public void Setup() 21 | { 22 | if (File.Exists(DatabaseName)) 23 | { 24 | File.Delete(DatabaseName); 25 | } 26 | 27 | _database = new Database(DatabaseName); 28 | 29 | _sqliteNetConnection = new SQLiteConnection(DatabaseName); 30 | } 31 | 32 | public class TableDetails 33 | { 34 | public string? Name { get; set; } 35 | } 36 | 37 | [Test] 38 | public void CreateTable() 39 | { 40 | // arrange 41 | // act 42 | _database!.CreateNullableContactTable(); 43 | 44 | // assert 45 | var tableDetails = _sqliteNetConnection?.Query("SELECT name FROM sqlite_master WHERE type='table' AND name='nullable_contact';"); 46 | Assert.That(tableDetails?.Any() ?? false); 47 | } 48 | 49 | record SqliteNetContact 50 | { 51 | public string? Name { get; set; } 52 | public string? Email { get; set; } 53 | public long? Age { get; set; } 54 | public double? Height { get; set; } 55 | public byte[]? PrivateKey { get; set; } 56 | public double? Mana { get; set; } 57 | } 58 | 59 | [Test] 60 | public void InsertInTransaction_Works() 61 | { 62 | // arrange 63 | _database!.CreateNullableContactTable(); 64 | 65 | var contact = new NullableContact() 66 | { 67 | Name = "Steve", 68 | Email = "steve.rogers@superhero.com", 69 | Age = 35, 70 | Height = 170.5, 71 | PrivateKey = new byte[] { 1, 2, 3 }, 72 | Mana = new Numeric(24.5) 73 | }; 74 | 75 | // act 76 | _database!.BeginTransaction(); 77 | _database!.InsertNullableContact(contact); 78 | _database!.CommitTransaction(); 79 | 80 | // assert 81 | var actualContact = _sqliteNetConnection?.Query("Select * from nullable_contact;").First()!; 82 | Assert.That(contact.Name, Is.EqualTo(actualContact.Name)); 83 | Assert.That(contact.Age, Is.EqualTo(actualContact.Age)); 84 | Assert.That(contact.Email, Is.EqualTo(actualContact.Email)); 85 | Assert.That(contact.Height, Is.EqualTo(actualContact.Height)); 86 | Assert.That(contact.PrivateKey, Is.EqualTo(actualContact.PrivateKey)); 87 | Assert.That(contact.Mana.Value.GetReal(), Is.EqualTo(actualContact.Mana)); 88 | } 89 | 90 | [Test] 91 | public void RollbackTransaction_Works() 92 | { 93 | // arrange 94 | _database!.CreateNullableContactTable(); 95 | 96 | var contact = new NullableContact() 97 | { 98 | Name = "Steve", 99 | Email = "steve.rogers@superhero.com", 100 | Age = 35, 101 | Height = 170.5, 102 | PrivateKey = new byte[] { 1, 2, 3 }, 103 | Mana = new Numeric(24.5) 104 | }; 105 | 106 | // act 107 | _database!.BeginTransaction(); 108 | _database!.InsertNullableContact(contact); 109 | _database!.RollbackTransaction(); 110 | 111 | // assert 112 | var count = _sqliteNetConnection?.Query("Select * from nullable_contact;").Count(); 113 | Assert.That(count, Is.EqualTo(0)); 114 | } 115 | } -------------------------------------------------------------------------------- /IntegrationTests/UniqueConstraintTests.cs: -------------------------------------------------------------------------------- 1 | using SqlSrcGen; 2 | using SQLite; 3 | 4 | namespace Tests; 5 | 6 | public class UniqueConstraintTests 7 | { 8 | const string DatabaseName = "database.sql"; 9 | 10 | Database? _database; 11 | SQLiteConnection? _sqliteNetConnection; 12 | 13 | [OneTimeTearDown] 14 | public void FixtureTearDown() 15 | { 16 | _database?.Dispose(); 17 | } 18 | 19 | [SetUp] 20 | public void Setup() 21 | { 22 | if (File.Exists(DatabaseName)) 23 | { 24 | File.Delete(DatabaseName); 25 | } 26 | 27 | _database = new Database(DatabaseName); 28 | 29 | _sqliteNetConnection = new SQLiteConnection(DatabaseName); 30 | } 31 | 32 | public class TableDetails 33 | { 34 | public string? Name { get; set; } 35 | } 36 | 37 | [Test] 38 | public void CreateTable() 39 | { 40 | // arrange 41 | // act 42 | _database!.CreateUniqueConstraintTable(); 43 | 44 | // assert 45 | var tableDetails = _sqliteNetConnection?.Query("SELECT name FROM sqlite_master WHERE type='table' AND name='unique_constraint';"); 46 | Assert.That(tableDetails?.Any() ?? false); 47 | } 48 | 49 | record SqliteUniqueConstraint 50 | { 51 | public string? Name { get; set; } 52 | public string? Email { get; set; } 53 | } 54 | 55 | [Test] 56 | public void Get_Composite_SetsValuesReturnsTrue() 57 | { 58 | // arrange 59 | _database!.CreateUniqueConstraintTable(); 60 | 61 | _sqliteNetConnection?.Execute("INSERT INTO unique_constraint (name, email) VALUES (\"Steve\", \"steve.rogers@avengers.com\")"); 62 | _sqliteNetConnection?.Execute("INSERT INTO unique_constraint (name, email) VALUES (\"Bruce\", \"bruce.banner@avengers.com\")"); 63 | 64 | UniqueConstraint contact = new(); 65 | 66 | // act 67 | bool found = _database!.GetUniqueConstraint(contact, "Bruce", "bruce.banner@avengers.com"); 68 | 69 | // assert 70 | Assert.That(found, Is.True); 71 | Assert.That(contact.Name, Is.EqualTo("Bruce")); 72 | Assert.That(contact.Email, Is.EqualTo("bruce.banner@avengers.com")); 73 | } 74 | 75 | [Test] 76 | public void Get_Single_SetsValuesReturnsTrue() 77 | { 78 | // arrange 79 | _database!.CreateUniqueConstraintTable(); 80 | 81 | _sqliteNetConnection?.Execute("INSERT INTO unique_constraint (name, email) VALUES (\"Steve\", \"steve.rogers@avengers.com\")"); 82 | _sqliteNetConnection?.Execute("INSERT INTO unique_constraint (name, email) VALUES (\"Bruce\", \"bruce.banner@avengers.com\")"); 83 | 84 | UniqueConstraint contact = new(); 85 | 86 | // act 87 | bool found = _database!.GetUniqueConstraint(contact, "Bruce"); 88 | 89 | // assert 90 | Assert.That(found, Is.True); 91 | Assert.That(contact.Name, Is.EqualTo("Bruce")); 92 | Assert.That(contact.Email, Is.EqualTo("bruce.banner@avengers.com")); 93 | } 94 | 95 | [Test] 96 | public void Get_DoesntExist_ReturnsFalse() 97 | { 98 | // arrange 99 | _database!.CreateUniqueConstraintTable(); 100 | 101 | _sqliteNetConnection?.Execute("INSERT INTO unique_constraint (name, email) VALUES (\"Steve\", \"steve.rogers@avengers.com\")"); 102 | _sqliteNetConnection?.Execute("INSERT INTO unique_constraint (name, email) VALUES (\"Bruce\", \"bruce.banner@avengers.com\")"); 103 | 104 | UniqueConstraint contact = new(); 105 | 106 | // act 107 | bool found = _database!.GetUniqueConstraint(contact, "Bruce", "steve.rogers@avengers.com"); 108 | 109 | // assert 110 | Assert.That(found, Is.False); 111 | } 112 | 113 | [Test] 114 | public void Delete_Composite_RowDeleted() 115 | { 116 | // arrange 117 | _database!.CreateUniqueConstraintTable(); 118 | 119 | _sqliteNetConnection?.Execute("INSERT INTO unique_constraint (name, email) VALUES (\"Steve\", \"steve.rogers@avengers.com\")"); 120 | _sqliteNetConnection?.Execute("INSERT INTO unique_constraint (name, email) VALUES (\"Bruce\", \"bruce.banner@avengers.com\")"); 121 | 122 | // act 123 | _database!.DeleteUniqueConstraint("Bruce", "bruce.banner@avengers.com"); 124 | 125 | // assert 126 | var rows = _sqliteNetConnection?.Query("Select * from unique_constraint;")!; 127 | Assert.That(rows.Count(), Is.EqualTo(1)); 128 | Assert.That(rows.First()!.Name, Is.EqualTo("Steve")); 129 | } 130 | 131 | [Test] 132 | public void Delete_Single_RowDeleted() 133 | { 134 | // arrange 135 | _database!.CreateUniqueConstraintTable(); 136 | 137 | _sqliteNetConnection?.Execute("INSERT INTO unique_constraint (name, email) VALUES (\"Steve\", \"steve.rogers@avengers.com\")"); 138 | _sqliteNetConnection?.Execute("INSERT INTO unique_constraint (name, email) VALUES (\"Bruce\", \"bruce.banner@avengers.com\")"); 139 | 140 | // act 141 | _database!.DeleteUniqueConstraint("Bruce"); 142 | 143 | // assert 144 | var rows = _sqliteNetConnection?.Query("Select * from unique_constraint;")!; 145 | Assert.That(rows.Count(), Is.EqualTo(1)); 146 | Assert.That(rows.First()!.Name, Is.EqualTo("Steve")); 147 | } 148 | 149 | [Test] 150 | public void Delete_DoesntExists_NoRowDeleted() 151 | { 152 | // arrange 153 | _database!.CreateUniqueConstraintTable(); 154 | 155 | _sqliteNetConnection?.Execute("INSERT INTO unique_constraint (name, email) VALUES (\"Steve\", \"steve.rogers@avengers.com\")"); 156 | _sqliteNetConnection?.Execute("INSERT INTO unique_constraint (name, email) VALUES (\"Bruce\", \"bruce.banner@avengers.com\")"); 157 | 158 | // act 159 | _database!.DeleteUniqueConstraint("Bruce", "steve.rogers@avengers.com"); 160 | 161 | // assert 162 | var rows = _sqliteNetConnection?.Query("Select * from unique_constraint;")!; 163 | Assert.That(rows.Count(), Is.EqualTo(2)); 164 | } 165 | } -------------------------------------------------------------------------------- /IntegrationTests/Usings.cs: -------------------------------------------------------------------------------- 1 | global using NUnit.Framework; -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Daniel Hughes 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Playground/Contact_GetOldContacts_Void.sql: -------------------------------------------------------------------------------- 1 | SELECT * FROM contact; -------------------------------------------------------------------------------- /Playground/Playground.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | Exe 4 | net6.0 5 | enable 6 | enable 7 | 9 | 10 | 11 | 12 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /Playground/Program.cs: -------------------------------------------------------------------------------- 1 | using SqlSrcGen; 2 | 3 | string databaseName = "database.db"; 4 | if (File.Exists(databaseName)) 5 | { 6 | File.Delete(databaseName); 7 | } 8 | 9 | var database = new Database(databaseName); 10 | database.CreateContactTable(); 11 | 12 | database.InsertContact(new Contact() { Name = "Bob", Email = "bob@marley.com", Age = 12, Height = 167.8, PrivateKey = new byte[] { 1, 2, 3, 4 }, Mana = new Numeric(24.4d) }); 13 | 14 | var list = new List(); 15 | database.GetOldContacts(list); 16 | 17 | foreach (var contact in list) 18 | { 19 | Console.WriteLine($"Name: {contact.Name} Email: {contact.Email} Age: {contact.Age} Height: {contact.Height} PrivateKey: {string.Join(',', contact.PrivateKey!)}"); 20 | Console.WriteLine($"Mana: {contact.Mana?.GetReal()}"); 21 | } 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Playground/SqlSchema.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE contact (name Text, email Text, age Integer, height Real, privateKey Blob, mana Numeric); -------------------------------------------------------------------------------- /Playground/database.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trampster/SqlSrcGen/f2980a0ae5890d8d4af2b0d0a10d07b18807ecad/Playground/database.db -------------------------------------------------------------------------------- /PlaygroundNuget/PlaygroundNuget.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | Exe 4 | net6.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /PlaygroundNuget/Program.cs: -------------------------------------------------------------------------------- 1 | using SqlSrcGen; 2 | 3 | string databaseName = "database.sql"; 4 | if (File.Exists(databaseName)) 5 | { 6 | File.Delete(databaseName); 7 | } 8 | 9 | var database = new Database(databaseName); 10 | database.CreateContactTable(); 11 | 12 | database.InsertContact(new Contact() { Name = "Bob", Email = "bob@marley.com", Age = 12, Height = 167.8, PrivateKey = new byte[] { 1, 2, 3, 4 }, Mana = new Numeric(24.4d) }); 13 | 14 | var list = new List(); 15 | database.AllContacts(list); 16 | 17 | foreach (var contact in list) 18 | { 19 | Console.WriteLine($"Name: {contact.Name} Email: {contact.Email} Age: {contact.Age} Height: {contact.Height} PrivateKey: {string.Join(',', contact.PrivateKey!)}"); 20 | Console.WriteLine($"Mana: {contact.Mana?.GetReal()}"); 21 | } 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /PlaygroundNuget/SqlSchema.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE contact (name Text, email Text, age Integer, height Real, privateKey Blob, mana Numeric); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ![Logo](https://raw.githubusercontent.com/trampster/SqlSrcGen/main/Icon/SqlSrcGen64.png) SqlSrcGen 2 | SqlSrcGen is a SQL first, reflection free micro ORM for SQLite using c# source generators. 3 | The class definitions and Object Relational Mappings are created automatically from your SQL CREATE TABLE commands. 4 | 5 | ## Advantages 6 | * No need to manually define c# classes for your tables 7 | * High performance - mapping code is reflection free and optimized at compile time 8 | * SQL code is compile time checked 9 | * AOT friendly - no reflection 10 | 11 | ## Getting Started 12 | 1. Create a .sql file in your project which includes the CREATE TABLE SQL commands defining your database. 13 | ```sql 14 | CREATE TABLE contact (name Text not null primary key, email Text not null); 15 | ``` 16 | 17 | 2. Include that .sql file as AdditionalFiles in your .csproj 18 | 19 | ```xml 20 | 21 | 22 | 23 | ``` 24 | 3. Do crud operations on the tables 25 | 26 | ```c# 27 | var database = new Database(databaseName); 28 | 29 | // create the table 30 | database.CreateContactTable(); 31 | 32 | // insert a record 33 | database.InsertContact(new Contact() 34 | { 35 | Name = "Steve Rogers", 36 | Email = "steve@avengers.com" 37 | }); 38 | 39 | // query all records in the table 40 | var list = new List(); 41 | database.AllContacts(list); 42 | 43 | // get row via primary key, (only generated for tables with a primary key) 44 | var contact = new Contact(); 45 | bool found = database.GetContact(contact, "Steve Rogers"); 46 | 47 | // delete all rows from table 48 | database.DeleteAllContacts(); 49 | 50 | // delete a row via primary key (only generated for tables with a primary key) 51 | database.DeleteContact("Steve Rogers"); 52 | 53 | // Begin a transaction 54 | database.BeginTransaction(); 55 | 56 | // Commit a transaction 57 | database.CommitTransaction(); 58 | 59 | // Rollback a transaction 60 | database.RollbackTransaction(); 61 | ``` 62 | 63 | ## Future Work 64 | SqlSrcGen currently only supports basic crud operations generated directly from sql table definitions. Future features include: 65 | * Custom queries (select, joins etc) 66 | -------------------------------------------------------------------------------- /SqlSrcGen.Generator/CSharp.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | 3 | namespace SqlSrcGen.Generator; 4 | 5 | public static class CSharp 6 | { 7 | public static string ToCSharpName(string sqlName) 8 | { 9 | var builder = new StringBuilder(); 10 | if (sqlName.StartsWith("[") && sqlName.EndsWith("]")) 11 | { 12 | sqlName = sqlName.Substring(1, sqlName.Length - 2); 13 | } 14 | bool startsLower = false; 15 | if (sqlName.Length > 0 && char.IsLower(sqlName[0])) 16 | { 17 | startsLower = true; 18 | } 19 | bool isFirst = true; 20 | for (int index = 0; index < sqlName.Length; index++) 21 | { 22 | var charactor = sqlName[index]; 23 | if (charactor == '_') 24 | { 25 | isFirst = true; 26 | continue; 27 | } 28 | if (charactor == ' ') 29 | { 30 | isFirst = true; 31 | continue; 32 | } 33 | if (charactor == '\r') 34 | { 35 | isFirst = true; 36 | continue; 37 | } 38 | if (charactor == '\n') 39 | { 40 | isFirst = true; 41 | continue; 42 | } 43 | if (isFirst) 44 | { 45 | builder.Append(charactor.ToString().ToUpperInvariant()[0]); 46 | isFirst = false; 47 | continue; 48 | } 49 | 50 | builder.Append(startsLower ? charactor.ToString() : charactor.ToString().ToLowerInvariant()); 51 | } 52 | var cSharpName = builder.ToString(); 53 | if (IsKeyword(cSharpName)) 54 | { 55 | return $"@{cSharpName}"; 56 | } 57 | return cSharpName; 58 | } 59 | 60 | static bool IsKeyword(string word) 61 | { 62 | switch (word) 63 | { 64 | case "abstract": 65 | case "as": 66 | case "base": 67 | case "bool": 68 | case "break": 69 | case "byte": 70 | case "case": 71 | case "catch": 72 | case "char": 73 | case "checked": 74 | case "class": 75 | case "const": 76 | case "continue": 77 | case "decimal": 78 | case "default": 79 | case "delegate": 80 | case "do": 81 | case "double": 82 | case "else": 83 | case "enum": 84 | case "event": 85 | case "explicit": 86 | case "extern": 87 | case "false": 88 | case "finally": 89 | case "fixed": 90 | case "float": 91 | case "for": 92 | case "foreach": 93 | case "goto": 94 | case "if": 95 | case "implicit": 96 | case "in": 97 | case "int": 98 | case "interface": 99 | case "internal": 100 | case "is": 101 | case "lock": 102 | case "long": 103 | case "namespace": 104 | case "new": 105 | case "null": 106 | case "object": 107 | case "operator": 108 | case "out": 109 | case "override": 110 | case "params": 111 | case "private": 112 | case "protected": 113 | case "public": 114 | case "readonly": 115 | case "ref": 116 | case "return": 117 | case "sbyte": 118 | case "sealed": 119 | case "short": 120 | case "sizeof": 121 | case "stackalloc": 122 | case "static": 123 | case "string": 124 | case "struct": 125 | case "switch": 126 | case "this": 127 | case "throw": 128 | case "true": 129 | case "try": 130 | case "typeof": 131 | case "uint": 132 | case "ulong": 133 | case "unchecked": 134 | case "unsafe": 135 | case "ushort": 136 | case "using": 137 | case "virtual": 138 | case "void": 139 | case "volatile": 140 | case "while": 141 | return true; 142 | default: 143 | return false; 144 | } 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /SqlSrcGen.Generator/CollationParser.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace SqlSrcGen.Generator; 4 | 5 | public class CollationParser : Parser 6 | { 7 | public void ParseCollationStatement(ref int index, Span tokens) 8 | { 9 | Expect(index, tokens, "collate"); 10 | Increment(ref index, 1, tokens); 11 | 12 | switch (tokens.GetValue(index)) 13 | { 14 | case "nocase": 15 | case "binary": 16 | case "rtrim": 17 | break; 18 | default: 19 | DiagnosticsReporter!.Warning(ErrorCode.SSG0002, "Collation types other than nocase, binary and rtrim require custom collation creation", tokens[index]); 20 | break; 21 | } 22 | index++; 23 | } 24 | 25 | public void PraseCollateConstraint(Span columnDefinition, ref int index, Column column) 26 | { 27 | int startIndex = index; 28 | ParseCollationStatement(ref index, columnDefinition); 29 | if (column.TypeAffinity != TypeAffinity.TEXT) 30 | { 31 | DiagnosticsReporter!.Warning(ErrorCode.SSG0003, "Collation only affects Text columns", columnDefinition[startIndex]); 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /SqlSrcGen.Generator/DatabaseInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using System.Runtime.InteropServices; 4 | 5 | namespace SqlSrcGen.Generator 6 | { 7 | public enum TypeAffinity 8 | { 9 | TEXT, 10 | NUMERIC, 11 | INTEGER, 12 | REAL, 13 | BLOB 14 | } 15 | 16 | public record Column 17 | { 18 | public string SqlName { get; set; } = ""; 19 | public string SqlType { get; set; } = ""; 20 | public string CSharpName { get; set; } = ""; 21 | public string CSharpType { get; set; } = ""; 22 | public TypeAffinity TypeAffinity { get; set; } 23 | public bool NotNull { get; set; } 24 | public bool PrimaryKey { get; set; } 25 | public bool AutoIncrement { get; set; } 26 | public bool Unique { get; set; } 27 | 28 | public string CSharpParameterName => char.ToLowerInvariant(CSharpName![0]) + CSharpName.Substring(1, CSharpName.Length - 1); 29 | 30 | public Table? Table { get; set; } 31 | } 32 | 33 | public class Table 34 | { 35 | public string SqlName { get; set; } = ""; 36 | public string CSharpName { get; set; } = ""; 37 | 38 | readonly List _columns = new(); 39 | public IEnumerable Columns => _columns; 40 | 41 | public void AddColumn(Column column) 42 | { 43 | column.Table = this; 44 | _columns.Add(column); 45 | } 46 | 47 | public string CreateTable { get; set; } = ""; 48 | public bool Tempory { get; set; } 49 | 50 | public List PrimaryKey 51 | { 52 | get; 53 | set; 54 | } = new List(); 55 | 56 | public List> Unique 57 | { 58 | get; 59 | set; 60 | } = new List>(); 61 | 62 | public bool IsUniqueBy(List columns) 63 | { 64 | if (columns.Count == 1) 65 | { 66 | return Columns.Where(column => column.SqlName.ToLowerInvariant() == columns[0] && (column.PrimaryKey || column.Unique)).Any(); 67 | } 68 | foreach (var uniqueColumns in Unique.Concat(new List> { PrimaryKey })) 69 | { 70 | if (columns.Count != uniqueColumns.Count) 71 | { 72 | continue; 73 | } 74 | foreach (var column in columns) 75 | { 76 | // check that this column exists 77 | 78 | var lowerName = column.ToLowerInvariant(); 79 | 80 | if (!uniqueColumns.Any(column => column.SqlName.ToLowerInvariant() == lowerName)) 81 | { 82 | continue; 83 | } 84 | } 85 | return true; 86 | } 87 | return false; 88 | } 89 | } 90 | 91 | public class DatabaseInfo 92 | { 93 | public List Tables 94 | { 95 | get; 96 | set; 97 | } = new List
(); 98 | } 99 | } -------------------------------------------------------------------------------- /SqlSrcGen.Generator/DiagnosticsReporter.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis; 2 | using Microsoft.CodeAnalysis.Text; 3 | 4 | namespace SqlSrcGen.Generator; 5 | 6 | public interface IDiagnosticsReporter 7 | { 8 | string Path { get; set; } 9 | 10 | void Warning(ErrorCode code, string message, Token token); 11 | 12 | void Warning(ErrorCode errorCode, string message); 13 | 14 | void Error(ErrorCode errorCode, string message); 15 | } 16 | 17 | public class DiagnosticsReporter : IDiagnosticsReporter 18 | { 19 | readonly GeneratorExecutionContext _context; 20 | 21 | public DiagnosticsReporter(GeneratorExecutionContext context) 22 | { 23 | _context = context; 24 | } 25 | 26 | public string Path 27 | { 28 | get; 29 | set; 30 | } = ""; 31 | 32 | public void Warning(ErrorCode errorCode, string message, Token token) 33 | { 34 | _context.ReportDiagnostic( 35 | Diagnostic.Create( 36 | new DiagnosticDescriptor( 37 | errorCode.ToString(), 38 | "SQL Warning", 39 | message, 40 | "SQL", 41 | DiagnosticSeverity.Warning, 42 | true), 43 | Location.Create(Path, 44 | TextSpan.FromBounds(token.Position, token.Value.Length + token.Position), 45 | new LinePositionSpan( 46 | new LinePosition(token.Line, token.CharacterInLine), new LinePosition(token.Line, token.CharacterInLine + token.Value.Length))))); 47 | } 48 | 49 | public void Warning(ErrorCode errorCode, string message) 50 | { 51 | _context.ReportDiagnostic( 52 | Diagnostic.Create( 53 | new DiagnosticDescriptor( 54 | errorCode.ToString(), 55 | "SQL Warning", 56 | message, 57 | "SQL", 58 | DiagnosticSeverity.Warning, 59 | true), 60 | null)); 61 | } 62 | 63 | public void Error(ErrorCode errorCode, string message) 64 | { 65 | _context.ReportDiagnostic( 66 | Diagnostic.Create( 67 | new DiagnosticDescriptor( 68 | errorCode.ToString(), 69 | "SQL Warning", 70 | message, 71 | "SQL", 72 | DiagnosticSeverity.Error, 73 | true), 74 | null)); 75 | } 76 | } -------------------------------------------------------------------------------- /SqlSrcGen.Generator/ErrorCode.cs: -------------------------------------------------------------------------------- 1 | namespace SqlSrcGen.Generator; 2 | 3 | public enum ErrorCode 4 | { 5 | None, 6 | // Generic sql parsing error 7 | SSG0001, 8 | // custom collation type warning 9 | SSG0002, 10 | // COLLATE on non text field 11 | SSG0003, 12 | // MATCH on references constrain is not supported in sqlite, it's parsed but has no effect 13 | SSG0004, 14 | // Not supported 15 | SSG0005, 16 | // missing SqlSchema.sql 17 | SSG0006, 18 | // invalid .sql filename 19 | SSG0007, 20 | // type mismatch 21 | SSG0008, 22 | } -------------------------------------------------------------------------------- /SqlSrcGen.Generator/InvalidSqlException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace SqlSrcGen.Generator; 4 | 5 | public class InvalidSqlException : FormatException 6 | { 7 | public InvalidSqlException(string message, Token? token) : base(message) 8 | { 9 | Token = token; 10 | } 11 | 12 | public Token? Token 13 | { 14 | get; 15 | } 16 | } -------------------------------------------------------------------------------- /SqlSrcGen.Generator/LiteralValueParser.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace SqlSrcGen.Generator; 4 | 5 | public class LiteralValueParser : Parser 6 | { 7 | public bool Parse(ref int index, Span tokens) 8 | { 9 | switch (tokens.GetValue(index)) 10 | { 11 | case "null": 12 | case "true": 13 | case "false": 14 | case "current_time": 15 | case "current_date": 16 | case "current_timestamp": 17 | index++; 18 | return true; 19 | default: 20 | var token = tokens[index]; 21 | if (token.TokenType == TokenType.StringLiteral) 22 | { 23 | index++; 24 | return true; 25 | } 26 | if (token.TokenType == TokenType.NumericLiteral) 27 | { 28 | index++; 29 | return true; 30 | } 31 | if (token.TokenType == TokenType.BlobLiteral) 32 | { 33 | index++; 34 | return true; 35 | } 36 | return false; 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /SqlSrcGen.Generator/NumberParser.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace SqlSrcGen.Generator; 4 | 5 | public class NumberParser : Parser 6 | { 7 | public bool ParseSignedNumber(ref int index, Span tokens) 8 | { 9 | var tokenValue = tokens.GetValue(index); 10 | if (tokenValue == "+" || tokenValue == "-") 11 | { 12 | Increment(ref index, 1, tokens); 13 | if (tokens[index].TokenType != TokenType.NumericLiteral) 14 | { 15 | throw new InvalidSqlException($"missing numeric literal in signed number", tokens[index]); 16 | } 17 | index++; 18 | return true; 19 | } 20 | if (tokens[index].TokenType == TokenType.NumericLiteral) 21 | { 22 | index++; 23 | return true; 24 | } 25 | 26 | return false; 27 | } 28 | } -------------------------------------------------------------------------------- /SqlSrcGen.Generator/Praser.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | 4 | namespace SqlSrcGen.Generator; 5 | 6 | public interface IParser 7 | { 8 | public Query? Query 9 | { 10 | get; 11 | set; 12 | } 13 | 14 | public IDiagnosticsReporter? DiagnosticsReporter 15 | { 16 | get; 17 | set; 18 | } 19 | } 20 | 21 | public abstract class Parser : IParser 22 | { 23 | public Query? Query 24 | { 25 | get; 26 | set; 27 | } 28 | 29 | public virtual IDiagnosticsReporter? DiagnosticsReporter 30 | { 31 | get; 32 | set; 33 | } 34 | 35 | protected void Increment(ref int index, int amount, Span tokens) 36 | { 37 | AssertEnoughTokens(tokens, index + amount); 38 | index += amount; 39 | } 40 | 41 | protected void AssertEnoughTokens(Span tokens, int index) 42 | { 43 | if (tokens.Length == 0) 44 | { 45 | throw new InvalidSqlException("Ran out of tokens to parse command.", null); 46 | } 47 | if (index > tokens.Length - 1) 48 | { 49 | throw new InvalidSqlException("Ran out of tokens to parse command.", tokens[tokens.Length - 1]); 50 | } 51 | } 52 | 53 | protected void Expect(int index, Span tokens, params string[] values) 54 | { 55 | var value = tokens.GetValue(index); 56 | if (!values.Contains(value)) 57 | { 58 | throw new InvalidSqlException($"Expected {string.Join(" or ", values)}", tokens[index]); 59 | } 60 | } 61 | 62 | protected bool Is(int index, Span tokens, params string[] values) 63 | { 64 | var value = tokens.GetValue(index); 65 | return values.Contains(value); 66 | } 67 | 68 | /// 69 | /// returns true if the token is past the end of the tokens span 70 | /// 71 | protected bool IsEnd(int index, Span tokens) 72 | { 73 | return index >= tokens.Length; 74 | } 75 | } -------------------------------------------------------------------------------- /SqlSrcGen.Generator/Query.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace SqlSrcGen.Generator; 5 | 6 | public class Query 7 | { 8 | uint _highestPararmeter = 0; 9 | readonly Dictionary _parameters = new Dictionary(); 10 | readonly HashSet _sqlNames = new HashSet(); 11 | readonly HashSet _csharpNames = new HashSet(); 12 | 13 | public void AddAutoNumbered(Token token) 14 | { 15 | _highestPararmeter++; 16 | _parameters.Add(_highestPararmeter, new Parameter 17 | { 18 | Number = _highestPararmeter, 19 | CSharpName = $"param{_highestPararmeter}", 20 | SqlName = "" 21 | }); 22 | } 23 | 24 | public void AddNumberedParameter(uint number, Token token) 25 | { 26 | if (_highestPararmeter < number) 27 | { 28 | _highestPararmeter = number; 29 | } 30 | 31 | if (_parameters.ContainsKey(number)) 32 | { 33 | return; // already have it 34 | } 35 | _parameters.Add(number, new Parameter 36 | { 37 | Number = number, 38 | CSharpName = $"param{number}", 39 | SqlName = "" 40 | }); 41 | } 42 | 43 | void AddParameter(Parameter parameter, Token token) 44 | { 45 | _parameters.Add(parameter.Number, parameter); 46 | 47 | if (_csharpNames.Contains(parameter.CSharpName)) 48 | { 49 | throw new InvalidSqlException("Parameter produces the same c# name as an existing parameter", token); 50 | } 51 | _csharpNames.Add(parameter.CSharpName); 52 | _sqlNames.Add(parameter.SqlName); 53 | } 54 | 55 | public void AddNamedParameter(string sqlName, Token token) 56 | { 57 | // Because the parameter names end up as csharp names :, @ and $ parameters in 58 | // although unique in sql due to including the :, @ or $ are not in SqlScrGen 59 | // as we can't have those in 60 | 61 | 62 | if (_sqlNames.Contains(sqlName)) 63 | { 64 | return; // already have it 65 | } 66 | _highestPararmeter++; 67 | 68 | string name = sqlName.AsSpan().Slice(1).ToString(); 69 | 70 | AddParameter(new Parameter 71 | { 72 | Number = _highestPararmeter, 73 | SqlName = sqlName, 74 | CSharpName = CSharp.ToCSharpName(name), 75 | }, token); 76 | } 77 | 78 | } 79 | 80 | public class Parameter 81 | { 82 | public uint Number { get; set; } 83 | public string CSharpName { get; set; } = ""; 84 | public string SqlName { get; set; } = ""; 85 | } -------------------------------------------------------------------------------- /SqlSrcGen.Generator/QueryInfo.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using Microsoft.CodeAnalysis.CSharp.Syntax; 5 | 6 | namespace SqlSrcGen.Generator; 7 | 8 | public enum QueryType 9 | { 10 | Select, 11 | Insert, 12 | Delete 13 | } 14 | 15 | public class QueryInfo 16 | { 17 | public string MethodName { get; set; } = ""; 18 | public string QueryString { get; set; } = ""; 19 | public string CSharpResultType { get; set; } = ""; 20 | public string CSharpInputType { get; set; } = ""; 21 | public QueryType QueryType { get; set; } 22 | 23 | List, List>> _columnGenerators = new(); 24 | public void AddColumnsGenerator(Func, List> generator) 25 | { 26 | _columnGenerators.Add(generator); 27 | } 28 | 29 | public List Columns 30 | { 31 | get; 32 | set; 33 | } = new List(); 34 | 35 | readonly List<(string, Table)> _fromTables = new(); 36 | public List<(string, Table)> FromTables => _fromTables; 37 | 38 | 39 | 40 | public void Process() 41 | { 42 | foreach (var generator in _columnGenerators) 43 | { 44 | foreach (var column in generator(_fromTables)) 45 | { 46 | Columns.Add(column); 47 | } 48 | } 49 | 50 | //Fixup duplicate CSharp names 51 | for (int index = 0; index < Columns.Count; index++) 52 | { 53 | var column = Columns[index]; 54 | var cSharpName = column.CSharpName; 55 | 56 | var matchingIndexes = GetColumnsWithSameCSharpName(cSharpName, index); 57 | if (matchingIndexes.Any()) 58 | { 59 | var otherTablesIndexes = matchingIndexes 60 | .Where(matchingIndex => Columns[matchingIndex].Table != column.Table); 61 | if (otherTablesIndexes.Any()) 62 | { 63 | //need to add tableName to columns 64 | foreach (int matchingIndex in matchingIndexes) 65 | { 66 | var matchingColumn = Columns[matchingIndex]; 67 | if (matchingColumn.CSharpName != cSharpName) 68 | { 69 | //already renamed as RenameColumn renames all duplicate columns from the same table 70 | continue; 71 | } 72 | RenameColumn(matchingIndex, $"{matchingColumn.Table!.CSharpName}{matchingColumn.CSharpName}"); 73 | } 74 | RenameColumn(index, $"{column.Table!.CSharpName}{column.CSharpName}"); 75 | } 76 | } 77 | } 78 | } 79 | 80 | IEnumerable GetColumnsWithSameCSharpName(string csharpName, int excludeIndex) 81 | { 82 | for (int index = 0; index < Columns.Count; index++) 83 | { 84 | if (index == excludeIndex) 85 | { 86 | continue; 87 | } 88 | var column = Columns[index]; 89 | if (column.CSharpName == csharpName) 90 | { 91 | yield return index; 92 | } 93 | } 94 | } 95 | 96 | void RenameColumn(int index, string newName) 97 | { 98 | string nameToTry = newName; 99 | int attempt = 1; 100 | var column = Columns[index]; 101 | 102 | while (GetColumnsWithSameCSharpName(nameToTry, index).Any()) 103 | { 104 | nameToTry = $"{newName}{attempt}"; 105 | } 106 | // rename all columns in with the same table and sql name, (you can select the same column multiple times) 107 | for (int columnIndex = 0; columnIndex < Columns.Count; columnIndex++) 108 | { 109 | if (Columns[columnIndex].Table == column.Table && Columns[columnIndex].SqlName == column.SqlName) 110 | { 111 | Columns[columnIndex] = column with { CSharpName = nameToTry }; 112 | } 113 | } 114 | } 115 | } -------------------------------------------------------------------------------- /SqlSrcGen.Generator/SelectParser.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using Microsoft.CodeAnalysis; 5 | using Microsoft.CodeAnalysis.CSharp.Syntax; 6 | 7 | namespace SqlSrcGen.Generator; 8 | 9 | public class SelectParser : Parser 10 | { 11 | readonly DatabaseInfo _databaseInfo; 12 | readonly ExpressionParser _expressionParser; 13 | 14 | public SelectParser( 15 | DatabaseInfo databaseInfo, 16 | ExpressionParser expressionParser) 17 | { 18 | _databaseInfo = databaseInfo; 19 | _expressionParser = expressionParser; 20 | } 21 | 22 | public bool Parse(ref int index, Span tokens, QueryInfo queryInfo) 23 | { 24 | if (IsEnd(index, tokens)) 25 | { 26 | return false; 27 | } 28 | switch (tokens.GetValue(index)) 29 | { 30 | case "with": 31 | throw new NotImplementedException(); 32 | case "select": 33 | ParseSelectStatement(ref index, tokens, queryInfo); 34 | return true; 35 | case "values": 36 | throw new NotImplementedException(); 37 | } 38 | return false; 39 | } 40 | 41 | void ParseSelectStatement(ref int index, Span tokens, QueryInfo queryInfo) 42 | { 43 | Expect(index, tokens, "select"); 44 | queryInfo.QueryType = QueryType.Select; 45 | Increment(ref index, 1, tokens); 46 | switch (tokens.GetValue(index)) 47 | { 48 | case "distinct": 49 | case "all": 50 | Increment(ref index, 1, tokens); 51 | break; 52 | } 53 | while (true) 54 | { 55 | ParseResultColumn(ref index, tokens, queryInfo); 56 | if (tokens.GetValue(index) == ",") 57 | { 58 | Increment(ref index, 1, tokens); 59 | continue; 60 | } 61 | break; 62 | } 63 | 64 | if (tokens.GetValue(index) == "from") 65 | { 66 | ParseFromStatement(ref index, tokens, queryInfo); 67 | } 68 | } 69 | 70 | void ParseFromStatement(ref int index, Span tokens, QueryInfo queryInfo) 71 | { 72 | Expect(index, tokens, "from"); 73 | Increment(ref index, 1, tokens); 74 | while (true) 75 | { 76 | ParseTableOrSubQuery(ref index, tokens, queryInfo); 77 | if (IsEnd(index, tokens) || tokens.GetValue(index) != ",") 78 | { 79 | break; 80 | } 81 | Increment(ref index, 1, tokens); 82 | } 83 | } 84 | 85 | void ParseTableOrSubQuery(ref int index, Span tokens, QueryInfo queryInfo) 86 | { 87 | if (tokens.GetValue(index) == "(") 88 | { 89 | throw new NotImplementedException(); 90 | } 91 | if (tokens[index].TokenType == TokenType.Other && tokens.NextIs(index, ".")) 92 | { 93 | throw new InvalidSqlException("Attached databases are not supported", tokens[index]); 94 | } 95 | if (tokens[index].TokenType == TokenType.Other && tokens.NextIs(index, "(")) 96 | { 97 | throw new NotImplementedException("functions in from statements not supported yet"); 98 | } 99 | 100 | if (tokens[index].TokenType != TokenType.Other) 101 | { 102 | throw new InvalidSqlException("Expected table name", tokens[index]); 103 | } 104 | var tableName = tokens.GetValue(index); 105 | var table = _databaseInfo.Tables.Where(table => table.SqlName.ToLowerInvariant() == tableName.ToLowerInvariant()).FirstOrDefault(); 106 | if (table == null) 107 | { 108 | throw new InvalidSqlException("Table doesn't exist", tokens[index]); 109 | } 110 | index++; 111 | if (!IsEnd(index, tokens)) 112 | { 113 | if (tokens.GetValue(index) == "as") 114 | { 115 | Increment(ref index, 1, tokens); 116 | if (tokens[index].TokenType != TokenType.Other) 117 | { 118 | throw new InvalidSqlException("Expected table name", tokens[index]); 119 | } 120 | tableName = tokens[index].Value; 121 | Increment(ref index, 1, tokens); 122 | } 123 | if (tokens[index].TokenType == TokenType.Other) 124 | { 125 | tableName = tokens[index].Value; 126 | Increment(ref index, 1, tokens); 127 | } 128 | switch (tokens.GetValue(index)) 129 | { 130 | case "indexed": 131 | Increment(ref index, 1, tokens); 132 | Expect(index, tokens, "by"); 133 | Increment(ref index, 1, tokens); 134 | if (tokens[index].TokenType != TokenType.Other) 135 | { 136 | throw new InvalidSqlException("Expected index name", tokens[index]); 137 | } 138 | index++; 139 | break; 140 | case "not": 141 | Increment(ref index, 1, tokens); 142 | Expect(index, tokens, "indexed"); 143 | index++; 144 | break; 145 | } 146 | } 147 | queryInfo.FromTables.Add((tableName, table)); 148 | } 149 | 150 | void ParseResultColumn(ref int index, Span tokens, QueryInfo queryInfo) 151 | { 152 | var value = tokens.GetValue(index); 153 | if (value == "*") 154 | { 155 | queryInfo.AddColumnsGenerator(tables => 156 | { 157 | List columns = new(); 158 | foreach ((var name, var table) in tables) 159 | { 160 | columns.AddRange(table.Columns); 161 | } 162 | return columns; 163 | }); 164 | Increment(ref index, 1, tokens); 165 | return; 166 | } 167 | var columnIdentifierToken = tokens[index]; 168 | var expression = _expressionParser.Parse(ref index, tokens, null); 169 | if (expression == null || expression.ExpressionType != ExpressionType.Column) 170 | { 171 | throw new InvalidSqlException("Expected column identifier", columnIdentifierToken); 172 | } 173 | string? alias = null; 174 | if (tokens.GetValue(index) == "as") 175 | { 176 | Increment(ref index, 1, tokens); 177 | if (tokens[index].TokenType != TokenType.Other) 178 | { 179 | throw new InvalidSqlException("Expected column alias", tokens[index]); 180 | } 181 | alias = tokens[index].Value; 182 | } 183 | 184 | if (tokens[index].TokenType == TokenType.Other) 185 | { 186 | alias = tokens[index].Value; 187 | index++; 188 | } 189 | 190 | queryInfo.AddColumnsGenerator(tables => 191 | { 192 | var columns = new List(); 193 | if (string.IsNullOrEmpty(expression.TableName)) 194 | { 195 | foreach (var table in tables) 196 | { 197 | foreach (var column in table.Item2.Columns) 198 | { 199 | if (column.SqlName.ToLowerInvariant() == expression.ColumnName?.ToLowerInvariant()) 200 | { 201 | if (columns.Count() != 0) 202 | { 203 | throw new InvalidSqlException("Ambiguous column name", columnIdentifierToken); 204 | } 205 | columns.Add(AliasColumn(column, alias)); 206 | } 207 | } 208 | } 209 | } 210 | else if (expression.ColumnName == "*") 211 | { 212 | foreach (var table in tables) 213 | { 214 | if (table.Item2.SqlName.ToLowerInvariant() == expression.TableName?.ToLowerInvariant()) 215 | { 216 | foreach (var column in table.Item2.Columns) 217 | { 218 | columns.Add(column); 219 | } 220 | } 221 | } 222 | } 223 | else 224 | { 225 | foreach (var table in tables) 226 | { 227 | if (table.Item2.SqlName.ToLowerInvariant() == expression.TableName?.ToLowerInvariant()) 228 | { 229 | foreach (var column in table.Item2.Columns) 230 | { 231 | if (column.SqlName.ToLowerInvariant() == expression.ColumnName?.ToLowerInvariant()) 232 | { 233 | columns.Add(AliasColumn(column, alias)); 234 | } 235 | } 236 | } 237 | } 238 | } 239 | return columns; 240 | }); 241 | } 242 | 243 | Column AliasColumn(Column column, string? alias) 244 | { 245 | if (alias == null) 246 | { 247 | return column; 248 | } 249 | return column with { SqlName = alias, CSharpName = CSharp.ToCSharpName(alias) }; 250 | } 251 | } -------------------------------------------------------------------------------- /SqlSrcGen.Generator/SourceBuilder.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | 3 | namespace SqlSrcGen.Generator 4 | { 5 | public class SourceBuilder 6 | { 7 | readonly StringBuilder _builder = new StringBuilder(); 8 | 9 | string _indent = ""; 10 | 11 | public void AppendLine(string line) => _builder.AppendLine($"{_indent}{line}"); 12 | 13 | public void IncreaseIndent() 14 | { 15 | _indent += " "; 16 | } 17 | 18 | public void AppendLine() => _builder.AppendLine(); 19 | 20 | public void Append(string value) => _builder.Append(value); 21 | 22 | public void AppendStart(string value) => _builder.Append($"{_indent}{value}"); 23 | 24 | public void DecreaseIndent() 25 | { 26 | if (_indent.Length >= 4) 27 | { 28 | _indent = _indent.Substring(0, _indent.Length - 4); 29 | } 30 | } 31 | 32 | public override string ToString() => _builder.ToString(); 33 | } 34 | } -------------------------------------------------------------------------------- /SqlSrcGen.Generator/SpanTokenExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace SqlSrcGen.Generator 4 | { 5 | public static class SpanTokenExtensions 6 | { 7 | public static string GetValue(this Span tokens, int index) 8 | { 9 | if (index > tokens.Length - 1) 10 | { 11 | if (tokens.Length == 0) 12 | { 13 | throw new InvalidSqlException("Ran out of tokens to parse command.", null); 14 | } 15 | throw new InvalidSqlException("Ran out of tokens to parse command.", tokens[tokens.Length - 1]); 16 | } 17 | return tokens[index].Value.ToLowerInvariant(); 18 | } 19 | 20 | public static bool HasIndex(this Span tokens, int index) 21 | { 22 | return index < tokens.Length; 23 | } 24 | 25 | public static bool NextIs(this Span tokens, int index, string value) 26 | { 27 | if (!tokens.HasIndex(index + 1)) 28 | { 29 | return false; 30 | } 31 | return tokens.GetValue(index + 1) == value; 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /SqlSrcGen.Generator/SqlSrcGen.Generator.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | 10.0 6 | true 7 | SqlSrcGen.Generator 8 | 0.1.3 9 | Daniel Hughes 10 | enable 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /SqlSrcGen.Generator/Token.cs: -------------------------------------------------------------------------------- 1 | namespace SqlSrcGen.Generator; 2 | 3 | public enum TokenType 4 | { 5 | StringLiteral, 6 | NumericLiteral, 7 | BlobLiteral, 8 | Operator, 9 | Symbol, 10 | Keyword, 11 | Other 12 | } 13 | 14 | public record Token 15 | { 16 | public Token() 17 | { 18 | } 19 | 20 | public Token(string value, int position, int line, int characterInLine, TokenType tokenType) 21 | { 22 | Value = value; 23 | Position = position; 24 | Line = line; 25 | CharacterInLine = characterInLine; 26 | TokenType = tokenType; 27 | } 28 | 29 | public string Value { get; set; } = ""; 30 | public int Position { get; set; } 31 | // zero based line index 32 | public int Line { get; set; } 33 | // zero based index of character in line 34 | public int CharacterInLine { get; set; } 35 | public TokenType TokenType { get; set; } = TokenType.Other; 36 | 37 | public bool BinaryOperator 38 | { 39 | get; 40 | set; 41 | } 42 | 43 | public bool UnaryOperator 44 | { 45 | get; 46 | set; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /SqlSrcGen.Generator/TypeNameParser.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text; 3 | 4 | namespace SqlSrcGen.Generator; 5 | 6 | public class TypeNameParser : Parser 7 | { 8 | public void ParseTypeName(ref int index, Span tokens, out string type) 9 | { 10 | AssertEnoughTokens(tokens, index); 11 | var typeBuilder = new StringBuilder(); 12 | typeBuilder.Append(tokens[index].Value); 13 | Increment(ref index, 1, tokens); 14 | int numericLiteralCount = 0; 15 | bool lastTokenNumericLiteral = false; 16 | if (tokens.GetValue(index) == "(") 17 | { 18 | Increment(ref index, 1, tokens); 19 | typeBuilder.Append("("); 20 | for (; index < tokens.Length; index++) 21 | { 22 | var token = tokens[index]; 23 | if (token.Value == ")") 24 | { 25 | typeBuilder.Append(token.Value); 26 | 27 | type = typeBuilder.ToString(); 28 | index++; 29 | return; 30 | } 31 | if (lastTokenNumericLiteral) 32 | { 33 | lastTokenNumericLiteral = false; 34 | if (token.Value != ",") 35 | { 36 | throw new InvalidSqlException("expected ',''", token); 37 | } 38 | typeBuilder.Append(","); 39 | continue; 40 | } 41 | if (!long.TryParse(token.Value, out long value)) 42 | { 43 | throw new InvalidSqlException("expected signed numeric-literal", token); 44 | } 45 | lastTokenNumericLiteral = true; 46 | typeBuilder.Append(token.Value); 47 | numericLiteralCount++; 48 | if (numericLiteralCount > 2) 49 | { 50 | throw new InvalidSqlException("type-name can't have more than two numeric-literals", token); 51 | } 52 | } 53 | throw new InvalidSqlException("Ran out of tokens trying to parse type", tokens[0]); 54 | } 55 | type = typeBuilder.ToString(); 56 | } 57 | } -------------------------------------------------------------------------------- /SqlSrcGen.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.0.31903.59 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Benchmarks", "Benchmarks\Benchmarks.csproj", "{CD000621-BEE9-40E0-89A1-5B3C24D290F9}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IntegrationTests", "IntegrationTests\IntegrationTests.csproj", "{E4D00B29-906F-4850-A41E-27A7080DFC29}" 9 | EndProject 10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Playground", "Playground\Playground.csproj", "{9BF599B9-E089-4E57-BF25-A797D245F79B}" 11 | EndProject 12 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SqlSrcGen.Generator", "SqlSrcGen.Generator\SqlSrcGen.Generator.csproj", "{072D96A4-479A-46A5-8FFE-A49C739EBA78}" 13 | EndProject 14 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SqlSrcGen", "SqlSrcGen\SqlSrcGen.csproj", "{90673840-6F12-4188-B4E5-F850E2C35457}" 15 | EndProject 16 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tests", "Tests\Tests.csproj", "{CDA73D36-9BE9-4703-86B1-FB94BC52BCC7}" 17 | EndProject 18 | Global 19 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 20 | Debug|Any CPU = Debug|Any CPU 21 | Release|Any CPU = Release|Any CPU 22 | EndGlobalSection 23 | GlobalSection(SolutionProperties) = preSolution 24 | HideSolutionNode = FALSE 25 | EndGlobalSection 26 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 27 | {CD000621-BEE9-40E0-89A1-5B3C24D290F9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 28 | {CD000621-BEE9-40E0-89A1-5B3C24D290F9}.Debug|Any CPU.Build.0 = Debug|Any CPU 29 | {CD000621-BEE9-40E0-89A1-5B3C24D290F9}.Release|Any CPU.ActiveCfg = Release|Any CPU 30 | {CD000621-BEE9-40E0-89A1-5B3C24D290F9}.Release|Any CPU.Build.0 = Release|Any CPU 31 | {E4D00B29-906F-4850-A41E-27A7080DFC29}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 32 | {E4D00B29-906F-4850-A41E-27A7080DFC29}.Debug|Any CPU.Build.0 = Debug|Any CPU 33 | {E4D00B29-906F-4850-A41E-27A7080DFC29}.Release|Any CPU.ActiveCfg = Release|Any CPU 34 | {E4D00B29-906F-4850-A41E-27A7080DFC29}.Release|Any CPU.Build.0 = Release|Any CPU 35 | {9BF599B9-E089-4E57-BF25-A797D245F79B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 36 | {9BF599B9-E089-4E57-BF25-A797D245F79B}.Debug|Any CPU.Build.0 = Debug|Any CPU 37 | {9BF599B9-E089-4E57-BF25-A797D245F79B}.Release|Any CPU.ActiveCfg = Release|Any CPU 38 | {9BF599B9-E089-4E57-BF25-A797D245F79B}.Release|Any CPU.Build.0 = Release|Any CPU 39 | {072D96A4-479A-46A5-8FFE-A49C739EBA78}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 40 | {072D96A4-479A-46A5-8FFE-A49C739EBA78}.Debug|Any CPU.Build.0 = Debug|Any CPU 41 | {072D96A4-479A-46A5-8FFE-A49C739EBA78}.Release|Any CPU.ActiveCfg = Release|Any CPU 42 | {072D96A4-479A-46A5-8FFE-A49C739EBA78}.Release|Any CPU.Build.0 = Release|Any CPU 43 | {90673840-6F12-4188-B4E5-F850E2C35457}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 44 | {90673840-6F12-4188-B4E5-F850E2C35457}.Debug|Any CPU.Build.0 = Debug|Any CPU 45 | {90673840-6F12-4188-B4E5-F850E2C35457}.Release|Any CPU.ActiveCfg = Release|Any CPU 46 | {90673840-6F12-4188-B4E5-F850E2C35457}.Release|Any CPU.Build.0 = Release|Any CPU 47 | {CDA73D36-9BE9-4703-86B1-FB94BC52BCC7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 48 | {CDA73D36-9BE9-4703-86B1-FB94BC52BCC7}.Debug|Any CPU.Build.0 = Debug|Any CPU 49 | {CDA73D36-9BE9-4703-86B1-FB94BC52BCC7}.Release|Any CPU.ActiveCfg = Release|Any CPU 50 | {CDA73D36-9BE9-4703-86B1-FB94BC52BCC7}.Release|Any CPU.Build.0 = Release|Any CPU 51 | EndGlobalSection 52 | EndGlobal 53 | -------------------------------------------------------------------------------- /SqlSrcGen/Contact.cs: -------------------------------------------------------------------------------- 1 | namespace SqlSrcGen; 2 | 3 | public class Contact1 4 | { 5 | public string Name 6 | { 7 | get; 8 | set; 9 | } = ""; 10 | 11 | public string Email 12 | { 13 | get; 14 | set; 15 | } = ""; 16 | } -------------------------------------------------------------------------------- /SqlSrcGen/Numeric.cs: -------------------------------------------------------------------------------- 1 | 2 | using System.Runtime.InteropServices; 3 | 4 | namespace SqlSrcGen; 5 | 6 | public enum NumericType 7 | { 8 | Integer, 9 | Text, 10 | Real, 11 | Blob, 12 | Null 13 | } 14 | 15 | public struct Numeric 16 | { 17 | double? _realValue = null; 18 | long? _integerValue = 0; 19 | string? _textValue = null; 20 | byte[]? _blobValue = null; 21 | NumericType _type = NumericType.Integer; 22 | 23 | 24 | public Numeric(double value) 25 | { 26 | _type = NumericType.Real; 27 | _realValue = value; 28 | } 29 | 30 | public Numeric(long value) 31 | { 32 | _type = NumericType.Integer; 33 | _integerValue = value; 34 | } 35 | 36 | public Numeric(string value) 37 | { 38 | _type = NumericType.Text; 39 | _textValue = value; 40 | } 41 | 42 | public Numeric(byte[] value) 43 | { 44 | _type = NumericType.Blob; 45 | _blobValue = value; 46 | } 47 | 48 | public double GetReal() 49 | { 50 | if (_type != NumericType.Real) 51 | { 52 | throw new InvalidOperationException($"Type access mismatch"); 53 | } 54 | return _realValue!.Value; 55 | } 56 | 57 | public long GetInteger() 58 | { 59 | if (_type != NumericType.Integer) 60 | { 61 | throw new InvalidOperationException($"Type access mismatch"); 62 | } 63 | return _integerValue!.Value; 64 | } 65 | 66 | public string GetText() 67 | { 68 | if (_type != NumericType.Text) 69 | { 70 | throw new InvalidOperationException($"Type access mismatch"); 71 | } 72 | return _textValue!; 73 | } 74 | 75 | public byte[] GetBlob() 76 | { 77 | if (_type != NumericType.Text) 78 | { 79 | throw new InvalidOperationException($"Type access mismatch"); 80 | } 81 | return _blobValue!; 82 | } 83 | 84 | public NumericType NumericType => _type; 85 | } 86 | 87 | public static class SqliteNumericSqliteMethods 88 | { 89 | public static Numeric Read(IntPtr statement, int columnIndex) 90 | { 91 | var type = SqliteNativeMethods.sqlite3_column_type(statement, columnIndex); 92 | return type switch 93 | { 94 | SqliteDataType.Integer => new Numeric(SqliteNativeMethods.sqlite3_column_int64(statement, columnIndex)), 95 | SqliteDataType.Float => new Numeric(SqliteNativeMethods.sqlite3_column_double(statement, columnIndex)), 96 | SqliteDataType.Text => new Numeric(Marshal.PtrToStringUni(SqliteNativeMethods.sqlite3_column_text16(statement, columnIndex))), 97 | SqliteDataType.Blob => new Numeric(GetBlob(statement, columnIndex)), 98 | _ => throw new InvalidOperationException($"Unexpected SqliteDataType in numeric column {type}") 99 | }; 100 | } 101 | 102 | static byte[] GetBlob(IntPtr statement, int columnIndex) 103 | { 104 | IntPtr blobPtr = SqliteNativeMethods.sqlite3_column_blob(statement, columnIndex); 105 | int length = SqliteNativeMethods.sqlite3_column_bytes(statement, columnIndex); 106 | var blob = new byte[length]; 107 | Marshal.Copy(blobPtr, blob, 0, blob.Length); 108 | return blob; 109 | } 110 | 111 | public static void Write(IntPtr statement, int columnIndex, Numeric value) 112 | { 113 | switch (value.NumericType) 114 | { 115 | case NumericType.Integer: 116 | SqliteNativeMethods.sqlite3_bind_int64(statement, columnIndex, value.GetInteger()); 117 | break; 118 | case NumericType.Real: 119 | SqliteNativeMethods.sqlite3_bind_double(statement, columnIndex, value.GetReal()); 120 | break; 121 | case NumericType.Text: 122 | var textValue = value.GetText(); 123 | SqliteNativeMethods.sqlite3_bind_text16(statement, columnIndex, textValue, -1, SqliteNativeMethods.SQLITE_TRANSIENT); 124 | break; 125 | case NumericType.Blob: 126 | var blobValue = value.GetBlob(); 127 | SqliteNativeMethods.sqlite3_bind_blob(statement, columnIndex, blobValue, blobValue.Length, SqliteNativeMethods.SQLITE_TRANSIENT); 128 | break; 129 | default: 130 | throw new InvalidOperationException($"Unexpected SqliteDataType in numeric column {value.NumericType}"); 131 | } 132 | } 133 | } -------------------------------------------------------------------------------- /SqlSrcGen/SqlSrcGen.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | enable 6 | enable 7 | 10.0 8 | SqlSrcGen 9 | 0.1.3 10 | Daniel Hughes 11 | README.md 12 | Copyright © Daniel Hughes 2023 13 | LICENSE 14 | SqlSrcGen256.png 15 | https://github.com/trampster/SqlSrcGen 16 | true 17 | sqlite;orm;sourcegenerator 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | all 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /SqlSrcGen/Sqlite.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | using System.Text; 3 | 4 | namespace SqlSrcGen; 5 | 6 | public class Sqlite : IDisposable 7 | { 8 | readonly IntPtr _dbHandle; 9 | 10 | public Sqlite(string filename) 11 | { 12 | var result = SqliteNativeMethods.sqlite3_open(filename, out _dbHandle); 13 | if (result != Result.Ok) 14 | { 15 | throw new SqliteException($"Failed to open database {filename}", result); 16 | } 17 | } 18 | 19 | static readonly byte[] _createContactBytes = new byte[] { 0x43, 0x52, 0x45, 0x41, 0x54, 0x45, 0x20, 0x54, 0x41, 0x42, 0x4C, 0x45, 0x20, 0x63, 0x6F, 0x6E, 0x74, 0x61, 0x63, 0x74, 0x20, 0x28, 0x20, 0x6E, 0x61, 0x6D, 0x65, 0x20, 0x54, 0x65, 0x78, 0x74, 0x20, 0x2C, 0x20, 0x65, 0x6D, 0x61, 0x69, 0x6C, 0x20, 0x54, 0x65, 0x78, 0x74, 0x20, 0x29, 0x20, 0x3B }; 20 | 21 | public void CreateContactTable() 22 | { 23 | //var queryBytes = UTF8Encoding.UTF8.GetBytes("CREATE TABLE contact ( name Text , email Text ) ;"); 24 | 25 | // if (!queryBytes.SequenceEqual(_createContactBytes)) 26 | // { 27 | // throw new Exception("it's different"); 28 | // } 29 | 30 | 31 | var result = SqliteNativeMethods.sqlite3_prepare_v2(_dbHandle, _createContactBytes, _createContactBytes.Length, out IntPtr statementPtr, IntPtr.Zero); 32 | try 33 | { 34 | if (result != Result.Ok) 35 | { 36 | throw new SqliteException("Failed to prepare sqlite statement CREATE TABLE contact ( name Text , email Text ) ;", result); 37 | } 38 | 39 | result = SqliteNativeMethods.sqlite3_step(statementPtr); 40 | 41 | if (result != Result.Done) 42 | { 43 | throw new SqliteException("Failed to execute sqlite statement CREATE TABLE contact ( name Text , email Text ) ;", result); 44 | 45 | } 46 | } 47 | finally 48 | { 49 | SqliteNativeMethods.sqlite3_finalize(statementPtr); 50 | } 51 | } 52 | 53 | public void Query(string query) 54 | { 55 | //TODO: requires allocation so we do this ahead of time in the source generator 56 | var queryBytes = UTF8Encoding.UTF8.GetBytes(query); 57 | 58 | var result = SqliteNativeMethods.sqlite3_prepare_v2(_dbHandle, queryBytes, queryBytes.Length, out IntPtr statementPtr, IntPtr.Zero); 59 | 60 | try 61 | { 62 | if (result != Result.Ok) 63 | { 64 | throw new SqliteException($"Failed to prepare sqlite statement {query}", result); 65 | } 66 | 67 | result = SqliteNativeMethods.sqlite3_step(statementPtr); 68 | 69 | if (result != Result.Done) 70 | { 71 | throw new SqliteException($"Failed to execute sqlite statement {query}", result); 72 | 73 | } 74 | } 75 | finally 76 | { 77 | SqliteNativeMethods.sqlite3_finalize(statementPtr); 78 | } 79 | } 80 | 81 | static readonly byte[] _queryAllContactsBytes = UTF8Encoding.UTF8.GetBytes("SELECT * FROM contact;"); 82 | IntPtr _queryContactStatement = IntPtr.Zero; 83 | 84 | public void AllContacts(string query, List list) 85 | { 86 | if (_queryContactStatement == IntPtr.Zero) 87 | { 88 | var prepareResult = SqliteNativeMethods.sqlite3_prepare_v2(_dbHandle, _queryAllContactsBytes, _queryAllContactsBytes.Length, out _queryContactStatement, IntPtr.Zero); 89 | if (prepareResult != Result.Ok) 90 | { 91 | throw new SqliteException($"Failed to prepare sqlite statement {query}", prepareResult); 92 | } 93 | } 94 | 95 | var result = SqliteNativeMethods.sqlite3_step(_queryContactStatement); 96 | 97 | int index = 0; 98 | while (result == Result.Row) 99 | { 100 | Contact1? contact = null; 101 | if (index >= list.Count) 102 | { 103 | contact = new Contact1(); 104 | Console.WriteLine("Making new contact"); 105 | list.Add(contact); 106 | } 107 | else 108 | { 109 | contact = list[index]; 110 | } 111 | 112 | contact!.Name = Marshal.PtrToStringUni(SqliteNativeMethods.sqlite3_column_text16(_queryContactStatement, 0))!; 113 | contact!.Email = Marshal.PtrToStringUni(SqliteNativeMethods.sqlite3_column_text16(_queryContactStatement, 1))!; 114 | 115 | result = SqliteNativeMethods.sqlite3_step(_queryContactStatement); 116 | } 117 | 118 | if (result != Result.Done) 119 | { 120 | throw new SqliteException($"Failed to execute sqlite statement {query}", result); 121 | } 122 | 123 | // reset the statement so it's ready for next time 124 | result = SqliteNativeMethods.sqlite3_reset(_queryContactStatement); 125 | if (result != Result.Ok) 126 | { 127 | throw new SqliteException($"Failed to reset sqlite statement {query}", result); 128 | } 129 | } 130 | 131 | static readonly byte[] InsertContactQueryBytes = UTF8Encoding.UTF8.GetBytes("INSERT INTO contact (name, email) VALUES (?, ?);"); 132 | IntPtr _insertContactStatementPtr = IntPtr.Zero; 133 | 134 | public void InsertContact(Contact1 contact) 135 | { 136 | if (_insertContactStatementPtr == IntPtr.Zero) 137 | { 138 | var prepareResult = SqliteNativeMethods.sqlite3_prepare_v2(_dbHandle, InsertContactQueryBytes, InsertContactQueryBytes.Length, out IntPtr statementPtr, IntPtr.Zero); 139 | 140 | if (prepareResult != Result.Ok) 141 | { 142 | throw new SqliteException($"Failed to prepare statement to insert contact", prepareResult); 143 | } 144 | } 145 | 146 | SqliteNativeMethods.sqlite3_bind_text16(_insertContactStatementPtr, 1, contact.Name, -1, SqliteNativeMethods.SQLITE_TRANSIENT); 147 | SqliteNativeMethods.sqlite3_bind_text16(_insertContactStatementPtr, 2, contact.Email, -1, SqliteNativeMethods.SQLITE_TRANSIENT); 148 | 149 | var result = SqliteNativeMethods.sqlite3_step(_insertContactStatementPtr); 150 | 151 | if (result != Result.Done) 152 | { 153 | throw new SqliteException($"Failed to insert contact {contact}", result); 154 | } 155 | 156 | SqliteNativeMethods.sqlite3_reset(_insertContactStatementPtr); 157 | } 158 | 159 | bool disposedValue; 160 | 161 | protected virtual void Dispose(bool disposing) 162 | { 163 | if (!disposedValue) 164 | { 165 | SqliteNativeMethods.sqlite3_close(_dbHandle); 166 | 167 | if (_insertContactStatementPtr != IntPtr.Zero) 168 | { 169 | SqliteNativeMethods.sqlite3_finalize(_insertContactStatementPtr); 170 | _insertContactStatementPtr = IntPtr.Zero; 171 | } 172 | 173 | disposedValue = true; 174 | } 175 | } 176 | 177 | ~Sqlite() 178 | { 179 | // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method 180 | Dispose(disposing: false); 181 | } 182 | 183 | public void Dispose() 184 | { 185 | // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method 186 | Dispose(disposing: true); 187 | GC.SuppressFinalize(this); 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /SqlSrcGen/SqliteException.cs: -------------------------------------------------------------------------------- 1 | namespace SqlSrcGen; 2 | 3 | public class SqliteException : IOException 4 | { 5 | readonly Result _result; 6 | 7 | public SqliteException(string message, Result result) : base(string.Format($"{message} with Result {result}")) 8 | { 9 | _result = result; 10 | } 11 | 12 | public Result Result => _result; 13 | } -------------------------------------------------------------------------------- /SqlSrcGen/SqliteNativeMethods.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | 3 | namespace SqlSrcGen; 4 | 5 | public enum Result 6 | { 7 | Ok = 0, 8 | Error = 1, 9 | Busy = 5, 10 | Misuse = 21, 11 | Row = 100, 12 | Done = 101 13 | } 14 | 15 | public enum SqliteDataType 16 | { 17 | Integer = 1, 18 | Float = 2, 19 | Blob = 4, 20 | Null = 5, 21 | Text = 3 22 | } 23 | 24 | public static class SqliteNativeMethods 25 | { 26 | const string SqliteLib = "/usr/lib/x86_64-linux-gnu/libsqlite3.so.0.8.6"; 27 | 28 | [DllImport(SqliteLib, EntryPoint = "sqlite3_open", CallingConvention = CallingConvention.Cdecl)] 29 | public static extern Result sqlite3_open([MarshalAs(UnmanagedType.LPStr)] string filename, out IntPtr db); 30 | 31 | [DllImport(SqliteLib, CallingConvention = CallingConvention.Cdecl)] 32 | public static extern Result sqlite3_close(IntPtr db); 33 | 34 | [DllImport(SqliteLib, CallingConvention = CallingConvention.Cdecl)] 35 | public static extern Result sqlite3_prepare_v2(IntPtr db, byte[] query, int numBytes, out IntPtr statementPtr, IntPtr pzTail); 36 | 37 | [DllImport(SqliteLib, CallingConvention = CallingConvention.Cdecl)] 38 | public static extern Result sqlite3_reset(IntPtr statementPtr); 39 | 40 | [DllImport(SqliteLib, CallingConvention = CallingConvention.Cdecl)] 41 | public static extern Result sqlite3_step(IntPtr statementPtr); 42 | 43 | [DllImport(SqliteLib, CallingConvention = CallingConvention.Cdecl)] 44 | public static extern IntPtr sqlite3_column_name(IntPtr statementPtr, int index); 45 | 46 | [DllImport(SqliteLib, CallingConvention = CallingConvention.Cdecl)] 47 | public static extern IntPtr sqlite3_column_text16(IntPtr statementPtr, int index); 48 | 49 | [DllImport(SqliteLib, CallingConvention = CallingConvention.Cdecl)] 50 | public static extern long sqlite3_column_int64(IntPtr statementPtr, int index); 51 | 52 | [DllImport(SqliteLib, CallingConvention = CallingConvention.Cdecl)] 53 | public static extern double sqlite3_column_double(IntPtr statementPtr, int index); 54 | 55 | [DllImport(SqliteLib, CallingConvention = CallingConvention.Cdecl)] 56 | public static extern SqliteDataType sqlite3_column_type(IntPtr statementPtr, int index); 57 | 58 | [DllImport(SqliteLib, CallingConvention = CallingConvention.Cdecl)] 59 | public static extern IntPtr sqlite3_column_blob(IntPtr statementPtr, int index); 60 | 61 | [DllImport(SqliteLib, CallingConvention = CallingConvention.Cdecl)] 62 | public static extern int sqlite3_column_bytes(IntPtr statementPtr, int index); 63 | 64 | [DllImport(SqliteLib, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Unicode)] 65 | public static extern int sqlite3_bind_text16(IntPtr statementPtr, int index, [MarshalAs(UnmanagedType.LPWStr)] string val, int length, IntPtr free); 66 | 67 | [DllImport(SqliteLib, CallingConvention = CallingConvention.Cdecl)] 68 | public static extern int sqlite3_bind_int64(IntPtr statementPtr, int index, long value); 69 | 70 | [DllImport(SqliteLib, CallingConvention = CallingConvention.Cdecl)] 71 | public static extern int sqlite3_bind_double(IntPtr statementPtr, int index, double value); 72 | 73 | [DllImport(SqliteLib, CallingConvention = CallingConvention.Cdecl)] 74 | public static extern int sqlite3_bind_blob(IntPtr statementPtr, int index, byte[] blob, int length, IntPtr free); 75 | 76 | [DllImport(SqliteLib, CallingConvention = CallingConvention.Cdecl)] 77 | public static extern int sqlite3_bind_null(IntPtr statementPtr, int index); 78 | 79 | [DllImport(SqliteLib, CallingConvention = CallingConvention.Cdecl)] 80 | public static extern Result sqlite3_finalize(IntPtr statementPtr); 81 | 82 | public static IntPtr SQLITE_TRANSIENT = new IntPtr(-1); 83 | 84 | } -------------------------------------------------------------------------------- /Tests/CheckConstraintTests.cs: -------------------------------------------------------------------------------- 1 | using SqlSrcGen.Generator; 2 | 3 | namespace Tests; 4 | 5 | public class CheckConstraintTests 6 | { 7 | [TestCase("Age>=18")] 8 | [TestCase("Age>=18 AND City='Sandnes'")] 9 | [TestCase("(Age>=18, City='Sandnes')")] 10 | public void ProcessSqlSchema_CheckConstraint_CreatesTableInfo(string expr) 11 | { 12 | // arrange 13 | var generator = new SqlGenerator(); 14 | var databaseInfo = new DatabaseInfo(); 15 | 16 | // act 17 | generator.ProcessSqlSchema($"CREATE TABLE contact (name Text CHECK ({expr}));", databaseInfo); 18 | 19 | // assert 20 | Assert.That(databaseInfo.Tables[0].SqlName, Is.EqualTo("contact")); 21 | Assert.That(databaseInfo.Tables[0].CSharpName, Is.EqualTo("Contact")); 22 | var columns = databaseInfo.Tables[0].Columns.ToArray(); 23 | Assert.That(columns[0].SqlName, Is.EqualTo("name")); 24 | Assert.That(columns[0].CSharpName, Is.EqualTo("Name")); 25 | Assert.That(columns[0].SqlType, Is.EqualTo("Text")); 26 | Assert.That(columns[0].CSharpType, Is.EqualTo("string?")); 27 | Assert.That(columns[0].TypeAffinity, Is.EqualTo(TypeAffinity.TEXT)); 28 | } 29 | } -------------------------------------------------------------------------------- /Tests/CollateConstraintTests.cs: -------------------------------------------------------------------------------- 1 | using Moq; 2 | using SqlSrcGen; 3 | using SqlSrcGen.Generator; 4 | 5 | namespace Tests; 6 | 7 | public class CollateConstraintTests 8 | { 9 | [Test] 10 | public void ProcessSqlSchema_CollateConstraint_CreatesTableInfo() 11 | { 12 | // arrange 13 | var generator = new SqlGenerator(); 14 | var databaseInfo = new DatabaseInfo(); 15 | 16 | // act 17 | generator.ProcessSqlSchema($"CREATE TABLE contact (name Text COLLATE NOCASE);", databaseInfo); 18 | 19 | // assert 20 | Assert.That(databaseInfo.Tables[0].SqlName, Is.EqualTo("contact")); 21 | Assert.That(databaseInfo.Tables[0].CSharpName, Is.EqualTo("Contact")); 22 | var columns = databaseInfo.Tables[0].Columns.ToArray(); 23 | Assert.That(columns[0].SqlName, Is.EqualTo("name")); 24 | Assert.That(columns[0].CSharpName, Is.EqualTo("Name")); 25 | Assert.That(columns[0].SqlType, Is.EqualTo("Text")); 26 | Assert.That(columns[0].CSharpType, Is.EqualTo("string?")); 27 | Assert.That(columns[0].TypeAffinity, Is.EqualTo(TypeAffinity.TEXT)); 28 | } 29 | 30 | [Test] 31 | public void ProcessSqlSchema_CustomCollateName_GeneratesWarning() 32 | { 33 | // arrange 34 | var generator = new SqlGenerator(); 35 | var databaseInfo = new DatabaseInfo(); 36 | var diagnosticsReporterMock = new Mock(); 37 | 38 | ErrorCode capturedErrorCode = ErrorCode.None; 39 | Token? capturedToken = null; 40 | string capturedMessage = ""; 41 | 42 | diagnosticsReporterMock 43 | .Setup(reporter => reporter.Warning(It.IsAny(), It.IsAny(), It.IsAny())) 44 | .Callback((errorCode, message, token) => 45 | { 46 | capturedErrorCode = errorCode; 47 | capturedToken = token; 48 | capturedMessage = message; 49 | }); 50 | generator.DiagnosticsReporter = diagnosticsReporterMock.Object; 51 | 52 | // act 53 | generator.ProcessSqlSchema($"CREATE TABLE contact (name Text COLLATE custom);", databaseInfo); 54 | 55 | // assert 56 | Assert.That(capturedToken, Is.Not.Null); 57 | Assert.That(capturedToken!.Line, Is.EqualTo(0)); 58 | Assert.That(capturedToken!.CharacterInLine, Is.EqualTo(40)); 59 | Assert.That(capturedMessage, Is.EqualTo("Collation types other than nocase, binary and rtrim require custom collation creation")); 60 | Assert.That(capturedErrorCode, Is.EqualTo(ErrorCode.SSG0002)); 61 | } 62 | 63 | [Test] 64 | public void ProcessSqlSchema_CollateOnNonTextColumn_GeneratesWarning() 65 | { 66 | // arrange 67 | var generator = new SqlGenerator(); 68 | var databaseInfo = new DatabaseInfo(); 69 | var diagnosticsReporterMock = new Mock(); 70 | 71 | ErrorCode capturedErrorCode = ErrorCode.None; 72 | Token? capturedToken = null; 73 | string capturedMessage = ""; 74 | 75 | diagnosticsReporterMock 76 | .Setup(reporter => reporter.Warning(It.IsAny(), It.IsAny(), It.IsAny())) 77 | .Callback((errorCode, message, token) => 78 | { 79 | capturedErrorCode = errorCode; 80 | capturedToken = token; 81 | capturedMessage = message; 82 | }); 83 | generator.DiagnosticsReporter = diagnosticsReporterMock.Object; 84 | 85 | // act 86 | generator.ProcessSqlSchema($"CREATE TABLE contact (name INTEGER COLLATE custom);", databaseInfo); 87 | 88 | // assert 89 | Assert.That(capturedToken, Is.Not.Null); 90 | Assert.That(capturedToken!.Line, Is.EqualTo(0)); 91 | Assert.That(capturedToken!.CharacterInLine, Is.EqualTo(35)); 92 | Assert.That(capturedMessage, Is.EqualTo("Collation only affects Text columns")); 93 | Assert.That(capturedErrorCode, Is.EqualTo(ErrorCode.SSG0003)); 94 | } 95 | } -------------------------------------------------------------------------------- /Tests/DefaultConstraintTests.cs: -------------------------------------------------------------------------------- 1 | using Moq; 2 | using SqlSrcGen.Generator; 3 | 4 | namespace Tests; 5 | 6 | public class DefaultConstraintTests 7 | { 8 | [TestCase("default 'mydefault'")] 9 | [TestCase("default'mydefault'")] 10 | [TestCase("default null")] 11 | [TestCase("default NULL")] 12 | [TestCase("default FALSE")] 13 | [TestCase("default CURRENT_TIME")] 14 | [TestCase("default CURRENT_DATE")] 15 | [TestCase("default CURRENT_TIMESTAMP")] 16 | [TestCase("default 123.123e+12")] 17 | [TestCase("default +123.123e+12")] 18 | [TestCase("default -123.123e+12")] 19 | [TestCase("default x'A1B2C3'")] 20 | [TestCase("default (1+2)")] 21 | public void ProcessSqlSchema_DefaultConstraint_CreatesTableInfo(string constraint) 22 | { 23 | // arrange 24 | var generator = new SqlGenerator(); 25 | var databaseInfo = new DatabaseInfo(); 26 | 27 | // act 28 | generator.ProcessSqlSchema($"CREATE TABLE contact (name Text {constraint});", databaseInfo); 29 | 30 | // assert 31 | Assert.That(databaseInfo.Tables[0].SqlName, Is.EqualTo("contact")); 32 | Assert.That(databaseInfo.Tables[0].CSharpName, Is.EqualTo("Contact")); 33 | var columns = databaseInfo.Tables[0].Columns.ToArray(); 34 | Assert.That(columns[0].SqlName, Is.EqualTo("name")); 35 | Assert.That(columns[0].CSharpName, Is.EqualTo("Name")); 36 | Assert.That(columns[0].SqlType, Is.EqualTo("Text")); 37 | Assert.That(columns[0].CSharpType, Is.EqualTo("string?")); 38 | Assert.That(columns[0].TypeAffinity, Is.EqualTo(TypeAffinity.TEXT)); 39 | } 40 | } -------------------------------------------------------------------------------- /Tests/GeneratedConstraintTests.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | using Moq; 3 | using SqlSrcGen.Generator; 4 | 5 | namespace Tests; 6 | 7 | public class GeneratedConstraintTests 8 | { 9 | [TestCase("generated always as (\"name\")")] 10 | [TestCase("generated always as (\"name\") stored")] 11 | [TestCase("generated always as (\"name\") virtual")] 12 | [TestCase("as (\"name\")")] 13 | [TestCase("as (\"name\") stored")] 14 | [TestCase("as (\"name\") virtual")] 15 | public void ProcessSqlSchema_GeneratedConstraint_CreatesTableInfo(string extraClauses) 16 | { 17 | // arrange 18 | var generator = new SqlGenerator(); 19 | var databaseInfo = new DatabaseInfo(); 20 | 21 | // act 22 | generator.ProcessSqlSchema($"CREATE TABLE child (name Text {extraClauses});", databaseInfo); 23 | 24 | // assert 25 | var columns = databaseInfo.Tables[0].Columns.ToArray(); 26 | Assert.That(columns[0].SqlName, Is.EqualTo("name")); 27 | Assert.That(columns[0].CSharpName, Is.EqualTo("Name")); 28 | Assert.That(columns[0].SqlType, Is.EqualTo("Text")); 29 | Assert.That(columns[0].CSharpType, Is.EqualTo("string?")); 30 | Assert.That(columns[0].TypeAffinity, Is.EqualTo(TypeAffinity.TEXT)); 31 | } 32 | 33 | [Test] 34 | public void ProcessSqlSchema_ReferencesConstraintMatchClause_CreatesWarning() 35 | { 36 | // arrange 37 | var generator = new SqlGenerator(); 38 | var databaseInfo = new DatabaseInfo(); 39 | var diagnosticsReporterMock = new Mock(); 40 | 41 | ErrorCode capturedErrorCode = ErrorCode.None; 42 | Token? capturedToken = null; 43 | string capturedMessage = ""; 44 | 45 | diagnosticsReporterMock 46 | .Setup(reporter => reporter.Warning(It.IsAny(), It.IsAny(), It.IsAny())) 47 | .Callback((errorCode, message, token) => 48 | { 49 | capturedErrorCode = errorCode; 50 | capturedToken = token; 51 | capturedMessage = message; 52 | }); 53 | generator.DiagnosticsReporter = diagnosticsReporterMock.Object; 54 | 55 | // act 56 | var schemaBuilder = new StringBuilder(); 57 | schemaBuilder.AppendLine("CREATE TABLE parent (name Text PRIMARY KEY);"); 58 | schemaBuilder.AppendLine($"CREATE TABLE child (name Text REFERENCES parent(name) match simple);"); 59 | generator.ProcessSqlSchema(schemaBuilder.ToString(), databaseInfo); 60 | 61 | // assert 62 | Assert.That(capturedToken, Is.Not.Null); 63 | Assert.That(capturedToken!.Line, Is.EqualTo(1)); 64 | Assert.That(capturedToken!.CharacterInLine, Is.EqualTo(54)); 65 | Assert.That(capturedMessage, Is.EqualTo("SQLite parses MATCH clauses, but does not enforce them. All foreign key constraints in SQLite are handled as if MATCH SIMPLE were specified.")); 66 | Assert.That(capturedErrorCode, Is.EqualTo(ErrorCode.SSG0004)); 67 | } 68 | 69 | [TestCase("DEFERRABLE")] 70 | [TestCase("DEFERRABLE INITIALLY DEFERRED")] 71 | [TestCase("DEFERRABLE INITIALLY IMMEDIATE")] 72 | [TestCase("not DEFERRABLE")] 73 | [TestCase("NOT DEFERRABLE INITIALLY DEFERRED")] 74 | [TestCase("not DEFERRABLE INITIALLY IMMEDIATE")] 75 | public void ProcessSqlSchema_ReferencesConstraintDeferrable_CreatesTableInfo(string extraClauses) 76 | { 77 | // arrange 78 | var generator = new SqlGenerator(); 79 | var databaseInfo = new DatabaseInfo(); 80 | 81 | var schemaBuilder = new StringBuilder(); 82 | schemaBuilder.AppendLine("CREATE TABLE parent (name Text PRIMARY KEY);"); 83 | schemaBuilder.AppendLine($"CREATE TABLE child (name Text REFERENCES parent(name) {extraClauses});"); 84 | 85 | // act 86 | generator.ProcessSqlSchema(schemaBuilder.ToString(), databaseInfo); 87 | 88 | // assert 89 | Assert.That(databaseInfo.Tables[1].SqlName, Is.EqualTo("child")); 90 | Assert.That(databaseInfo.Tables[1].CSharpName, Is.EqualTo("Child")); 91 | var columns = databaseInfo.Tables[0].Columns.ToArray(); 92 | Assert.That(columns[0].SqlName, Is.EqualTo("name")); 93 | Assert.That(columns[0].CSharpName, Is.EqualTo("Name")); 94 | Assert.That(columns[0].SqlType, Is.EqualTo("Text")); 95 | Assert.That(columns[0].CSharpType, Is.EqualTo("string?")); 96 | Assert.That(columns[0].TypeAffinity, Is.EqualTo(TypeAffinity.TEXT)); 97 | } 98 | } -------------------------------------------------------------------------------- /Tests/NamedConstraintTests.cs: -------------------------------------------------------------------------------- 1 | using SqlSrcGen.Generator; 2 | 3 | namespace Tests; 4 | 5 | public class NamedConstraintTests 6 | { 7 | [Test] 8 | public void ProcessSqlSchema_NamedConstraint_CreatesTableInfo() 9 | { 10 | // arrange 11 | var generator = new SqlGenerator(); 12 | var databaseInfo = new DatabaseInfo(); 13 | 14 | // act 15 | generator.ProcessSqlSchema($"CREATE TABLE contact (name Text CONSTRAINT myconstraint PRIMARY KEY);", databaseInfo); 16 | 17 | // assert 18 | Assert.That(databaseInfo.Tables[0].SqlName, Is.EqualTo("contact")); 19 | Assert.That(databaseInfo.Tables[0].CSharpName, Is.EqualTo("Contact")); 20 | var columns = databaseInfo.Tables[0].Columns.ToArray(); 21 | Assert.That(columns[0].SqlName, Is.EqualTo("name")); 22 | Assert.That(columns[0].CSharpName, Is.EqualTo("Name")); 23 | Assert.That(columns[0].SqlType, Is.EqualTo("Text")); 24 | Assert.That(columns[0].CSharpType, Is.EqualTo("string?")); 25 | Assert.That(columns[0].TypeAffinity, Is.EqualTo(TypeAffinity.TEXT)); 26 | } 27 | } -------------------------------------------------------------------------------- /Tests/NotNullConstraintTests.cs: -------------------------------------------------------------------------------- 1 | using Moq; 2 | using SqlSrcGen.Generator; 3 | 4 | namespace Tests; 5 | 6 | public class NotNullConstraintTests 7 | { 8 | [Test] 9 | public void ProcessSqlSchema_TextColumnNotNull_CreatesTableInfo() 10 | { 11 | // arrange 12 | var generator = new SqlGenerator(); 13 | var databaseInfo = new DatabaseInfo(); 14 | 15 | // act 16 | generator.ProcessSqlSchema("CREATE TABLE contact (name Text not null);", databaseInfo); 17 | 18 | // assert 19 | Assert.That(databaseInfo.Tables[0].SqlName, Is.EqualTo("contact")); 20 | Assert.That(databaseInfo.Tables[0].CSharpName, Is.EqualTo("Contact")); 21 | var columns = databaseInfo.Tables[0].Columns.ToArray(); 22 | Assert.That(columns[0].SqlName, Is.EqualTo("name")); 23 | Assert.That(columns[0].CSharpName, Is.EqualTo("Name")); 24 | Assert.That(columns[0].SqlType, Is.EqualTo("Text")); 25 | Assert.That(columns[0].CSharpType, Is.EqualTo("string")); 26 | Assert.That(columns[0].TypeAffinity, Is.EqualTo(TypeAffinity.TEXT)); 27 | Assert.That(columns[0].NotNull, Is.True); 28 | } 29 | 30 | 31 | [Test] 32 | public void ProcessSqlSchema_IntegerColumnNotNull_CreatesTableInfo() 33 | { 34 | // arrange 35 | var generator = new SqlGenerator(); 36 | var databaseInfo = new DatabaseInfo(); 37 | 38 | // act 39 | generator.ProcessSqlSchema("CREATE TABLE contact (age Integer NOT NULL);", databaseInfo); 40 | 41 | // assert 42 | Assert.That(databaseInfo.Tables[0].SqlName, Is.EqualTo("contact")); 43 | Assert.That(databaseInfo.Tables[0].CSharpName, Is.EqualTo("Contact")); 44 | var columns = databaseInfo.Tables[0].Columns.ToArray(); 45 | Assert.That(columns[0].SqlName, Is.EqualTo("age")); 46 | Assert.That(columns[0].CSharpName, Is.EqualTo("Age")); 47 | Assert.That(columns[0].SqlType, Is.EqualTo("Integer")); 48 | Assert.That(columns[0].CSharpType, Is.EqualTo("long")); 49 | Assert.That(columns[0].TypeAffinity, Is.EqualTo(TypeAffinity.INTEGER)); 50 | } 51 | 52 | [Test] 53 | public void ProcessSqlSchema_RealColumnNotNull_CreatesTableInfo() 54 | { 55 | // arrange 56 | var generator = new SqlGenerator(); 57 | var databaseInfo = new DatabaseInfo(); 58 | 59 | // act 60 | generator.ProcessSqlSchema("CREATE TABLE contact (height Real Not Null);", databaseInfo); 61 | 62 | // assert 63 | Assert.That(databaseInfo.Tables[0].SqlName, Is.EqualTo("contact")); 64 | Assert.That(databaseInfo.Tables[0].CSharpName, Is.EqualTo("Contact")); 65 | var columns = databaseInfo.Tables[0].Columns.ToArray(); 66 | Assert.That(columns[0].SqlName, Is.EqualTo("height")); 67 | Assert.That(columns[0].CSharpName, Is.EqualTo("Height")); 68 | Assert.That(columns[0].SqlType, Is.EqualTo("Real")); 69 | Assert.That(columns[0].CSharpType, Is.EqualTo("double")); 70 | Assert.That(columns[0].TypeAffinity, Is.EqualTo(TypeAffinity.REAL)); 71 | } 72 | 73 | [Test] 74 | public void ProcessSqlSchema_BlobColumnNotNull_CreatesTableInfo() 75 | { 76 | // arrange 77 | var generator = new SqlGenerator(); 78 | var databaseInfo = new DatabaseInfo(); 79 | 80 | // act 81 | generator.ProcessSqlSchema("CREATE TABLE contact (key Blob not null);", databaseInfo); 82 | 83 | // assert 84 | Assert.That(databaseInfo.Tables[0].SqlName, Is.EqualTo("contact")); 85 | Assert.That(databaseInfo.Tables[0].CSharpName, Is.EqualTo("Contact")); 86 | var columns = databaseInfo.Tables[0].Columns.ToArray(); 87 | Assert.That(columns[0].SqlName, Is.EqualTo("key")); 88 | Assert.That(columns[0].CSharpName, Is.EqualTo("Key")); 89 | Assert.That(columns[0].SqlType, Is.EqualTo("Blob")); 90 | Assert.That(columns[0].CSharpType, Is.EqualTo("byte[]")); 91 | Assert.That(columns[0].TypeAffinity, Is.EqualTo(TypeAffinity.BLOB)); 92 | } 93 | 94 | [Test] 95 | public void ProcessSqlSchema_NumericColumnNotNull_CreatesTableInfo() 96 | { 97 | // arrange 98 | var generator = new SqlGenerator(); 99 | var databaseInfo = new DatabaseInfo(); 100 | 101 | // act 102 | generator.ProcessSqlSchema("CREATE TABLE contact (distance Numeric NOT NULL);", databaseInfo); 103 | 104 | // assert 105 | Assert.That(databaseInfo.Tables[0].SqlName, Is.EqualTo("contact")); 106 | Assert.That(databaseInfo.Tables[0].CSharpName, Is.EqualTo("Contact")); 107 | var columns = databaseInfo.Tables[0].Columns.ToArray(); 108 | Assert.That(columns[0].SqlName, Is.EqualTo("distance")); 109 | Assert.That(columns[0].CSharpName, Is.EqualTo("Distance")); 110 | Assert.That(columns[0].SqlType, Is.EqualTo("Numeric")); 111 | Assert.That(columns[0].CSharpType, Is.EqualTo("Numeric")); 112 | Assert.That(columns[0].TypeAffinity, Is.EqualTo(TypeAffinity.NUMERIC)); 113 | } 114 | 115 | [Test] 116 | public void ProcessSqlSchema_InvalidConstraintNot_ThrowsInvalidSqlException() 117 | { 118 | // arrange 119 | var generator = new SqlGenerator(); 120 | var databaseInfo = new DatabaseInfo(); 121 | 122 | try 123 | { 124 | // act 125 | generator.ProcessSqlSchema("CREATE TABLE contact (distance Numeric NOT);", databaseInfo); 126 | 127 | // assert 128 | Assert.Fail("InvalidSqlException didn't occur"); 129 | } 130 | catch (InvalidSqlException exception) 131 | { 132 | Assert.That(exception.Message, Is.EqualTo("Invalid column constraint, did you mean 'not null'?")); 133 | Assert.That(exception.Token!.Line, Is.EqualTo(0)); 134 | Assert.That(exception.Token.CharacterInLine, Is.EqualTo(39)); 135 | } 136 | } 137 | 138 | [Test] 139 | public void ProcessSqlSchema_NotNulWithOnConflict_Parsed() 140 | { 141 | // arrange 142 | var generator = new SqlGenerator(); 143 | var databaseInfo = new DatabaseInfo(); 144 | 145 | // act 146 | generator.ProcessSqlSchema("CREATE TABLE contact (distance Numeric NOT NULL ON CONFLICT ROLLBACK);", databaseInfo); 147 | 148 | // assert 149 | Assert.That(databaseInfo.Tables[0].Columns.First().NotNull, Is.True); 150 | } 151 | 152 | } -------------------------------------------------------------------------------- /Tests/NunitAttribures.cs: -------------------------------------------------------------------------------- 1 | [assembly: FixtureLifeCycle(LifeCycle.InstancePerTestCase)] -------------------------------------------------------------------------------- /Tests/ReadBlobLiteralTests.cs: -------------------------------------------------------------------------------- 1 | using SqlSrcGen.Generator; 2 | 3 | namespace Tests; 4 | 5 | public class ReadBlobLiteralTests 6 | { 7 | [TestCase("x'1234'")] 8 | [TestCase("X'1234'")] 9 | [TestCase("X'ABCDEF1234567890'")] 10 | public void ReadBlobLiteral_Valid_ParsedCorrectly(string literal) 11 | { 12 | // arrange 13 | var generator = new Tokenizer(); 14 | int position = 0; 15 | int lineIndex = 1; 16 | int characterInLineIndex = 0; 17 | 18 | // act 19 | var after = generator.ReadBlobLiteral(literal + " ", out Token token, ref position, ref lineIndex, ref characterInLineIndex); 20 | 21 | // assert 22 | Assert.That(after.ToString(), Is.EqualTo(" ")); 23 | Assert.That(token.Value, Is.EqualTo(literal)); 24 | Assert.That(token.Line, Is.EqualTo(1)); 25 | Assert.That(token.CharacterInLine, Is.EqualTo(0)); 26 | Assert.That(token.Position, Is.EqualTo(0)); 27 | Assert.That(token.TokenType, Is.EqualTo(TokenType.BlobLiteral)); 28 | 29 | Assert.That(position, Is.EqualTo(literal.Length)); 30 | Assert.That(lineIndex, Is.EqualTo(1)); 31 | Assert.That(characterInLineIndex, Is.EqualTo(literal.Length)); 32 | } 33 | 34 | [TestCase("", "Ran out of text parsing blob literal", 0)] 35 | [TestCase("'", "Blob literal must start with a 'x' or 'X'", 0)] 36 | [TestCase("x1234", "Second character in a blob literal must be a single quote", 1)] 37 | [TestCase("x'AB3'", "Blob literals must have an even number of hex digits", 5)] 38 | [TestCase("x'G'", "Invalid charactor in blob literal", 2)] 39 | [TestCase("x'ABCD", "Invalid charactor in blob literal", 6)] 40 | public void ReadBlobLiteral_Invalid_InvalidSqlException(string literal, string exceptionMessage, int charactorInLine) 41 | { 42 | // arrange 43 | var generator = new Tokenizer(); 44 | int position = 0; 45 | int lineIndex = 1; 46 | int characterInLineIndex = 0; 47 | 48 | // act 49 | try 50 | { 51 | var after = generator.ReadBlobLiteral(literal + " ", out Token token, ref position, ref lineIndex, ref characterInLineIndex); 52 | Assert.Fail("InvalidSqlException didn't occur"); 53 | } 54 | catch (InvalidSqlException exception) 55 | { 56 | Assert.That(exception.Message, Is.EqualTo(exceptionMessage)); 57 | Assert.That(exception.Token!.CharacterInLine, Is.EqualTo(charactorInLine)); 58 | Assert.That(exception.Token.Position, Is.EqualTo(charactorInLine)); 59 | } 60 | } 61 | } -------------------------------------------------------------------------------- /Tests/ReadNumericLiteralTests.cs: -------------------------------------------------------------------------------- 1 | using SqlSrcGen.Generator; 2 | 3 | namespace Tests; 4 | 5 | public class ReadNumericLiteralTests 6 | { 7 | [TestCase("1234")] 8 | [TestCase("1234.6543")] 9 | [TestCase("1234.")] 10 | [TestCase(".1234")] 11 | [TestCase("1234e12")] 12 | [TestCase("1234e+12")] 13 | [TestCase("1234e-12")] 14 | [TestCase("0xABCEDF0123456789")] 15 | public void ParseNumericLiteral_Valid_ParsedCorrectly(string literal) 16 | { 17 | // arrange 18 | var generator = new Tokenizer(); 19 | int position = 0; 20 | int lineIndex = 1; 21 | int characterInLineIndex = 0; 22 | 23 | // act 24 | var after = generator.ParseNumericLiteral(literal + " ", out Token token, ref position, ref lineIndex, ref characterInLineIndex); 25 | 26 | // assert 27 | Assert.That(after.ToString(), Is.EqualTo(" ")); 28 | Assert.That(token.Value, Is.EqualTo(literal)); 29 | Assert.That(token.Line, Is.EqualTo(1)); 30 | Assert.That(token.CharacterInLine, Is.EqualTo(0)); 31 | Assert.That(token.Position, Is.EqualTo(0)); 32 | Assert.That(token.TokenType, Is.EqualTo(TokenType.NumericLiteral)); 33 | 34 | Assert.That(position, Is.EqualTo(literal.Length)); 35 | Assert.That(lineIndex, Is.EqualTo(1)); 36 | Assert.That(characterInLineIndex, Is.EqualTo(literal.Length)); 37 | } 38 | } -------------------------------------------------------------------------------- /Tests/ReferencesConstraintTests.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | using Moq; 3 | using SqlSrcGen.Generator; 4 | 5 | namespace Tests; 6 | 7 | public class ReferencesConstraintTests 8 | { 9 | [Test] 10 | public void ProcessSqlSchema_ReferencesConstraintTableOnly_CreatesTableInfo() 11 | { 12 | // arrange 13 | var generator = new SqlGenerator(); 14 | var databaseInfo = new DatabaseInfo(); 15 | 16 | var schemaBuilder = new StringBuilder(); 17 | schemaBuilder.AppendLine("CREATE TABLE parent (name Text PRIMARY KEY);"); 18 | schemaBuilder.AppendLine("CREATE TABLE child (name Text REFERENCES parent);"); 19 | 20 | // act 21 | generator.ProcessSqlSchema(schemaBuilder.ToString(), databaseInfo); 22 | 23 | // assert 24 | Assert.That(databaseInfo.Tables[1].SqlName, Is.EqualTo("child")); 25 | Assert.That(databaseInfo.Tables[1].CSharpName, Is.EqualTo("Child")); 26 | var columns = databaseInfo.Tables[0].Columns.ToArray(); 27 | Assert.That(columns[0].SqlName, Is.EqualTo("name")); 28 | Assert.That(columns[0].CSharpName, Is.EqualTo("Name")); 29 | Assert.That(columns[0].SqlType, Is.EqualTo("Text")); 30 | Assert.That(columns[0].CSharpType, Is.EqualTo("string?")); 31 | Assert.That(columns[0].TypeAffinity, Is.EqualTo(TypeAffinity.TEXT)); 32 | } 33 | 34 | [Test] 35 | public void ProcessSqlSchema_ReferencesConstraintTableDoesntExist_InvalidSqlException() 36 | { 37 | // arrange 38 | var generator = new SqlGenerator(); 39 | var databaseInfo = new DatabaseInfo(); 40 | 41 | var schemaBuilder = new StringBuilder(); 42 | schemaBuilder.AppendLine("CREATE TABLE parent1 (name Text PRIMARY KEY);"); 43 | schemaBuilder.AppendLine("CREATE TABLE child (name Text REFERENCES parent);"); 44 | 45 | // act 46 | try 47 | { 48 | generator.ProcessSqlSchema(schemaBuilder.ToString(), databaseInfo); 49 | Assert.Fail("InvalidSqlException didn't occur"); 50 | } 51 | catch (InvalidSqlException exception) 52 | { 53 | // assert 54 | Assert.That(exception.Message, Is.EqualTo("referenced table parent does not exist")); 55 | Assert.That(exception.Token!.Line, Is.EqualTo(1)); 56 | Assert.That(exception.Token.CharacterInLine, Is.EqualTo(41)); 57 | Assert.That(exception.Token.Position, Is.EqualTo(87)); 58 | } 59 | } 60 | 61 | [Test] 62 | public void ProcessSqlSchema_ReferencesConstraintColumnDefined_CreatesTableInfo() 63 | { 64 | // arrange 65 | var generator = new SqlGenerator(); 66 | var databaseInfo = new DatabaseInfo(); 67 | 68 | var schemaBuilder = new StringBuilder(); 69 | schemaBuilder.AppendLine("CREATE TABLE parent (name Text PRIMARY KEY);"); 70 | schemaBuilder.AppendLine("CREATE TABLE child (name Text REFERENCES parent(name));"); 71 | 72 | // act 73 | generator.ProcessSqlSchema(schemaBuilder.ToString(), databaseInfo); 74 | 75 | // assert 76 | Assert.That(databaseInfo.Tables[1].SqlName, Is.EqualTo("child")); 77 | Assert.That(databaseInfo.Tables[1].CSharpName, Is.EqualTo("Child")); 78 | var columns = databaseInfo.Tables[0].Columns.ToArray(); 79 | Assert.That(columns[0].SqlName, Is.EqualTo("name")); 80 | Assert.That(columns[0].CSharpName, Is.EqualTo("Name")); 81 | Assert.That(columns[0].SqlType, Is.EqualTo("Text")); 82 | Assert.That(columns[0].CSharpType, Is.EqualTo("string?")); 83 | Assert.That(columns[0].TypeAffinity, Is.EqualTo(TypeAffinity.TEXT)); 84 | } 85 | 86 | [TestCase("on delete set null")] 87 | [TestCase("on delete set default")] 88 | [TestCase("on delete cascade")] 89 | [TestCase("on delete restrict")] 90 | [TestCase("on delete no action")] 91 | [TestCase("on update set null")] 92 | [TestCase("on update set default")] 93 | [TestCase("on update cascade")] 94 | [TestCase("on update restrict")] 95 | [TestCase("on update no action")] 96 | [TestCase("on update no action on delete set null")] 97 | public void ProcessSqlSchema_ReferencesConstraintExtraClauses_CreatesTableInfo(string extraClauses) 98 | { 99 | // arrange 100 | var generator = new SqlGenerator(); 101 | var databaseInfo = new DatabaseInfo(); 102 | 103 | var schemaBuilder = new StringBuilder(); 104 | schemaBuilder.AppendLine("CREATE TABLE parent (name Text PRIMARY KEY);"); 105 | schemaBuilder.AppendLine($"CREATE TABLE child (name Text REFERENCES parent(name) {extraClauses});"); 106 | 107 | // act 108 | generator.ProcessSqlSchema(schemaBuilder.ToString(), databaseInfo); 109 | 110 | // assert 111 | Assert.That(databaseInfo.Tables[1].SqlName, Is.EqualTo("child")); 112 | Assert.That(databaseInfo.Tables[1].CSharpName, Is.EqualTo("Child")); 113 | var columns = databaseInfo.Tables[0].Columns.ToArray(); 114 | Assert.That(columns[0].SqlName, Is.EqualTo("name")); 115 | Assert.That(columns[0].CSharpName, Is.EqualTo("Name")); 116 | Assert.That(columns[0].SqlType, Is.EqualTo("Text")); 117 | Assert.That(columns[0].CSharpType, Is.EqualTo("string?")); 118 | Assert.That(columns[0].TypeAffinity, Is.EqualTo(TypeAffinity.TEXT)); 119 | } 120 | 121 | [Test] 122 | public void ProcessSqlSchema_ReferencesConstraintMatchClause_CreatesWarning() 123 | { 124 | // arrange 125 | var generator = new SqlGenerator(); 126 | var databaseInfo = new DatabaseInfo(); 127 | var diagnosticsReporterMock = new Mock(); 128 | 129 | ErrorCode capturedErrorCode = ErrorCode.None; 130 | Token? capturedToken = null; 131 | string capturedMessage = ""; 132 | 133 | diagnosticsReporterMock 134 | .Setup(reporter => reporter.Warning(It.IsAny(), It.IsAny(), It.IsAny())) 135 | .Callback((errorCode, message, token) => 136 | { 137 | capturedErrorCode = errorCode; 138 | capturedToken = token; 139 | capturedMessage = message; 140 | }); 141 | generator.DiagnosticsReporter = diagnosticsReporterMock.Object; 142 | 143 | // act 144 | var schemaBuilder = new StringBuilder(); 145 | schemaBuilder.AppendLine("CREATE TABLE parent (name Text PRIMARY KEY);"); 146 | schemaBuilder.AppendLine($"CREATE TABLE child (name Text REFERENCES parent(name) match simple);"); 147 | generator.ProcessSqlSchema(schemaBuilder.ToString(), databaseInfo); 148 | 149 | // assert 150 | Assert.That(capturedToken, Is.Not.Null); 151 | Assert.That(capturedToken!.Line, Is.EqualTo(1)); 152 | Assert.That(capturedToken!.CharacterInLine, Is.EqualTo(54)); 153 | Assert.That(capturedMessage, Is.EqualTo("SQLite parses MATCH clauses, but does not enforce them. All foreign key constraints in SQLite are handled as if MATCH SIMPLE were specified.")); 154 | Assert.That(capturedErrorCode, Is.EqualTo(ErrorCode.SSG0004)); 155 | } 156 | 157 | [TestCase("DEFERRABLE")] 158 | [TestCase("DEFERRABLE INITIALLY DEFERRED")] 159 | [TestCase("DEFERRABLE INITIALLY IMMEDIATE")] 160 | [TestCase("not DEFERRABLE")] 161 | [TestCase("NOT DEFERRABLE INITIALLY DEFERRED")] 162 | [TestCase("not DEFERRABLE INITIALLY IMMEDIATE")] 163 | public void ProcessSqlSchema_ReferencesConstraintDeferrable_CreatesTableInfo(string extraClauses) 164 | { 165 | // arrange 166 | var generator = new SqlGenerator(); 167 | var databaseInfo = new DatabaseInfo(); 168 | 169 | var schemaBuilder = new StringBuilder(); 170 | schemaBuilder.AppendLine("CREATE TABLE parent (name Text PRIMARY KEY);"); 171 | schemaBuilder.AppendLine($"CREATE TABLE child (name Text REFERENCES parent(name) {extraClauses});"); 172 | 173 | // act 174 | generator.ProcessSqlSchema(schemaBuilder.ToString(), databaseInfo); 175 | 176 | // assert 177 | Assert.That(databaseInfo.Tables[1].SqlName, Is.EqualTo("child")); 178 | Assert.That(databaseInfo.Tables[1].CSharpName, Is.EqualTo("Child")); 179 | var columns = databaseInfo.Tables[0].Columns.ToArray(); 180 | Assert.That(columns[0].SqlName, Is.EqualTo("name")); 181 | Assert.That(columns[0].CSharpName, Is.EqualTo("Name")); 182 | Assert.That(columns[0].SqlType, Is.EqualTo("Text")); 183 | Assert.That(columns[0].CSharpType, Is.EqualTo("string?")); 184 | Assert.That(columns[0].TypeAffinity, Is.EqualTo(TypeAffinity.TEXT)); 185 | } 186 | } -------------------------------------------------------------------------------- /Tests/SelectParserTests/StarColumnsTests.cs: -------------------------------------------------------------------------------- 1 | using SqlSrcGen.Generator; 2 | 3 | namespace Tests.SelectParserTests; 4 | 5 | public class StarColumnsTests 6 | { 7 | readonly DatabaseInfo _databaseInfo; 8 | readonly SelectParser _selectParser; 9 | 10 | public StarColumnsTests() 11 | { 12 | _databaseInfo = new DatabaseInfo(); 13 | var expressionParser = new ExpressionParser( 14 | _databaseInfo, 15 | new LiteralValueParser(), 16 | new TypeNameParser(), 17 | new CollationParser()); 18 | _selectParser = new SelectParser(_databaseInfo, expressionParser); 19 | } 20 | 21 | [Test] 22 | public void Select_StarColumnsOneTable_CorrectColumns() 23 | { 24 | // arrange 25 | var table = new Table(); 26 | table.SqlName = "contact"; 27 | table.AddColumn(new Column() { SqlName = "name", CSharpName = "Name" }); 28 | table.AddColumn(new Column() { SqlName = "email", CSharpName = "Email" }); 29 | _databaseInfo.Tables.Add(table); 30 | 31 | var tokenizer = new Tokenizer(); 32 | var tokens = tokenizer.Tokenize("SELECT * FROM contact").ToArray().AsSpan(); 33 | int index = 0; 34 | var queryInfo = new QueryInfo(); 35 | 36 | // act 37 | _selectParser.Parse(ref index, tokens, queryInfo); 38 | queryInfo.Process(); 39 | 40 | // assert 41 | Assert.That(queryInfo.Columns[0].SqlName, Is.EqualTo("name")); 42 | Assert.That(queryInfo.Columns[0].CSharpName, Is.EqualTo("Name")); 43 | Assert.That(queryInfo.Columns[1].SqlName, Is.EqualTo("email")); 44 | Assert.That(queryInfo.Columns[1].CSharpName, Is.EqualTo("Email")); 45 | } 46 | 47 | [Test] 48 | public void Select_StarColumnsTwoTables_HasAllColumnsFromBothTables() 49 | { 50 | // arrange 51 | var contactsTable = new Table() 52 | { 53 | SqlName = "contact" 54 | }; 55 | contactsTable.AddColumn(new Column() { SqlName = "name", CSharpName = "Name" }); 56 | contactsTable.AddColumn(new Column() { SqlName = "email", CSharpName = "Email" }); 57 | _databaseInfo.Tables.Add(contactsTable); 58 | 59 | var jobTable = new Table() 60 | { 61 | SqlName = "job" 62 | }; 63 | jobTable.AddColumn(new Column() { SqlName = "title", CSharpName = "Title" }); 64 | jobTable.AddColumn(new Column() { SqlName = "salary", CSharpName = "Salary" }); 65 | _databaseInfo.Tables.Add(jobTable); 66 | 67 | var tokenizer = new Tokenizer(); 68 | var tokens = tokenizer.Tokenize("SELECT * FROM contact, job").ToArray().AsSpan(); 69 | int index = 0; 70 | var queryInfo = new QueryInfo(); 71 | 72 | // act 73 | _selectParser.Parse(ref index, tokens, queryInfo); 74 | queryInfo.Process(); 75 | 76 | // assert 77 | Assert.That(queryInfo.Columns[0].SqlName, Is.EqualTo("name")); 78 | Assert.That(queryInfo.Columns[0].CSharpName, Is.EqualTo("Name")); 79 | Assert.That(queryInfo.Columns[1].SqlName, Is.EqualTo("email")); 80 | Assert.That(queryInfo.Columns[1].CSharpName, Is.EqualTo("Email")); 81 | Assert.That(queryInfo.Columns[2].SqlName, Is.EqualTo("title")); 82 | Assert.That(queryInfo.Columns[2].CSharpName, Is.EqualTo("Title")); 83 | Assert.That(queryInfo.Columns[3].SqlName, Is.EqualTo("salary")); 84 | Assert.That(queryInfo.Columns[3].CSharpName, Is.EqualTo("Salary")); 85 | } 86 | 87 | [Test] 88 | public void Select_StarColumnsTwoDulicateColumName_UsesTableNameToDifferentiate() 89 | { 90 | // arrange 91 | var contactsTable = new Table() 92 | { 93 | SqlName = "contact", 94 | CSharpName = "Contact" 95 | }; 96 | contactsTable.AddColumn(new Column() { SqlName = "name", CSharpName = "Name" }); 97 | contactsTable.AddColumn(new Column() { SqlName = "email", CSharpName = "Email" }); 98 | _databaseInfo.Tables.Add(contactsTable); 99 | 100 | var jobTable = new Table() 101 | { 102 | SqlName = "job", 103 | CSharpName = "Job" 104 | }; 105 | jobTable.AddColumn(new Column() { SqlName = "name", CSharpName = "Name" }); 106 | jobTable.AddColumn(new Column() { SqlName = "title", CSharpName = "Title" }); 107 | jobTable.AddColumn(new Column() { SqlName = "salary", CSharpName = "Salary" }); 108 | _databaseInfo.Tables.Add(jobTable); 109 | 110 | var tokenizer = new Tokenizer(); 111 | var tokens = tokenizer.Tokenize("SELECT * FROM contact, job").ToArray().AsSpan(); 112 | int index = 0; 113 | var queryInfo = new QueryInfo(); 114 | 115 | // act 116 | _selectParser.Parse(ref index, tokens, queryInfo); 117 | queryInfo.Process(); 118 | 119 | // assert 120 | Assert.That(queryInfo.Columns[0].SqlName, Is.EqualTo("name")); 121 | Assert.That(queryInfo.Columns[0].CSharpName, Is.EqualTo("ContactName")); 122 | Assert.That(queryInfo.Columns[1].SqlName, Is.EqualTo("email")); 123 | Assert.That(queryInfo.Columns[1].CSharpName, Is.EqualTo("Email")); 124 | Assert.That(queryInfo.Columns[2].SqlName, Is.EqualTo("name")); 125 | Assert.That(queryInfo.Columns[2].CSharpName, Is.EqualTo("JobName")); 126 | Assert.That(queryInfo.Columns[3].SqlName, Is.EqualTo("title")); 127 | Assert.That(queryInfo.Columns[3].CSharpName, Is.EqualTo("Title")); 128 | Assert.That(queryInfo.Columns[4].SqlName, Is.EqualTo("salary")); 129 | Assert.That(queryInfo.Columns[4].CSharpName, Is.EqualTo("Salary")); 130 | } 131 | 132 | [Test] 133 | public void Select_StarColumnsThreeDulicateColumName_UsesTableNameToDifferentiate() 134 | { 135 | // arrange 136 | var contactsTable = new Table() 137 | { 138 | SqlName = "contact", 139 | CSharpName = "Contact" 140 | }; 141 | contactsTable.AddColumn(new Column() { SqlName = "name", CSharpName = "Name" }); 142 | _databaseInfo.Tables.Add(contactsTable); 143 | 144 | var jobTable = new Table() 145 | { 146 | SqlName = "job", 147 | CSharpName = "Job" 148 | }; 149 | jobTable.AddColumn(new Column() { SqlName = "name", CSharpName = "Name" }); 150 | _databaseInfo.Tables.Add(jobTable); 151 | 152 | var locationTable = new Table() 153 | { 154 | SqlName = "location", 155 | CSharpName = "Location" 156 | }; 157 | locationTable.AddColumn(new Column() { SqlName = "name", CSharpName = "Name" }); 158 | _databaseInfo.Tables.Add(locationTable); 159 | 160 | var tokenizer = new Tokenizer(); 161 | var tokens = tokenizer.Tokenize("SELECT * FROM contact, job, location").ToArray().AsSpan(); 162 | int index = 0; 163 | var queryInfo = new QueryInfo(); 164 | 165 | // act 166 | _selectParser.Parse(ref index, tokens, queryInfo); 167 | queryInfo.Process(); 168 | 169 | // assert 170 | Assert.That(queryInfo.Columns[0].SqlName, Is.EqualTo("name")); 171 | Assert.That(queryInfo.Columns[0].CSharpName, Is.EqualTo("ContactName")); 172 | Assert.That(queryInfo.Columns[1].SqlName, Is.EqualTo("name")); 173 | Assert.That(queryInfo.Columns[1].CSharpName, Is.EqualTo("JobName")); 174 | Assert.That(queryInfo.Columns[2].SqlName, Is.EqualTo("name")); 175 | Assert.That(queryInfo.Columns[2].CSharpName, Is.EqualTo("LocationName")); 176 | } 177 | 178 | [Test] 179 | public void Select_ConflictingColumnsAfterAddingTablename_AddsNumber() 180 | { 181 | // arrange 182 | var contactsTable = new Table() 183 | { 184 | SqlName = "contact", 185 | CSharpName = "Contact" 186 | }; 187 | contactsTable.AddColumn(new Column() { SqlName = "name", CSharpName = "Name" }); 188 | contactsTable.AddColumn(new Column() { SqlName = "contactName", CSharpName = "ContactName" }); 189 | _databaseInfo.Tables.Add(contactsTable); 190 | 191 | var jobTable = new Table() 192 | { 193 | SqlName = "job", 194 | CSharpName = "Job" 195 | }; 196 | jobTable.AddColumn(new Column() { SqlName = "name", CSharpName = "Name" }); 197 | _databaseInfo.Tables.Add(jobTable); 198 | 199 | var tokenizer = new Tokenizer(); 200 | var tokens = tokenizer.Tokenize("SELECT * FROM contact, job").ToArray().AsSpan(); 201 | int index = 0; 202 | var queryInfo = new QueryInfo(); 203 | 204 | // act 205 | _selectParser.Parse(ref index, tokens, queryInfo); 206 | queryInfo.Process(); 207 | 208 | // assert 209 | Assert.That(queryInfo.Columns[0].CSharpName, Is.EqualTo("ContactName1")); 210 | Assert.That(queryInfo.Columns[1].CSharpName, Is.EqualTo("ContactName")); 211 | Assert.That(queryInfo.Columns[2].CSharpName, Is.EqualTo("JobName")); 212 | } 213 | 214 | [Test] 215 | public void Select_NamedColumnsOneTable_CorrectColumns() 216 | { 217 | // arrange 218 | var table = new Table(); 219 | table.SqlName = "contact"; 220 | table.AddColumn(new Column() { SqlName = "name", CSharpName = "Name" }); 221 | table.AddColumn(new Column() { SqlName = "email", CSharpName = "Email" }); 222 | table.AddColumn(new Column() { SqlName = "age", CSharpName = "Age" }); 223 | _databaseInfo.Tables.Add(table); 224 | 225 | var tokenizer = new Tokenizer(); 226 | var tokens = tokenizer.Tokenize("SELECT name, age FROM contact").ToArray().AsSpan(); 227 | int index = 0; 228 | var queryInfo = new QueryInfo(); 229 | 230 | // act 231 | _selectParser.Parse(ref index, tokens, queryInfo); 232 | queryInfo.Process(); 233 | 234 | // assert 235 | Assert.That(queryInfo.Columns[0].SqlName, Is.EqualTo("name")); 236 | Assert.That(queryInfo.Columns[0].CSharpName, Is.EqualTo("Name")); 237 | Assert.That(queryInfo.Columns[1].SqlName, Is.EqualTo("age")); 238 | Assert.That(queryInfo.Columns[1].CSharpName, Is.EqualTo("Age")); 239 | } 240 | 241 | [Test] 242 | public void Select_AmbiguousColumn_InvalidSqlException() 243 | { 244 | // arrange 245 | var contactTable = new Table { SqlName = "contact" }; 246 | contactTable.AddColumn(new Column() { SqlName = "name", CSharpName = "Name" }); 247 | contactTable.AddColumn(new Column() { SqlName = "email", CSharpName = "Email" }); 248 | contactTable.AddColumn(new Column() { SqlName = "age", CSharpName = "Age" }); 249 | _databaseInfo.Tables.Add(contactTable); 250 | 251 | var jobTable = new Table { SqlName = "job" }; 252 | jobTable.AddColumn(new Column() { SqlName = "name", CSharpName = "Name" }); 253 | jobTable.AddColumn(new Column() { SqlName = "salary", CSharpName = "Salary" }); 254 | _databaseInfo.Tables.Add(jobTable); 255 | 256 | var tokenizer = new Tokenizer(); 257 | var tokens = tokenizer.Tokenize("SELECT name FROM contact, job").ToArray().AsSpan(); 258 | int index = 0; 259 | var queryInfo = new QueryInfo(); 260 | 261 | // act 262 | try 263 | { 264 | _selectParser.Parse(ref index, tokens, queryInfo); 265 | queryInfo.Process(); 266 | 267 | // assert 268 | Assert.Fail("InvalidSqlException didn't occur"); 269 | } 270 | catch (InvalidSqlException exception) 271 | { 272 | Assert.That(exception.Message, Is.EqualTo("Ambiguous column name")); 273 | Assert.That(exception.Token!.Position, Is.EqualTo(7)); 274 | } 275 | } 276 | 277 | [Test] 278 | public void Select_FullyQualifiedColumns_CorrectColumns() 279 | { 280 | // arrange 281 | var contactTable = new Table { SqlName = "contact", CSharpName = "Contact" }; 282 | contactTable.AddColumn(new Column() { SqlName = "name", CSharpName = "Name" }); 283 | contactTable.AddColumn(new Column() { SqlName = "email", CSharpName = "Email" }); 284 | contactTable.AddColumn(new Column() { SqlName = "age", CSharpName = "Age" }); 285 | _databaseInfo.Tables.Add(contactTable); 286 | 287 | var jobTable = new Table { SqlName = "job", CSharpName = "Job" }; 288 | jobTable.AddColumn(new Column() { SqlName = "name", CSharpName = "Name" }); 289 | jobTable.AddColumn(new Column() { SqlName = "salary", CSharpName = "Salary" }); 290 | _databaseInfo.Tables.Add(jobTable); 291 | 292 | var tokenizer = new Tokenizer(); 293 | var tokens = tokenizer.Tokenize("SELECT contact.name, job.name, salary FROM contact, job").ToArray().AsSpan(); 294 | int index = 0; 295 | var queryInfo = new QueryInfo(); 296 | 297 | // act 298 | _selectParser.Parse(ref index, tokens, queryInfo); 299 | queryInfo.Process(); 300 | 301 | // assert 302 | Assert.That(queryInfo.Columns[0].SqlName, Is.EqualTo("name")); 303 | Assert.That(queryInfo.Columns[0].CSharpName, Is.EqualTo("ContactName")); 304 | Assert.That(queryInfo.Columns[1].SqlName, Is.EqualTo("name")); 305 | Assert.That(queryInfo.Columns[1].CSharpName, Is.EqualTo("JobName")); 306 | Assert.That(queryInfo.Columns[2].SqlName, Is.EqualTo("salary")); 307 | Assert.That(queryInfo.Columns[2].CSharpName, Is.EqualTo("Salary")); 308 | } 309 | 310 | [Test] 311 | public void Select_QualifiedAliasedColumn_CorrectColumns() 312 | { 313 | // arrange 314 | var contactTable = new Table { SqlName = "contact", CSharpName = "Contact" }; 315 | contactTable.AddColumn(new Column() { SqlName = "name", CSharpName = "Name" }); 316 | contactTable.AddColumn(new Column() { SqlName = "email", CSharpName = "Email" }); 317 | contactTable.AddColumn(new Column() { SqlName = "age", CSharpName = "Age" }); 318 | _databaseInfo.Tables.Add(contactTable); 319 | 320 | var jobTable = new Table { SqlName = "job", CSharpName = "Job" }; 321 | jobTable.AddColumn(new Column() { SqlName = "name", CSharpName = "Name" }); 322 | jobTable.AddColumn(new Column() { SqlName = "salary", CSharpName = "Salary" }); 323 | _databaseInfo.Tables.Add(jobTable); 324 | 325 | var tokenizer = new Tokenizer(); 326 | var tokens = tokenizer.Tokenize("SELECT contact.name as name1, job.name, salary FROM contact, job").ToArray().AsSpan(); 327 | int index = 0; 328 | var queryInfo = new QueryInfo(); 329 | 330 | // act 331 | _selectParser.Parse(ref index, tokens, queryInfo); 332 | queryInfo.Process(); 333 | 334 | // assert 335 | Assert.That(queryInfo.Columns[0].SqlName, Is.EqualTo("name1")); 336 | Assert.That(queryInfo.Columns[0].CSharpName, Is.EqualTo("Name1")); 337 | Assert.That(queryInfo.Columns[1].SqlName, Is.EqualTo("name")); 338 | Assert.That(queryInfo.Columns[1].CSharpName, Is.EqualTo("Name")); 339 | Assert.That(queryInfo.Columns[2].SqlName, Is.EqualTo("salary")); 340 | Assert.That(queryInfo.Columns[2].CSharpName, Is.EqualTo("Salary")); 341 | } 342 | 343 | [Test] 344 | public void Select_UnqualifiedAliasedColumn_CorrectColumns() 345 | { 346 | // arrange 347 | var contactTable = new Table { SqlName = "contact", CSharpName = "Contact" }; 348 | contactTable.AddColumn(new Column() { SqlName = "name", CSharpName = "Name" }); 349 | contactTable.AddColumn(new Column() { SqlName = "email", CSharpName = "Email" }); 350 | contactTable.AddColumn(new Column() { SqlName = "age", CSharpName = "Age" }); 351 | _databaseInfo.Tables.Add(contactTable); 352 | 353 | var jobTable = new Table { SqlName = "job", CSharpName = "Job" }; 354 | jobTable.AddColumn(new Column() { SqlName = "name", CSharpName = "Name" }); 355 | jobTable.AddColumn(new Column() { SqlName = "salary", CSharpName = "Salary" }); 356 | _databaseInfo.Tables.Add(jobTable); 357 | 358 | var tokenizer = new Tokenizer(); 359 | var tokens = tokenizer.Tokenize("SELECT age as oldness, job.name, salary FROM contact, job").ToArray().AsSpan(); 360 | int index = 0; 361 | var queryInfo = new QueryInfo(); 362 | 363 | // act 364 | _selectParser.Parse(ref index, tokens, queryInfo); 365 | queryInfo.Process(); 366 | 367 | // assert 368 | Assert.That(queryInfo.Columns[0].SqlName, Is.EqualTo("oldness")); 369 | Assert.That(queryInfo.Columns[0].CSharpName, Is.EqualTo("Oldness")); 370 | Assert.That(queryInfo.Columns[1].SqlName, Is.EqualTo("name")); 371 | Assert.That(queryInfo.Columns[1].CSharpName, Is.EqualTo("Name")); 372 | Assert.That(queryInfo.Columns[2].SqlName, Is.EqualTo("salary")); 373 | Assert.That(queryInfo.Columns[2].CSharpName, Is.EqualTo("Salary")); 374 | } 375 | 376 | [Test] 377 | public void Select_QualidifedStart_CorrectColumns() 378 | { 379 | // arrange 380 | var contactTable = new Table { SqlName = "contact", CSharpName = "Contact" }; 381 | contactTable.AddColumn(new Column() { SqlName = "name", CSharpName = "Name" }); 382 | contactTable.AddColumn(new Column() { SqlName = "email", CSharpName = "Email" }); 383 | contactTable.AddColumn(new Column() { SqlName = "age", CSharpName = "Age" }); 384 | _databaseInfo.Tables.Add(contactTable); 385 | 386 | var jobTable = new Table { SqlName = "job", CSharpName = "Job" }; 387 | jobTable.AddColumn(new Column() { SqlName = "name", CSharpName = "Name" }); 388 | jobTable.AddColumn(new Column() { SqlName = "salary", CSharpName = "Salary" }); 389 | _databaseInfo.Tables.Add(jobTable); 390 | 391 | var tokenizer = new Tokenizer(); 392 | var tokens = tokenizer.Tokenize("SELECT job.* FROM contact, job").ToArray().AsSpan(); 393 | int index = 0; 394 | var queryInfo = new QueryInfo(); 395 | 396 | // act 397 | _selectParser.Parse(ref index, tokens, queryInfo); 398 | queryInfo.Process(); 399 | 400 | // assert 401 | Assert.That(queryInfo.Columns[0].SqlName, Is.EqualTo("name")); 402 | Assert.That(queryInfo.Columns[0].CSharpName, Is.EqualTo("Name")); 403 | Assert.That(queryInfo.Columns[1].SqlName, Is.EqualTo("salary")); 404 | Assert.That(queryInfo.Columns[1].CSharpName, Is.EqualTo("Salary")); 405 | } 406 | } -------------------------------------------------------------------------------- /Tests/SquareBacketNameTests.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | using SqlSrcGen.Generator; 3 | 4 | namespace Tests; 5 | 6 | public class SquareBacketNameTests 7 | { 8 | [TestCase("[table]", "Table")] 9 | [TestCase("[two part]", "TwoPart")] 10 | [TestCase("[two part]", "TwoPart")] 11 | public void ProcessSqlSchema_SquareBracketInTableName_CreatesTableInfo(string tableName, string cSharpName) 12 | { 13 | // arrange 14 | var generator = new SqlGenerator(); 15 | var databaseInfo = new DatabaseInfo(); 16 | 17 | // act 18 | generator.ProcessSqlSchema($"CREATE TABLE {tableName} (name Text);", databaseInfo); 19 | 20 | // assert 21 | Assert.That(databaseInfo.Tables[0].SqlName, Is.EqualTo(tableName)); 22 | Assert.That(databaseInfo.Tables[0].CSharpName, Is.EqualTo(cSharpName)); 23 | var columns = databaseInfo.Tables[0].Columns.ToArray(); 24 | Assert.That(columns[0].SqlName, Is.EqualTo("name")); 25 | Assert.That(columns[0].CSharpName, Is.EqualTo("Name")); 26 | Assert.That(columns[0].SqlType, Is.EqualTo("Text")); 27 | Assert.That(columns[0].CSharpType, Is.EqualTo("string?")); 28 | Assert.That(columns[0].TypeAffinity, Is.EqualTo(TypeAffinity.TEXT)); 29 | } 30 | 31 | [Test] 32 | public void ProcessSqlSchema_TwoTablesMapToSameCSharpName_InvalidSqlException() 33 | { 34 | // arrange 35 | var generator = new SqlGenerator(); 36 | var databaseInfo = new DatabaseInfo(); 37 | var builder = new StringBuilder(); 38 | builder.Append($"CREATE TABLE [one two] (name Text);"); 39 | builder.Append("\n"); 40 | builder.Append($"CREATE TABLE [one two] (name Text);"); 41 | 42 | // act 43 | try 44 | { 45 | generator.ProcessSqlSchema(builder.ToString(), databaseInfo); 46 | Assert.Fail("InvalidSqlException didn't occur"); 47 | } 48 | catch (InvalidSqlException exception) 49 | { 50 | Assert.That(exception.Message, Is.EqualTo("Table maps to same csharp class name as an existing table")); 51 | Assert.That(exception.Token!.Line, Is.EqualTo(1)); 52 | Assert.That(exception.Token.CharacterInLine, Is.EqualTo(13)); 53 | Assert.That(exception.Token.Position, Is.EqualTo(49)); 54 | } 55 | } 56 | 57 | [TestCase("[NUMBER]", "Number")] 58 | [TestCase("[two part]", "TwoPart")] 59 | [TestCase("[two part]", "TwoPart")] 60 | public void ProcessSqlSchema_SquareBracketInColumnName_CreatesTableInfo(string columnName, string cSharpName) 61 | { 62 | // arrange 63 | var generator = new SqlGenerator(); 64 | var databaseInfo = new DatabaseInfo(); 65 | 66 | // act 67 | generator.ProcessSqlSchema($"CREATE TABLE test ({columnName} Text);", databaseInfo); 68 | 69 | // assert 70 | Assert.That(databaseInfo.Tables[0].SqlName, Is.EqualTo("test")); 71 | Assert.That(databaseInfo.Tables[0].CSharpName, Is.EqualTo("Test")); 72 | var columns = databaseInfo.Tables[0].Columns.ToArray(); 73 | Assert.That(columns[0].SqlName, Is.EqualTo(columnName)); 74 | Assert.That(columns[0].CSharpName, Is.EqualTo(cSharpName)); 75 | Assert.That(columns[0].SqlType, Is.EqualTo("Text")); 76 | Assert.That(columns[0].CSharpType, Is.EqualTo("string?")); 77 | Assert.That(columns[0].TypeAffinity, Is.EqualTo(TypeAffinity.TEXT)); 78 | } 79 | 80 | [Test] 81 | public void ProcessSqlSchema_SquareBracketInColumnName_CreatesTableInfo() 82 | { 83 | // arrange 84 | var generator = new SqlGenerator(); 85 | var databaseInfo = new DatabaseInfo(); 86 | 87 | try 88 | { 89 | // act 90 | generator.ProcessSqlSchema($"CREATE TABLE test ([one two] Text, [one two] Text);", databaseInfo); 91 | Assert.Fail("InvalidSqlException didn't occur"); 92 | } 93 | catch (InvalidSqlException exception) 94 | { 95 | Assert.That(exception.Message, Is.EqualTo("Column maps to same csharp name as an existing column")); 96 | Assert.That(exception.Token!.Line, Is.EqualTo(0)); 97 | Assert.That(exception.Token.CharacterInLine, Is.EqualTo(35)); 98 | Assert.That(exception.Token.Position, Is.EqualTo(35)); 99 | } 100 | } 101 | } -------------------------------------------------------------------------------- /Tests/TableConstraintTests/TableCheckConstraintTests.cs: -------------------------------------------------------------------------------- 1 | using Moq; 2 | using SqlSrcGen.Generator; 3 | 4 | namespace Tests; 5 | 6 | public class TableCheckConstraintTests 7 | { 8 | [TestCase("Age>=18")] 9 | [TestCase("Age>=18 AND City='Sandnes'")] 10 | [TestCase("(Age>=18, City='Sandnes')")] 11 | public void CheckConstraint_ParsedWithoutError_CreatesTableInfo(string expr) 12 | { 13 | // arrange 14 | var generator = new SqlGenerator(); 15 | var databaseInfo = new DatabaseInfo(); 16 | 17 | // act 18 | generator.ProcessSqlSchema($"CREATE TABLE contact (name Text, CHECK ({expr}));", databaseInfo); 19 | 20 | // assert 21 | Assert.That(databaseInfo.Tables[0].SqlName, Is.EqualTo("contact")); 22 | Assert.That(databaseInfo.Tables[0].CSharpName, Is.EqualTo("Contact")); 23 | var columns = databaseInfo.Tables[0].Columns.ToArray(); 24 | Assert.That(columns[0].SqlName, Is.EqualTo("name")); 25 | Assert.That(columns[0].CSharpName, Is.EqualTo("Name")); 26 | Assert.That(columns[0].SqlType, Is.EqualTo("Text")); 27 | Assert.That(columns[0].CSharpType, Is.EqualTo("string?")); 28 | Assert.That(columns[0].TypeAffinity, Is.EqualTo(TypeAffinity.TEXT)); 29 | } 30 | } -------------------------------------------------------------------------------- /Tests/TableConstraintTests/TableForeignKeyConstraintTests.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | using SqlSrcGen.Generator; 3 | 4 | namespace Tests; 5 | 6 | public class TableForeignKeyConstraintTests 7 | { 8 | [Test] 9 | public void ForeignKeyConstraint_ReferencesTwoColumnsUnique_CreatesTableInfo() 10 | { 11 | // arrange 12 | var generator = new SqlGenerator(); 13 | var databaseInfo = new DatabaseInfo(); 14 | var queryBuilder = new StringBuilder(); 15 | queryBuilder.Append("CREATE TABLE addresses (street_name Text, street_number Integer, UNIQUE (street_name, street_number));\n"); 16 | queryBuilder.Append("CREATE TABLE contact (name Text, street Text, number Integer, FOREIGN KEY (street, number) references addresses (street_name, street_number));"); 17 | 18 | // act 19 | generator.ProcessSqlSchema(queryBuilder.ToString(), databaseInfo); 20 | 21 | // assert 22 | Assert.That(databaseInfo.Tables[1].SqlName, Is.EqualTo("contact")); 23 | Assert.That(databaseInfo.Tables[1].CSharpName, Is.EqualTo("Contact")); 24 | var columns = databaseInfo.Tables[1].Columns.ToArray(); 25 | Assert.That(columns[0].SqlName, Is.EqualTo("name")); 26 | Assert.That(columns[1].SqlName, Is.EqualTo("street")); 27 | Assert.That(columns[2].SqlName, Is.EqualTo("number")); 28 | } 29 | 30 | [Test] 31 | public void ForeignKeyConstraint_ReferencesTwoColumnsPrimaryKey_CreatesTableInfo() 32 | { 33 | // arrange 34 | var generator = new SqlGenerator(); 35 | var databaseInfo = new DatabaseInfo(); 36 | var queryBuilder = new StringBuilder(); 37 | queryBuilder.Append("CREATE TABLE addresses (street_name Text, street_number Integer, PRIMARY KEY (street_name, street_number));\n"); 38 | queryBuilder.Append("CREATE TABLE contact (name Text, street Text, number Integer, FOREIGN KEY (street, number) references addresses (street_name, street_number));"); 39 | 40 | // act 41 | generator.ProcessSqlSchema(queryBuilder.ToString(), databaseInfo); 42 | 43 | // assert 44 | Assert.That(databaseInfo.Tables[1].SqlName, Is.EqualTo("contact")); 45 | Assert.That(databaseInfo.Tables[1].CSharpName, Is.EqualTo("Contact")); 46 | var columns = databaseInfo.Tables[1].Columns.ToArray(); 47 | Assert.That(columns[0].SqlName, Is.EqualTo("name")); 48 | Assert.That(columns[1].SqlName, Is.EqualTo("street")); 49 | Assert.That(columns[2].SqlName, Is.EqualTo("number")); 50 | } 51 | 52 | [Test] 53 | public void ForeignKeyConstraint_TableOnlyReferenceHasPrimaryKey_CreatesTableInfo() 54 | { 55 | // arrange 56 | var generator = new SqlGenerator(); 57 | var databaseInfo = new DatabaseInfo(); 58 | var queryBuilder = new StringBuilder(); 59 | queryBuilder.Append("CREATE TABLE addresses (street_name Text, street_number Integer, PRIMARY KEY (street_name, street_number));\n"); 60 | queryBuilder.Append("CREATE TABLE contact (name Text, street Text, number Integer, FOREIGN KEY (street, number) references addresses);"); 61 | 62 | // act 63 | generator.ProcessSqlSchema(queryBuilder.ToString(), databaseInfo); 64 | 65 | // assert 66 | Assert.That(databaseInfo.Tables[1].SqlName, Is.EqualTo("contact")); 67 | Assert.That(databaseInfo.Tables[1].CSharpName, Is.EqualTo("Contact")); 68 | var columns = databaseInfo.Tables[1].Columns.ToArray(); 69 | Assert.That(columns[0].SqlName, Is.EqualTo("name")); 70 | Assert.That(columns[1].SqlName, Is.EqualTo("street")); 71 | Assert.That(columns[2].SqlName, Is.EqualTo("number")); 72 | } 73 | 74 | [Test] 75 | public void ForeignKeyConstraint_TableOnlyReferenceColumnsUnique_InvalidSqlException() 76 | { 77 | // arrange 78 | var generator = new SqlGenerator(); 79 | var databaseInfo = new DatabaseInfo(); 80 | var queryBuilder = new StringBuilder(); 81 | queryBuilder.Append("CREATE TABLE addresses (street_name Text, street_number Integer, UNIQUE (street_name, street_number));\n"); 82 | queryBuilder.Append("CREATE TABLE contact (name Text, street Text, number Integer, FOREIGN KEY (street, number) references addresses);"); 83 | 84 | try 85 | { 86 | // act 87 | generator.ProcessSqlSchema(queryBuilder.ToString(), databaseInfo); 88 | Assert.Fail("InvalidSqlException didn't occur"); 89 | } 90 | catch (InvalidSqlException exception) 91 | { 92 | Assert.That(exception.Message, Is.EqualTo("Referenced table doesn't have a matching foreign key")); 93 | Assert.That(exception.Token!.CharacterInLine, Is.EqualTo(91)); 94 | Assert.That(exception.Token.Position, Is.EqualTo(194)); 95 | Assert.That(exception.Token.Line, Is.EqualTo(1)); 96 | } 97 | } 98 | 99 | [Test] 100 | public void ForeignKeyConstraint_MismatchedColumnCounts_InvalidSqlException() 101 | { 102 | // arrange 103 | var generator = new SqlGenerator(); 104 | var databaseInfo = new DatabaseInfo(); 105 | var queryBuilder = new StringBuilder(); 106 | queryBuilder.Append("CREATE TABLE addresses (street_name Text, street_number Integer, PRIMARY KEY (street_name, street_number));\n"); 107 | queryBuilder.Append("CREATE TABLE contact (name Text, street Text, number Integer, FOREIGN KEY (street) references addresses (street_name, street_number));"); 108 | 109 | try 110 | { 111 | // act 112 | generator.ProcessSqlSchema(queryBuilder.ToString(), databaseInfo); 113 | Assert.Fail("InvalidSqlException didn't occur"); 114 | } 115 | catch (InvalidSqlException exception) 116 | { 117 | Assert.That(exception.Message, Is.EqualTo("Local and foreign column counts must match")); 118 | Assert.That(exception.Token!.CharacterInLine, Is.EqualTo(83)); 119 | Assert.That(exception.Token.Position, Is.EqualTo(191)); 120 | Assert.That(exception.Token.Line, Is.EqualTo(1)); 121 | } 122 | } 123 | 124 | [Test] 125 | public void ForeignKeyConstraint_ReferencedColumnDoesntExist_InvalidSqlException() 126 | { 127 | // arrange 128 | var generator = new SqlGenerator(); 129 | var databaseInfo = new DatabaseInfo(); 130 | var queryBuilder = new StringBuilder(); 131 | queryBuilder.Append("CREATE TABLE addresses (street_name Text, street_number Integer, PRIMARY KEY (street_name, street_number));\n"); 132 | queryBuilder.Append("CREATE TABLE contact (name Text, street Text, number Integer, FOREIGN KEY (street, number) references addresses (street_name, street_number1));"); 133 | 134 | try 135 | { 136 | // act 137 | generator.ProcessSqlSchema(queryBuilder.ToString(), databaseInfo); 138 | Assert.Fail("InvalidSqlException didn't occur"); 139 | } 140 | catch (InvalidSqlException exception) 141 | { 142 | Assert.That(exception.Message, Is.EqualTo("Referenced column street_number1 does not exist")); 143 | Assert.That(exception.Token!.CharacterInLine, Is.EqualTo(126)); 144 | Assert.That(exception.Token.Position, Is.EqualTo(234)); 145 | Assert.That(exception.Token.Line, Is.EqualTo(1)); 146 | } 147 | } 148 | 149 | [Test] 150 | public void ForeignKeyConstraint_ForeignTableNotUniqueByColumns_InvalidSqlException() 151 | { 152 | // arrange 153 | var generator = new SqlGenerator(); 154 | var databaseInfo = new DatabaseInfo(); 155 | var queryBuilder = new StringBuilder(); 156 | queryBuilder.Append("CREATE TABLE addresses (street_name Text, street_number Integer, PRIMARY KEY (street_name, street_number));\n"); 157 | queryBuilder.Append("CREATE TABLE contact (name Text, street Text, number Integer, FOREIGN KEY (street) references addresses (street_name));"); 158 | 159 | try 160 | { 161 | // act 162 | generator.ProcessSqlSchema(queryBuilder.ToString(), databaseInfo); 163 | Assert.Fail("InvalidSqlException didn't occur"); 164 | } 165 | catch (InvalidSqlException exception) 166 | { 167 | Assert.That(exception.Message, Is.EqualTo("Foreign table is not unique by these columns")); 168 | Assert.That(exception.Token!.CharacterInLine, Is.EqualTo(83)); 169 | Assert.That(exception.Token.Position, Is.EqualTo(191)); 170 | Assert.That(exception.Token.Line, Is.EqualTo(1)); 171 | } 172 | } 173 | } -------------------------------------------------------------------------------- /Tests/TableConstraintTests/TableNamedConstraintTests.cs: -------------------------------------------------------------------------------- 1 | using Moq; 2 | using SqlSrcGen.Generator; 3 | 4 | namespace Tests.TableConstraintTests; 5 | 6 | public class TableNamedConstraintTests 7 | { 8 | [Test] 9 | public void TableNamedConstraint_Valid_CreatesTableInfo() 10 | { 11 | // arrange 12 | var generator = new SqlGenerator(); 13 | var databaseInfo = new DatabaseInfo(); 14 | 15 | // act 16 | generator.ProcessSqlSchema($"CREATE TABLE contact (name Text, constraint ConsraintName unique (name));", databaseInfo); 17 | 18 | // assert 19 | Assert.That(databaseInfo.Tables[0].SqlName, Is.EqualTo("contact")); 20 | Assert.That(databaseInfo.Tables[0].CSharpName, Is.EqualTo("Contact")); 21 | var columns = databaseInfo.Tables[0].Columns.ToArray(); 22 | Assert.That(columns[0].SqlName, Is.EqualTo("name")); 23 | Assert.That(columns[0].CSharpName, Is.EqualTo("Name")); 24 | Assert.That(columns[0].SqlType, Is.EqualTo("Text")); 25 | Assert.That(columns[0].CSharpType, Is.EqualTo("string?")); 26 | Assert.That(columns[0].TypeAffinity, Is.EqualTo(TypeAffinity.TEXT)); 27 | Assert.That(columns[0].Unique, Is.True); 28 | } 29 | } -------------------------------------------------------------------------------- /Tests/TableConstraintTests/TablePrimaryKeyConstraintTests.cs: -------------------------------------------------------------------------------- 1 | using Moq; 2 | using SqlSrcGen.Generator; 3 | 4 | namespace Tests.TableConstraintTests; 5 | 6 | public class TablePrimaryKeyConstraintTests 7 | { 8 | [Test] 9 | public void PrimaryKeyTableConstraint_Valid_CreatesTableInfo() 10 | { 11 | // arrange 12 | var generator = new SqlGenerator(); 13 | var databaseInfo = new DatabaseInfo(); 14 | 15 | // act 16 | generator.ProcessSqlSchema($"CREATE TABLE contact (name Text, PRIMARY KEY (name));", databaseInfo); 17 | 18 | // assert 19 | Assert.That(databaseInfo.Tables[0].SqlName, Is.EqualTo("contact")); 20 | Assert.That(databaseInfo.Tables[0].CSharpName, Is.EqualTo("Contact")); 21 | var columns = databaseInfo.Tables[0].Columns.ToArray(); 22 | Assert.That(columns[0].SqlName, Is.EqualTo("name")); 23 | Assert.That(columns[0].CSharpName, Is.EqualTo("Name")); 24 | Assert.That(columns[0].SqlType, Is.EqualTo("Text")); 25 | Assert.That(columns[0].CSharpType, Is.EqualTo("string?")); 26 | Assert.That(columns[0].TypeAffinity, Is.EqualTo(TypeAffinity.TEXT)); 27 | Assert.That(columns[0].PrimaryKey, Is.True); 28 | } 29 | 30 | [Test] 31 | public void PrimaryKeyTableConstraint_TwoColumns_CreatesTableInfo() 32 | { 33 | // arrange 34 | var generator = new SqlGenerator(); 35 | var databaseInfo = new DatabaseInfo(); 36 | 37 | // act 38 | generator.ProcessSqlSchema($"CREATE TABLE contact (name Text, id integer, address Text, PRIMARY KEY (name, id));", databaseInfo); 39 | 40 | // assert 41 | Assert.That(databaseInfo.Tables[0].PrimaryKey.Any(column => column.SqlName == "name"), Is.True); 42 | Assert.That(databaseInfo.Tables[0].PrimaryKey.Any(column => column.SqlName == "id"), Is.True); 43 | } 44 | 45 | [Test] 46 | public void TablePrimaryKeyConstraint_TableAlreadyHasPrimaryKey_ThrowsInvalidJsonException() 47 | { 48 | // arrange 49 | var generator = new SqlGenerator(); 50 | var databaseInfo = new DatabaseInfo(); 51 | 52 | // act 53 | try 54 | { 55 | generator.ProcessSqlSchema($"CREATE TABLE contact (name Text Primary Key, id integer, address Text, PRIMARY KEY (id));", databaseInfo); 56 | Assert.Fail("InvalidSqlException didn't occur"); 57 | } 58 | catch (InvalidSqlException exception) 59 | { 60 | Assert.That(exception.Message, Is.EqualTo("Table already has a primary key")); 61 | Assert.That(exception.Token!.CharacterInLine, Is.EqualTo(71)); 62 | Assert.That(exception.Token.Position, Is.EqualTo(71)); 63 | Assert.That(exception.Token.Line, Is.EqualTo(0)); 64 | } 65 | } 66 | 67 | [Test] 68 | public void TablePrimaryKeyConstraint_ColumnAlreadyHasPrimaryKey_ThrowsInvalidJsonException() 69 | { 70 | // arrange 71 | var generator = new SqlGenerator(); 72 | var databaseInfo = new DatabaseInfo(); 73 | 74 | // act 75 | try 76 | { 77 | generator.ProcessSqlSchema($"CREATE TABLE contact (name Text, id integer Primary Key, address Text, PRIMARY KEY (id));", databaseInfo); 78 | Assert.Fail("InvalidSqlException didn't occur"); 79 | } 80 | catch (InvalidSqlException exception) 81 | { 82 | Assert.That(exception.Message, Is.EqualTo("Table already has a primary key")); 83 | Assert.That(exception.Token!.CharacterInLine, Is.EqualTo(71)); 84 | Assert.That(exception.Token.Position, Is.EqualTo(71)); 85 | Assert.That(exception.Token.Line, Is.EqualTo(0)); 86 | } 87 | } 88 | 89 | [Test] 90 | public void TablePrimaryKeyConstraint_ColumnAlreadyUnique_ThrowsInvalidJsonException() 91 | { 92 | // arrange 93 | var generator = new SqlGenerator(); 94 | var databaseInfo = new DatabaseInfo(); 95 | 96 | // act 97 | try 98 | { 99 | generator.ProcessSqlSchema($"CREATE TABLE contact (name Text, id integer UNIQUE, address Text, PRIMARY KEY (id));", databaseInfo); 100 | Assert.Fail("InvalidSqlException didn't occur"); 101 | } 102 | catch (InvalidSqlException exception) 103 | { 104 | Assert.That(exception.Message, Is.EqualTo("Column is already unique")); 105 | Assert.That(exception.Token!.CharacterInLine, Is.EqualTo(79)); 106 | Assert.That(exception.Token.Position, Is.EqualTo(79)); 107 | Assert.That(exception.Token.Line, Is.EqualTo(0)); 108 | } 109 | } 110 | 111 | [Test] 112 | public void PrimaryKeyTableConstraint_TableAlreadyPrimaryKey_ThrowsInvalidJsonException() 113 | { 114 | // arrange 115 | var generator = new SqlGenerator(); 116 | var databaseInfo = new DatabaseInfo(); 117 | 118 | // act 119 | try 120 | { 121 | generator.ProcessSqlSchema($"CREATE TABLE contact (name Text, id integer, address Text, PRIMARY KEY (id, name), PRIMARY KEY (id, address));", databaseInfo); 122 | Assert.Fail("InvalidSqlException didn't occur"); 123 | } 124 | catch (InvalidSqlException exception) 125 | { 126 | // assert 127 | Assert.That(exception.Message, Is.EqualTo("Table already has a primary key")); 128 | Assert.That(exception.Token!.CharacterInLine, Is.EqualTo(83)); 129 | Assert.That(exception.Token.Position, Is.EqualTo(83)); 130 | Assert.That(exception.Token.Line, Is.EqualTo(0)); 131 | } 132 | } 133 | 134 | [Test] 135 | public void PrimaryKeyTableConstraint_ColumnsAlreadyUnique_ThrowsInvalidJsonException() 136 | { 137 | // arrange 138 | var generator = new SqlGenerator(); 139 | var databaseInfo = new DatabaseInfo(); 140 | 141 | // act 142 | try 143 | { 144 | generator.ProcessSqlSchema($"CREATE TABLE contact (name Text, id integer, address Text, UNIQUE (id, name), PRIMARY KEY (id, name));", databaseInfo); 145 | Assert.Fail("InvalidSqlException didn't occur"); 146 | } 147 | catch (InvalidSqlException exception) 148 | { 149 | // assert 150 | Assert.That(exception.Message, Is.EqualTo("Columns are already unique")); 151 | Assert.That(exception.Token!.CharacterInLine, Is.EqualTo(78)); 152 | Assert.That(exception.Token.Position, Is.EqualTo(78)); 153 | Assert.That(exception.Token.Line, Is.EqualTo(0)); 154 | } 155 | } 156 | 157 | [Test] 158 | public void PrimaryKeyTableConstraint_HasConflictClause_CreatesTableInfo() 159 | { 160 | // arrange 161 | var generator = new SqlGenerator(); 162 | var databaseInfo = new DatabaseInfo(); 163 | 164 | // act 165 | generator.ProcessSqlSchema($"CREATE TABLE contact (name Text, id integer, address Text, PRIMARY KEY (name, id) ON CONFLICT ROLLBACK);", databaseInfo); 166 | 167 | // assert 168 | Assert.That(databaseInfo.Tables[0].PrimaryKey.Any(column => column.SqlName == "name"), Is.True); 169 | Assert.That(databaseInfo.Tables[0].PrimaryKey.Any(column => column.SqlName == "id"), Is.True); 170 | } 171 | 172 | 173 | [TestCase("(name, id)")] 174 | [TestCase("(name collate nocase, id)")] 175 | [TestCase("(name collate nocase asc, id)")] 176 | [TestCase("(name collate nocase desc, id)")] 177 | public void PrimaryKeyTableConstraint_IndexColumns_CreatesTableInfo(string indexedColumns) 178 | { 179 | // arrange 180 | var generator = new SqlGenerator(); 181 | var databaseInfo = new DatabaseInfo(); 182 | 183 | // act 184 | generator.ProcessSqlSchema($"CREATE TABLE contact (name Text, id integer, address Text, PRIMARY KEY {indexedColumns} ON CONFLICT ROLLBACK);", databaseInfo); 185 | 186 | // assert 187 | Assert.That(databaseInfo.Tables[0].PrimaryKey.Any(column => column.SqlName == "name"), Is.True); 188 | Assert.That(databaseInfo.Tables[0].PrimaryKey.Any(column => column.SqlName == "id"), Is.True); 189 | } 190 | } -------------------------------------------------------------------------------- /Tests/TableConstraintTests/TableUniqueConstraintTests.cs: -------------------------------------------------------------------------------- 1 | using Moq; 2 | using SqlSrcGen.Generator; 3 | 4 | namespace Tests.TableConstraintTests; 5 | 6 | public class TableUniqueConstraintTests 7 | { 8 | [Test] 9 | public void UniqueTableConstraint_Valid_CreatesTableInfo() 10 | { 11 | // arrange 12 | var generator = new SqlGenerator(); 13 | var databaseInfo = new DatabaseInfo(); 14 | 15 | // act 16 | generator.ProcessSqlSchema($"CREATE TABLE contact (name Text, unique (name));", databaseInfo); 17 | 18 | // assert 19 | Assert.That(databaseInfo.Tables[0].SqlName, Is.EqualTo("contact")); 20 | Assert.That(databaseInfo.Tables[0].CSharpName, Is.EqualTo("Contact")); 21 | var columns = databaseInfo.Tables[0].Columns.ToArray(); 22 | Assert.That(columns[0].SqlName, Is.EqualTo("name")); 23 | Assert.That(columns[0].CSharpName, Is.EqualTo("Name")); 24 | Assert.That(columns[0].SqlType, Is.EqualTo("Text")); 25 | Assert.That(columns[0].CSharpType, Is.EqualTo("string?")); 26 | Assert.That(columns[0].TypeAffinity, Is.EqualTo(TypeAffinity.TEXT)); 27 | Assert.That(columns[0].Unique, Is.True); 28 | } 29 | 30 | [Test] 31 | public void UniqueTableConstraint_TwoColumns_CreatesTableInfo() 32 | { 33 | // arrange 34 | var generator = new SqlGenerator(); 35 | var databaseInfo = new DatabaseInfo(); 36 | 37 | // act 38 | generator.ProcessSqlSchema($"CREATE TABLE contact (name Text, id integer, address Text, UNIQUE (name, id));", databaseInfo); 39 | 40 | // assert 41 | Assert.That(databaseInfo.Tables[0].Unique[0].Any(column => column.SqlName == "name"), Is.True); 42 | Assert.That(databaseInfo.Tables[0].Unique[0].Any(column => column.SqlName == "id"), Is.True); 43 | } 44 | 45 | [Test] 46 | public void UniqueTableConstraint_TwoUniqueColumns_CreatesTableInfo() 47 | { 48 | // arrange 49 | var generator = new SqlGenerator(); 50 | var databaseInfo = new DatabaseInfo(); 51 | 52 | // act 53 | generator.ProcessSqlSchema($"CREATE TABLE contact (name Text, id integer, address Text, UNIQUE (name, id), UNIQUE (id, address));", databaseInfo); 54 | 55 | // assert 56 | Assert.That(databaseInfo.Tables[0].Unique[0].Any(column => column.SqlName == "name"), Is.True); 57 | Assert.That(databaseInfo.Tables[0].Unique[0].Any(column => column.SqlName == "id"), Is.True); 58 | Assert.That(databaseInfo.Tables[0].Unique[1].Any(column => column.SqlName == "id"), Is.True); 59 | Assert.That(databaseInfo.Tables[0].Unique[1].Any(column => column.SqlName == "address"), Is.True); 60 | } 61 | 62 | [Test] 63 | public void UniqueTableConstraint_ColumnAlreadyPrimaryKey_ThrowsInvalidJsonException() 64 | { 65 | // arrange 66 | var generator = new SqlGenerator(); 67 | var databaseInfo = new DatabaseInfo(); 68 | 69 | // act 70 | try 71 | { 72 | generator.ProcessSqlSchema($"CREATE TABLE contact (name Text, id integer Primary Key, address Text, Unique (id));", databaseInfo); 73 | Assert.Fail("InvalidSqlException didn't occur"); 74 | } 75 | catch (InvalidSqlException exception) 76 | { 77 | Assert.That(exception.Message, Is.EqualTo("Column is already unqiue because it's a primary key")); 78 | Assert.That(exception.Token!.CharacterInLine, Is.EqualTo(79)); 79 | Assert.That(exception.Token.Position, Is.EqualTo(79)); 80 | Assert.That(exception.Token.Line, Is.EqualTo(0)); 81 | } 82 | } 83 | 84 | [Test] 85 | public void UniqueTableConstraint_ColumnsAlreadyPrimaryKey_ThrowsInvalidJsonException() 86 | { 87 | // arrange 88 | var generator = new SqlGenerator(); 89 | var databaseInfo = new DatabaseInfo(); 90 | 91 | // act 92 | try 93 | { 94 | generator.ProcessSqlSchema($"CREATE TABLE contact (name Text, id integer, address Text, PRIMARY KEY (id, name), UNIQUE (id, name));", databaseInfo); 95 | Assert.Fail("InvalidSqlException didn't occur"); 96 | } 97 | catch (InvalidSqlException exception) 98 | { 99 | // assert 100 | Assert.That(exception.Message, Is.EqualTo("Columns are already unqiue because they are a primary key")); 101 | Assert.That(exception.Token!.CharacterInLine, Is.EqualTo(83)); 102 | Assert.That(exception.Token.Position, Is.EqualTo(83)); 103 | Assert.That(exception.Token.Line, Is.EqualTo(0)); 104 | } 105 | } 106 | 107 | [Test] 108 | public void UniqueTableConstraint_ColumnsAlreadyUnique_ThrowsInvalidJsonException() 109 | { 110 | // arrange 111 | var generator = new SqlGenerator(); 112 | var databaseInfo = new DatabaseInfo(); 113 | 114 | // act 115 | try 116 | { 117 | generator.ProcessSqlSchema($"CREATE TABLE contact (name Text, id integer, address Text, UNIQUE (id, name), UNIQUE (id, name));", databaseInfo); 118 | Assert.Fail("InvalidSqlException didn't occur"); 119 | } 120 | catch (InvalidSqlException exception) 121 | { 122 | // assert 123 | Assert.That(exception.Message, Is.EqualTo("Columns are already unique")); 124 | Assert.That(exception.Token!.CharacterInLine, Is.EqualTo(78)); 125 | Assert.That(exception.Token.Position, Is.EqualTo(78)); 126 | Assert.That(exception.Token.Line, Is.EqualTo(0)); 127 | } 128 | } 129 | 130 | [Test] 131 | public void UniqueTableConstraint_HasConflictClause_CreatesTableInfo() 132 | { 133 | // arrange 134 | var generator = new SqlGenerator(); 135 | var databaseInfo = new DatabaseInfo(); 136 | 137 | // act 138 | generator.ProcessSqlSchema($"CREATE TABLE contact (name Text, id integer, address Text, Unique (name, id) ON CONFLICT ROLLBACK);", databaseInfo); 139 | 140 | // assert 141 | Assert.That(databaseInfo.Tables[0].Unique[0].Any(column => column.SqlName == "name"), Is.True); 142 | Assert.That(databaseInfo.Tables[0].Unique[0].Any(column => column.SqlName == "id"), Is.True); 143 | } 144 | } -------------------------------------------------------------------------------- /Tests/TableOptionsTests.cs: -------------------------------------------------------------------------------- 1 | using Moq; 2 | using SqlSrcGen.Generator; 3 | 4 | namespace Tests; 5 | 6 | public class TableOptionsTests 7 | { 8 | [TestCase("")] 9 | [TestCase("WITHOUT ROWID")] 10 | [TestCase("STRICT")] 11 | [TestCase("STRICT, WITHOUT ROWID")] 12 | [TestCase("WITHOUT ROWID, STRICT")] 13 | public void ProcessSqlSchema_CheckConstraint_CreatesTableInfo(string expr) 14 | { 15 | // arrange 16 | var generator = new SqlGenerator(); 17 | var databaseInfo = new DatabaseInfo(); 18 | 19 | // act 20 | generator.ProcessSqlSchema($"CREATE TABLE contact (name Text) {expr};", databaseInfo); 21 | 22 | // assert 23 | Assert.That(databaseInfo.Tables[0].SqlName, Is.EqualTo("contact")); 24 | Assert.That(databaseInfo.Tables[0].CSharpName, Is.EqualTo("Contact")); 25 | var columns = databaseInfo.Tables[0].Columns.ToArray(); 26 | Assert.That(columns[0].SqlName, Is.EqualTo("name")); 27 | Assert.That(columns[0].CSharpName, Is.EqualTo("Name")); 28 | Assert.That(columns[0].SqlType, Is.EqualTo("Text")); 29 | Assert.That(columns[0].CSharpType, Is.EqualTo("string?")); 30 | Assert.That(columns[0].TypeAffinity, Is.EqualTo(TypeAffinity.TEXT)); 31 | } 32 | } -------------------------------------------------------------------------------- /Tests/Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | enable 6 | enable 7 | 8 | false 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /Tests/UniqueConstraintTests.cs: -------------------------------------------------------------------------------- 1 | using Moq; 2 | using SqlSrcGen.Generator; 3 | 4 | namespace Tests; 5 | 6 | public class UniqueConstraintTests 7 | { 8 | [Test] 9 | public void ProcessSqlSchema_UniqueConstraint_CreatesTableInfo() 10 | { 11 | // arrange 12 | var generator = new SqlGenerator(); 13 | var databaseInfo = new DatabaseInfo(); 14 | 15 | // act 16 | generator.ProcessSqlSchema("CREATE TABLE contact (name Text unique);", databaseInfo); 17 | 18 | // assert 19 | Assert.That(databaseInfo.Tables[0].SqlName, Is.EqualTo("contact")); 20 | Assert.That(databaseInfo.Tables[0].CSharpName, Is.EqualTo("Contact")); 21 | var columns = databaseInfo.Tables[0].Columns.ToArray(); 22 | Assert.That(columns[0].Unique, Is.True); 23 | Assert.That(columns[0].SqlName, Is.EqualTo("name")); 24 | Assert.That(columns[0].CSharpName, Is.EqualTo("Name")); 25 | Assert.That(columns[0].SqlType, Is.EqualTo("Text")); 26 | Assert.That(columns[0].CSharpType, Is.EqualTo("string?")); 27 | Assert.That(columns[0].TypeAffinity, Is.EqualTo(TypeAffinity.TEXT)); 28 | } 29 | 30 | [Test] 31 | public void ProcessSqlSchema_UnqiueConstraintWithOnConflict_Parsed() 32 | { 33 | // arrange 34 | var generator = new SqlGenerator(); 35 | var databaseInfo = new DatabaseInfo(); 36 | 37 | // act 38 | generator.ProcessSqlSchema("CREATE TABLE contact (distance INTEGER UNIQUE ON CONFLICT ROLLBACK);", databaseInfo); 39 | 40 | // assert 41 | var columns = databaseInfo.Tables[0].Columns.ToArray(); 42 | Assert.That(columns[0].Unique, Is.True); 43 | Assert.That(columns[0].SqlType, Is.EqualTo("INTEGER")); 44 | Assert.That(columns[0].SqlName, Is.EqualTo("distance")); 45 | Assert.That(columns[0].CSharpType, Is.EqualTo("long?")); 46 | Assert.That(columns[0].TypeAffinity, Is.EqualTo(TypeAffinity.INTEGER)); 47 | } 48 | 49 | } -------------------------------------------------------------------------------- /Tests/Usings.cs: -------------------------------------------------------------------------------- 1 | global using NUnit.Framework; --------------------------------------------------------------------------------