Skip to content

Commit

Permalink
SQLite support
Browse files Browse the repository at this point in the history
  • Loading branch information
piaste committed Jun 25, 2018
1 parent 19b1dae commit 96ce229
Show file tree
Hide file tree
Showing 4 changed files with 97 additions and 32 deletions.
25 changes: 25 additions & 0 deletions docs/content/core/crud.fsx
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,31 @@ SQLProvider also supports async database operations:
*)

ctx.SubmitUpdatesAsync() |> Async.StartAsTask

(**
### OnConflict
The [SQLite](http://sqlite.org/lang_conflict.html) and [PostgreSQL 9.5+](https://www.postgresql.org/docs/current/static/sql-insert.html#SQL-ON-CONFLICT) providers support conflict resolution for INSERT statements.
They allow the user to specify if a unique constraint violation should be solved by ignoring the statement (DO NOTHING) or updating existing rows (DO UPDATE).
You can leverage this feature by setting the `OnConflict` property on a row object:
* Setting it to `DoNothing` will add the DO NOTHING clause (PostgreSQL) or the OR IGNORE clause (SQLite).
* Setting it to `Update` will add a DO UPDATE clause on the primary key constraint for all columns (PostgreSQL) or a OR REPLACE clause (SQLite).
Sql Server has a similar feature in the form of the MERGE statement. This is not yet supported.
*)

let ctx = sql.GetDataContext()

let emp = ctx.Main.Employees.Create()
emp.Id <- 1
emp.FirstName <- "Jane"
emp.LastName <- "Doe"

emp.OnConflict <- FSharp.Data.Sql.Common.OnConflict.Update

ctx.SubmitUpdates()

(**
Expand Down
31 changes: 0 additions & 31 deletions docs/content/core/postgresql.fsx
Original file line number Diff line number Diff line change
Expand Up @@ -104,34 +104,3 @@ type sql =
useOptTypes,
owner>


(**
### OnConflict
Beginning with version 9.5, [PostgreSQL supports the ON CONFLICT clause to INSERT statements.](https://www.postgresql.org/docs/current/static/sql-insert.html#SQL-ON-CONFLICT)
It allows the user to specify if a unique constraint violation should be solved by ignoring the statement (DO NOTHING) or updating existing rows (DO UPDATE).
SqlProvider can leverage this feature by setting the `OnConflict` property on a row object. Setting it to `DoNothing` will add the DO NOTHING clause. Setting it to `Update` will add a DO UPDATE clause that updates all existing columns if the conflict happened on the primary key constraint.
It is not currently supported to emit a DO UPDATE clause for non-primary key unique constraints.
*)

let ctx = sql.GetDataContext()

let orders = ctx.Main.Orders

let row = orders.Create()
row.CustomerId <- customer.CustomerId
row.EmployeeId <- employee.EmployeeId
row.Freight <- 10M
row.OrderDate <- now.AddDays(-1.0)
row.RequiredDate <- now.AddDays(1.0)
row.ShipAddress <- "10 Downing St"
row.ShipCity <- "London"
row.ShipName <- "Dragons den"
row.ShipPostalCode <- "SW1A 2AA"
row.ShipRegion <- "UK"
row.ShippedDate <- now

row.OnConflict <- FSharp.Data.Sql.Common.OnConflict.Update

ctx.SubmitUpdates()
9 changes: 8 additions & 1 deletion src/SQLProvider/Providers.SQLite.fs
Original file line number Diff line number Diff line change
Expand Up @@ -240,8 +240,15 @@ type internal SQLiteProvider(resolutionPath, contextSchemaPath, referencedAssemb
|> List.toArray
|> Array.unzip

let conflictClause =
match entity.OnConflict with
| Throw -> ""
| Update -> " OR REPLACE "
| DoNothing -> " OR IGNORE "

sb.Clear() |> ignore
~~(sprintf "INSERT INTO %s (%s) VALUES (%s); SELECT last_insert_rowid();"
~~(sprintf "INSERT %s INTO %s (%s) VALUES (%s); SELECT last_insert_rowid();"
conflictClause
entity.Table.FullName
(String.Join(",",columnNames))
(String.Join(",",values |> Array.map(fun p -> p.ParameterName))))
Expand Down
64 changes: 64 additions & 0 deletions tests/SqlProvider.Tests/CrudTests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -148,3 +148,67 @@ let ``Can enlist in a transaction scope and rollback changes without complete``(
|> Seq.toList

Assert.AreEqual([], created)


[<Test>]
let ``Insert on conflict replace applies correctly``() =
let dc = sql.GetDataContext()

let getCustomer =
query { for cust in dc.Main.Customers do
select cust }

let originalCustomer = getCustomer |> Seq.head

let ent = createCustomer dc

let newAddress = "FsProjects 2.0"

ent.CustomerId <- originalCustomer.CustomerId
ent.Address <- newAddress
ent.OnConflict <- FSharp.Data.Sql.Common.OnConflict.Update

dc.SubmitUpdates()
dc.SubmitUpdates() // run twice just to test extra won't hurt

let updatedCustomer = getCustomer |> Seq.head

Assert.AreEqual(updatedCustomer.Address, newAddress)

let ent2 = createCustomer dc

ent2.CustomerId <- updatedCustomer.CustomerId
ent2.Address <- "asdkjskdjsldjskjdls"
ent2.OnConflict <- FSharp.Data.Sql.Common.OnConflict.DoNothing

dc.SubmitUpdates()
dc.SubmitUpdates()

let notUpdatedCustomer = getCustomer |> Seq.head

Assert.AreEqual(notUpdatedCustomer.Address, newAddress)

// let's create new context just to test that it is actually there.
let dc2 = sql.GetDataContext()

let newCustomers =
query { for cust in dc2.Main.Customers do
select cust }
|> Seq.toList

let created =
newCustomers |> List.find (fun x -> x.CustomerId = "SQLPROVIDER")

Assert.AreEqual(originalCustomers.Length, newCustomers.Length - 1)

Assert.AreEqual("Updated Number", created.Phone)
created.Delete()
dc2.SubmitUpdates()
dc2.SubmitUpdates()

let reallyDeleted =
query { for cust in dc2.Main.Customers do
select cust }
|> Seq.toList

Assert.AreEqual(originalCustomers.Length, reallyDeleted.Length)

0 comments on commit 96ce229

Please sign in to comment.