From 91f1b5516d295c416c0cde54d1b0ad8b618c2797 Mon Sep 17 00:00:00 2001
From: Christian Findlay <16697547+MelbourneDeveloper@users.noreply.github.com>
Date: Mon, 11 Aug 2025 21:27:31 +0800
Subject: [PATCH 01/16] Add type provider
---
.vscode/launch.json | 11 +
DataProvider.sln | 14 ++
.../DataProvider.Example.FSharp.fsproj | 36 ++++
.../GetCustomers.lql | 5 +
.../GetInvoices.lql | 5 +
.../DataProvider.Example.FSharp/Program.fs | 200 ++++++++++++++++++
.../DataProvider.SQLite.csproj | 38 +++-
.../DataProvider.SqlServer.csproj | 38 +++-
DataProvider/DataProvider/DataProvider.csproj | 35 ++-
.../Lql.TypeProvider.FSharp.fsproj | 26 +++
.../LqlTypeProvider.fs | 200 ++++++++++++++++++
11 files changed, 596 insertions(+), 12 deletions(-)
create mode 100644 DataProvider/DataProvider.Example.FSharp/DataProvider.Example.FSharp.fsproj
create mode 100644 DataProvider/DataProvider.Example.FSharp/GetCustomers.lql
create mode 100644 DataProvider/DataProvider.Example.FSharp/GetInvoices.lql
create mode 100644 DataProvider/DataProvider.Example.FSharp/Program.fs
create mode 100644 Lql/Lql.TypeProvider.FSharp/Lql.TypeProvider.FSharp.fsproj
create mode 100644 Lql/Lql.TypeProvider.FSharp/LqlTypeProvider.fs
diff --git a/.vscode/launch.json b/.vscode/launch.json
index b8986da..1434b0a 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -29,6 +29,17 @@
"cwd": "${workspaceFolder}/DataProvider/DataProvider.Example",
"console": "internalConsole",
"stopAtEntry": false
+ },
+ {
+ "name": "Run DataProvider.Example.FSharp",
+ "type": "coreclr",
+ "request": "launch",
+ "preLaunchTask": "build",
+ "program": "${workspaceFolder}/DataProvider/DataProvider.Example.FSharp/bin/Debug/net9.0/DataProvider.Example.FSharp.dll",
+ "args": [],
+ "cwd": "${workspaceFolder}/DataProvider/DataProvider.Example.FSharp",
+ "console": "internalConsole",
+ "stopAtEntry": false
}
]
}
\ No newline at end of file
diff --git a/DataProvider.sln b/DataProvider.sln
index e02ac8b..73f85ea 100644
--- a/DataProvider.sln
+++ b/DataProvider.sln
@@ -37,6 +37,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DataProvider.Example.Tests"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DataProvider.Example", "DataProvider\DataProvider.Example\DataProvider.Example.csproj", "{EA9A0385-249F-4141-AD03-D67649110A84}"
EndProject
+Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "Lql.TypeProvider.FSharp", "Lql\Lql.TypeProvider.FSharp\Lql.TypeProvider.FSharp.fsproj", "{B1234567-89AB-CDEF-0123-456789ABCDEF}"
+EndProject
+Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "DataProvider.Example.FSharp", "DataProvider\DataProvider.Example.FSharp\DataProvider.Example.FSharp.fsproj", "{C1234567-89AB-CDEF-0123-456789ABCDEF}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -103,6 +107,14 @@ Global
{EA9A0385-249F-4141-AD03-D67649110A84}.Debug|Any CPU.Build.0 = Debug|Any CPU
{EA9A0385-249F-4141-AD03-D67649110A84}.Release|Any CPU.ActiveCfg = Release|Any CPU
{EA9A0385-249F-4141-AD03-D67649110A84}.Release|Any CPU.Build.0 = Release|Any CPU
+ {B1234567-89AB-CDEF-0123-456789ABCDEF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {B1234567-89AB-CDEF-0123-456789ABCDEF}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {B1234567-89AB-CDEF-0123-456789ABCDEF}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {B1234567-89AB-CDEF-0123-456789ABCDEF}.Release|Any CPU.Build.0 = Release|Any CPU
+ {C1234567-89AB-CDEF-0123-456789ABCDEF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {C1234567-89AB-CDEF-0123-456789ABCDEF}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {C1234567-89AB-CDEF-0123-456789ABCDEF}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {C1234567-89AB-CDEF-0123-456789ABCDEF}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -121,6 +133,8 @@ Global
{A7EC2050-FE5E-4BBD-AF5F-7F07D3688118} = {43BAF0A3-C050-BE83-B489-7FC6F9FDE235}
{16FA9B36-CB2A-4B79-A3BE-937C94BF03F8} = {43BAF0A3-C050-BE83-B489-7FC6F9FDE235}
{EA9A0385-249F-4141-AD03-D67649110A84} = {43BAF0A3-C050-BE83-B489-7FC6F9FDE235}
+ {B1234567-89AB-CDEF-0123-456789ABCDEF} = {54B846BA-A27D-B76F-8730-402A5742FF43}
+ {C1234567-89AB-CDEF-0123-456789ABCDEF} = {43BAF0A3-C050-BE83-B489-7FC6F9FDE235}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {53128A75-E7B6-4B83-B079-A309FCC2AD9C}
diff --git a/DataProvider/DataProvider.Example.FSharp/DataProvider.Example.FSharp.fsproj b/DataProvider/DataProvider.Example.FSharp/DataProvider.Example.FSharp.fsproj
new file mode 100644
index 0000000..901ea5b
--- /dev/null
+++ b/DataProvider/DataProvider.Example.FSharp/DataProvider.Example.FSharp.fsproj
@@ -0,0 +1,36 @@
+
+
+
+ Exe
+ net9.0
+ true
+ preview
+ true
+ 5
+
+
+
+
+
+
+
+
+ PreserveNewest
+
+
+ PreserveNewest
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/DataProvider/DataProvider.Example.FSharp/GetCustomers.lql b/DataProvider/DataProvider.Example.FSharp/GetCustomers.lql
new file mode 100644
index 0000000..a9341d4
--- /dev/null
+++ b/DataProvider/DataProvider.Example.FSharp/GetCustomers.lql
@@ -0,0 +1,5 @@
+Customer
+|> join(Address, on = Customer.Id = Address.CustomerId)
+|> filter(fn(row) => (@customerId IS NULL OR Customer.Id = @customerId))
+|> select(Customer.Id, Customer.CustomerName, Customer.Email, Customer.Phone, Customer.CreatedDate, Address.Id AS AddressId, Address.CustomerId, Address.Street, Address.City, Address.State, Address.ZipCode, Address.Country)
+|> order_by(Customer.CustomerName)
\ No newline at end of file
diff --git a/DataProvider/DataProvider.Example.FSharp/GetInvoices.lql b/DataProvider/DataProvider.Example.FSharp/GetInvoices.lql
new file mode 100644
index 0000000..9fdd394
--- /dev/null
+++ b/DataProvider/DataProvider.Example.FSharp/GetInvoices.lql
@@ -0,0 +1,5 @@
+Invoice
+|> join(InvoiceLine, on = Invoice.Id = InvoiceLine.InvoiceId)
+|> filter(fn(row) => (@customerName IS NULL OR Invoice.CustomerName LIKE '%' + @customerName + '%'))
+|> select(Invoice.Id, Invoice.InvoiceNumber, Invoice.InvoiceDate, Invoice.CustomerName, Invoice.CustomerEmail, Invoice.TotalAmount, InvoiceLine.Description, InvoiceLine.Quantity, InvoiceLine.UnitPrice, InvoiceLine.Amount)
+|> order_by(Invoice.InvoiceDate)
\ No newline at end of file
diff --git a/DataProvider/DataProvider.Example.FSharp/Program.fs b/DataProvider/DataProvider.Example.FSharp/Program.fs
new file mode 100644
index 0000000..770ecf9
--- /dev/null
+++ b/DataProvider/DataProvider.Example.FSharp/Program.fs
@@ -0,0 +1,200 @@
+open System
+open System.IO
+open Microsoft.Data.Sqlite
+open Lql.TypeProvider.FSharp
+open Lql
+open Lql.SQLite
+
+///
+/// F# example demonstrating LQL usage with SQLite database
+///
+[]
+let main argv =
+ let connectionString = "Data Source=invoices.db"
+
+ // Ensure database file exists and create tables if needed
+ let setupDatabase () =
+ use connection = new SqliteConnection(connectionString)
+ connection.Open()
+
+ // Create tables
+ let createTablesSql = """
+ CREATE TABLE IF NOT EXISTS Invoice (
+ Id INTEGER PRIMARY KEY,
+ InvoiceNumber TEXT NOT NULL,
+ InvoiceDate TEXT NOT NULL,
+ CustomerName TEXT NOT NULL,
+ CustomerEmail TEXT NULL,
+ TotalAmount REAL NOT NULL,
+ DiscountAmount REAL NULL,
+ Notes TEXT NULL
+ );
+
+ CREATE TABLE IF NOT EXISTS InvoiceLine (
+ Id INTEGER PRIMARY KEY,
+ InvoiceId SMALLINT NOT NULL,
+ Description TEXT NOT NULL,
+ Quantity REAL NOT NULL,
+ UnitPrice REAL NOT NULL,
+ Amount REAL NOT NULL,
+ DiscountPercentage REAL NULL,
+ Notes TEXT NULL,
+ FOREIGN KEY (InvoiceId) REFERENCES Invoice (Id)
+ );
+
+ CREATE TABLE IF NOT EXISTS Customer (
+ Id INTEGER PRIMARY KEY,
+ CustomerName TEXT NOT NULL,
+ Email TEXT NULL,
+ Phone TEXT NULL,
+ CreatedDate TEXT NOT NULL
+ );
+
+ CREATE TABLE IF NOT EXISTS Address (
+ Id INTEGER PRIMARY KEY,
+ CustomerId SMALLINT NOT NULL,
+ Street TEXT NOT NULL,
+ City TEXT NOT NULL,
+ State TEXT NOT NULL,
+ ZipCode TEXT NOT NULL,
+ Country TEXT NOT NULL,
+ FOREIGN KEY (CustomerId) REFERENCES Customer (Id)
+ );
+ """
+
+ use command = new SqliteCommand(createTablesSql, connection)
+ command.ExecuteNonQuery() |> ignore
+
+ // Clear existing data
+ let clearDataSql = "DELETE FROM InvoiceLine; DELETE FROM Invoice; DELETE FROM Address; DELETE FROM Customer;"
+ use clearCommand = new SqliteCommand(clearDataSql, connection)
+ clearCommand.ExecuteNonQuery() |> ignore
+
+ // Insert sample data
+ let insertDataSql = """
+ INSERT INTO Invoice (InvoiceNumber, InvoiceDate, CustomerName, CustomerEmail, TotalAmount, DiscountAmount, Notes)
+ VALUES ('INV-001', '2024-01-15', 'Acme Corp', 'billing@acme.com', 1250.00, 50.00, 'Sample invoice'),
+ ('INV-002', '2024-01-20', 'Tech Solutions', 'billing@techsolutions.com', 800.00, 25.00, 'Monthly service');
+
+ INSERT INTO InvoiceLine (InvoiceId, Description, Quantity, UnitPrice, Amount, DiscountPercentage, Notes)
+ VALUES
+ (1, 'Software License', 1.0, 1000.00, 1000.00, NULL, NULL),
+ (1, 'Support Package', 1.0, 250.00, 250.00, NULL, 'First year'),
+ (2, 'Consulting Hours', 8.0, 100.00, 800.00, NULL, 'Development work');
+
+ INSERT INTO Customer (CustomerName, Email, Phone, CreatedDate)
+ VALUES
+ ('Acme Corp', 'contact@acme.com', '555-0100', '2024-01-01'),
+ ('Tech Solutions', 'info@techsolutions.com', '555-0200', '2024-01-02'),
+ ('Global Industries', 'hello@global.com', '555-0300', '2024-01-03');
+
+ INSERT INTO Address (CustomerId, Street, City, State, ZipCode, Country)
+ VALUES
+ (1, '123 Business Ave', 'New York', 'NY', '10001', 'USA'),
+ (1, '456 Main St', 'Albany', 'NY', '12201', 'USA'),
+ (2, '789 Tech Blvd', 'San Francisco', 'CA', '94105', 'USA'),
+ (3, '321 Corporate Dr', 'Chicago', 'IL', '60601', 'USA');
+ """
+
+ use insertCommand = new SqliteCommand(insertDataSql, connection)
+ insertCommand.ExecuteNonQuery() |> ignore
+
+ // Function to execute LQL queries using the extension functions
+ let testLqlQueries () =
+ async {
+ printfn "=== Testing LQL Queries in F# ==="
+
+ // Test GetCustomers query
+ let customersLql = File.ReadAllText("GetCustomers.lql")
+ printfn "\n--- Executing GetCustomers.lql ---"
+ printfn "LQL Query:\n%s\n" customersLql
+
+ let! customersResult = executeLqlQuery connectionString customersLql
+ match customersResult with
+ | Ok customers ->
+ printfn "Found %d customers:" customers.Length
+ for customer in customers do
+ let customerName = customer.["CustomerName"] :?> string
+ let email = customer.["Email"]
+ let city = customer.["City"] :?> string
+ let state = customer.["State"] :?> string
+ printfn " - %s (%A) from %s, %s" customerName email city state
+ | Error errorMsg ->
+ printfn "Error executing customers query: %s" errorMsg
+
+ // Test GetInvoices query with parameter
+ let invoicesLql = File.ReadAllText("GetInvoices.lql")
+ printfn "\n--- Executing GetInvoices.lql ---"
+ printfn "LQL Query:\n%s\n" invoicesLql
+
+ let! invoicesResult = executeLqlQuery connectionString invoicesLql
+ match invoicesResult with
+ | Ok invoices ->
+ printfn "Found %d invoice lines:" invoices.Length
+ for invoice in invoices do
+ let invoiceNumber = invoice.["InvoiceNumber"] :?> string
+ let customerName = invoice.["CustomerName"] :?> string
+ let description = invoice.["Description"] :?> string
+ let amount = invoice.["Amount"] :?> float
+ printfn " - %s for %s: %s ($%.2f)" invoiceNumber customerName description amount
+ | Error errorMsg ->
+ printfn "Error executing invoices query: %s" errorMsg
+
+ // Test a simple inline query
+ printfn "\n--- Executing inline LQL query ---"
+ let inlineLql = """
+ Customer
+ |> select(Customer.Id, Customer.CustomerName, Customer.Email)
+ |> filter(fn(row) => Customer.CustomerName LIKE '%Corp%')
+ |> order_by(Customer.CustomerName)
+ """
+ printfn "LQL Query:\n%s\n" inlineLql
+
+ let! inlineResult = executeLqlQuery connectionString inlineLql
+ match inlineResult with
+ | Ok results ->
+ printfn "Found %d matching customers:" results.Length
+ for result in results do
+ let id = result.["Id"] :?> int64
+ let name = result.["CustomerName"] :?> string
+ let email = result.["Email"]
+ printfn " - ID: %d, Name: %s, Email: %A" id name email
+ | Error errorMsg ->
+ printfn "Error executing inline query: %s" errorMsg
+ }
+
+ // Function to demonstrate direct SQL conversion using the type provider functions
+ let testSqlConversion () =
+ printfn "\n=== Testing LQL to SQL Conversion ==="
+
+ let testQueries = [
+ "Simple Select", "Customer |> select(Customer.Id, Customer.CustomerName)"
+ "With Filter", "Customer |> filter(fn(row) => Customer.Id > 1) |> select(Customer.CustomerName)"
+ "With Join", "Customer |> join(Address, on = Customer.Id = Address.CustomerId) |> select(Customer.CustomerName, Address.City)"
+ ]
+
+ for (name, lql) in testQueries do
+ printfn "\n--- %s ---" name
+ printfn "LQL: %s" lql
+
+ match lqlToSql lql with
+ | Ok sql ->
+ printfn "SQL: %s" sql
+ | Error errorMsg ->
+ printfn "Error: %s" errorMsg
+
+ try
+ setupDatabase()
+ printfn "Database setup completed successfully."
+
+ testSqlConversion()
+
+ testLqlQueries() |> Async.RunSynchronously
+
+ printfn "\nF# LQL example completed successfully!"
+ 0
+ with
+ | ex ->
+ printfn "Error: %s" ex.Message
+ printfn "Stack trace: %s" ex.StackTrace
+ 1
\ No newline at end of file
diff --git a/DataProvider/DataProvider.SQLite/DataProvider.SQLite.csproj b/DataProvider/DataProvider.SQLite/DataProvider.SQLite.csproj
index 9331522..ce1ba00 100644
--- a/DataProvider/DataProvider.SQLite/DataProvider.SQLite.csproj
+++ b/DataProvider/DataProvider.SQLite/DataProvider.SQLite.csproj
@@ -1,11 +1,21 @@
- false
- false
+
DataProvider.SQLite
- 1.0.0
- DataProvider
- SQLite source generator for DataProvider
+ 0.1.0-beta
+ ChristianFindlay
+ SQLite source generator for DataProvider. Provides compile-time safe database access with automatic code generation from SQL files for SQLite databases.
+ source-generator;sql;sqlite;database;compile-time-safety;code-generation
+ https://github.com/MelbourneDeveloper/DataProvider
+ https://github.com/MelbourneDeveloper/DataProvider
+ git
+ MIT
+ README.md
+ false
+ Initial beta release of DataProvider.SQLite source generator.
+
+
+ true
CA1849;CA2100;CA1308;EPC13;CA1017;CA1305;CA1307;
@@ -45,4 +55,22 @@
+
+
+
+
+
+
+
+
+ true
+ portable
+ true
+ true
+ snupkg
+ true
+ true
+ true
+ true
+
\ No newline at end of file
diff --git a/DataProvider/DataProvider.SqlServer/DataProvider.SqlServer.csproj b/DataProvider/DataProvider.SqlServer/DataProvider.SqlServer.csproj
index e4e57ce..476a2d1 100644
--- a/DataProvider/DataProvider.SqlServer/DataProvider.SqlServer.csproj
+++ b/DataProvider/DataProvider.SqlServer/DataProvider.SqlServer.csproj
@@ -1,11 +1,21 @@
- false
- false
+
DataProvider.SqlServer
- 1.0.0
- DataProvider
- SQL Server source generator for DataProvider
+ 0.1.0-beta
+ ChristianFindlay
+ SQL Server source generator for DataProvider. Provides compile-time safe database access with automatic code generation from SQL files for SQL Server databases.
+ source-generator;sql;sqlserver;database;compile-time-safety;code-generation
+ https://github.com/MelbourneDeveloper/DataProvider
+ https://github.com/MelbourneDeveloper/DataProvider
+ git
+ MIT
+ README.md
+ false
+ Initial beta release of DataProvider.SqlServer source generator.
+
+
+ true
@@ -16,4 +26,22 @@
+
+
+
+
+
+
+
+
+ true
+ portable
+ true
+ true
+ snupkg
+ true
+ true
+ true
+ true
+
\ No newline at end of file
diff --git a/DataProvider/DataProvider/DataProvider.csproj b/DataProvider/DataProvider/DataProvider.csproj
index b995cf7..ef51ccd 100644
--- a/DataProvider/DataProvider/DataProvider.csproj
+++ b/DataProvider/DataProvider/DataProvider.csproj
@@ -1,9 +1,22 @@
-
- false
+
+ DataProvider
+ 0.1.0-beta
+ ChristianFindlay
+ A source generator that creates compile-time safe extension methods for database operations from SQL files. Generates strongly-typed C# code based on your SQL queries and database schema, ensuring type safety and eliminating runtime SQL errors.
+ source-generator;sql;database;compile-time-safety;code-generation;sqlite;sqlserver
+ https://github.com/christianfindlay/DataProvider
+ https://github.com/christianfindlay/DataProvider
+ git
+ MIT
+ README.md
false
+ Initial release of DataProvider source generator for compile-time safe database operations.
+
+
+ true
@@ -13,9 +26,27 @@
+
+
+
+
+
$(NoWarn);EPC13;EPS06;CA1017;CA1002;CA1822;CA1859
+
+
+ true
+ portable
+ true
+ true
+ snupkg
+ true
+ true
+ true
+ true
+
+
diff --git a/Lql/Lql.TypeProvider.FSharp/Lql.TypeProvider.FSharp.fsproj b/Lql/Lql.TypeProvider.FSharp/Lql.TypeProvider.FSharp.fsproj
new file mode 100644
index 0000000..d84284e
--- /dev/null
+++ b/Lql/Lql.TypeProvider.FSharp/Lql.TypeProvider.FSharp.fsproj
@@ -0,0 +1,26 @@
+
+
+
+ net9.0
+ true
+ preview
+ false
+ 3
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Lql/Lql.TypeProvider.FSharp/LqlTypeProvider.fs b/Lql/Lql.TypeProvider.FSharp/LqlTypeProvider.fs
new file mode 100644
index 0000000..9186b8b
--- /dev/null
+++ b/Lql/Lql.TypeProvider.FSharp/LqlTypeProvider.fs
@@ -0,0 +1,200 @@
+namespace Lql.TypeProvider.FSharp
+
+open System
+open System.IO
+open Microsoft.Data.Sqlite
+open Lql
+open Lql.SQLite
+
+///
+/// Extension module for working with LQL queries in F#
+///
+[]
+module LqlExtensions =
+
+ ///
+ /// Execute an LQL query against a SQLite database using exception-based error handling
+ ///
+ /// The SQLite connection string
+ /// The LQL query string
+ let executeLqlQuery (connectionString: string) (lqlQuery: string) =
+ async {
+ try
+ use connection = new SqliteConnection(connectionString)
+ do! connection.OpenAsync() |> Async.AwaitTask
+
+ let lqlStatement = LqlStatementConverter.ToStatement(lqlQuery)
+
+ // Handle the Result type from the library
+ if lqlStatement.GetType().Name.Contains("Success") then
+ let statement = lqlStatement.GetType().GetProperty("Value").GetValue(lqlStatement) :?> LqlStatement
+ let sqlResult = statement.ToSQLite()
+
+ if sqlResult.GetType().Name.Contains("Success") then
+ let sql = sqlResult.GetType().GetProperty("Value").GetValue(sqlResult) :?> string
+
+ use command = new SqliteCommand(sql, connection)
+ use reader = command.ExecuteReader()
+
+ let results = ResizeArray