├── .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 |
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 | #  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;
--------------------------------------------------------------------------------