git.iko.soy/rowtype-yoga/purescript-yoga-node-sqlite
on main help sign in
Initial: yoga-node-sqlite
65e54f1Mark
.github/workflows/ci.ymlnew file+9
@@ -0,0 +1,9 @@
1+name: CI
2+on: [push, pull_request]
3+jobs:
4+ build:
5+ runs-on: ubuntu-latest
6+ steps:
7+ - uses: actions/checkout@v4
8+ - uses: rowtype-yoga/purescript-yoga-actions/.github/actions/setup@main
9+ - run: spago build
.gitignorenew file+3
@@ -0,0 +1,3 @@
1+node_modules/
2+.spago/
3+output*/
package.jsonnew file+12
@@ -0,0 +1,12 @@
1+{
2+ "name": "purescript-yoga-node-sqlite",
3+ "version": "0.1.0",
4+ "description": "PureScript FFI bindings for SQLite",
5+ "license": "MIT",
6+ "repository": {
7+ "type": "git",
8+ "url": "https://github.com/rowtype-yoga/purescript-yoga-backend-stack.git",
9+ "directory": "packages/yoga-node-sqlite"
10+ },
11+ "keywords": ["purescript", "ffi", "bindings", "yoga"]
12+}
spago.yamlnew file+23
@@ -0,0 +1,23 @@
1+workspace:
2+ package_set:
3+ registry: 51.1.0
4+
5+package:
6+ name: yoga-node-sqlite
7+ publish:
8+ version: 0.1.0
9+ license: MIT
10+ location:
11+ githubOwner: rowtype-yoga
12+ githubRepo: purescript-yoga-node-sqlite
13+ dependencies:
14+ - aff: ">=7.0.0 <8.0.0"
15+ - effect: ">=4.0.0 <5.0.0"
16+ - prelude: ">=6.0.0 <7.0.0"
17+ - yoga-sql-types: "*"
18+ - arrays: ">=7.0.0 <8.0.0"
19+ - foreign: ">=7.0.0 <8.0.0"
20+ test:
21+ main: Test.NodeSQLite.Main
22+ dependencies:
23+ - spec: ">=7.0.0 <8.0.0"
src/Yoga/NodeSQLite/NodeSQLite.jsnew file+43
@@ -0,0 +1,43 @@
1+import { DatabaseSync } from 'node:sqlite';
2+
3+// Open a database connection
4+export const openImpl = (path) => {
5+ return new DatabaseSync(path);
6+};
7+
8+// Close a database connection
9+export const closeImpl = (db) => {
10+ db.close();
11+};
12+
13+// Execute SQL (for DDL/DML without results)
14+export const execImpl = (sql, db) => {
15+ db.exec(sql);
16+};
17+
18+// Prepare a statement
19+export const prepareImpl = (sql, db) => {
20+ return db.prepare(sql);
21+};
22+
23+// Run a statement (INSERT/UPDATE/DELETE)
24+export const runImpl = (params, stmt) => {
25+ stmt.run(...params);
26+};
27+
28+// Get a single row
29+export const getImpl = (params, stmt) => {
30+ const row = stmt.get(...params);
31+ return row || null;
32+};
33+
34+// Get all rows
35+export const allImpl = (params, stmt) => {
36+ return stmt.all(...params);
37+};
38+
39+// Finalize a statement
40+export const finalizeImpl = (stmt) => {
41+ // Note: Node's built-in SQLite doesn't have explicit finalize,
42+ // but we provide this for API compatibility
43+};
src/Yoga/NodeSQLite/NodeSQLite.pursnew file+85
@@ -0,0 +1,85 @@
1+module Yoga.NodeSQLite.NodeSQLite where
2+
3+import Prelude
4+
5+import Data.Either (Either(..))
6+import Data.Maybe (Maybe)
7+import Data.Newtype (class Newtype)
8+import Data.Nullable (Nullable)
9+import Data.Nullable as Nullable
10+import Effect (Effect)
11+import Effect.Uncurried (EffectFn1, EffectFn2, runEffectFn1, runEffectFn2)
12+import Foreign (Foreign)
13+
14+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
15+-- Opaque Foreign Types
16+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
17+
18+foreign import data Database :: Type
19+foreign import data Statement :: Type
20+
21+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
22+-- Newtypes for Type Safety
23+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
24+
25+newtype DatabasePath = DatabasePath String
26+
27+derive instance Newtype DatabasePath _
28+derive newtype instance Show DatabasePath
29+
30+-- | Special in-memory database path
31+inMemory :: DatabasePath
32+inMemory = DatabasePath ":memory:"
33+
34+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
35+-- FFI Imports
36+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
37+
38+foreign import openImpl :: EffectFn1 String Database
39+foreign import closeImpl :: EffectFn1 Database Unit
40+foreign import execImpl :: EffectFn2 String Database Unit
41+foreign import prepareImpl :: EffectFn2 String Database Statement
42+foreign import runImpl :: EffectFn2 (Array Foreign) Statement Unit
43+foreign import getImpl :: EffectFn2 (Array Foreign) Statement (Nullable Foreign)
44+foreign import allImpl :: EffectFn2 (Array Foreign) Statement (Array Foreign)
45+foreign import finalizeImpl :: EffectFn1 Statement Unit
46+
47+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
48+-- Database Operations
49+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
50+
51+-- | Open a database connection
52+open :: DatabasePath -> Effect Database
53+open (DatabasePath path) = runEffectFn1 openImpl path
54+
55+-- | Close a database connection
56+close :: Database -> Effect Unit
57+close = runEffectFn1 closeImpl
58+
59+-- | Execute SQL (for DDL/DML without results)
60+exec :: String -> Database -> Effect Unit
61+exec sql db = runEffectFn2 execImpl sql db
62+
63+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
64+-- Prepared Statements
65+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
66+
67+-- | Prepare a statement
68+prepare :: String -> Database -> Effect Statement
69+prepare sql db = runEffectFn2 prepareImpl sql db
70+
71+-- | Run a statement (INSERT/UPDATE/DELETE)
72+run :: Array Foreign -> Statement -> Effect Unit
73+run params stmt = runEffectFn2 runImpl params stmt
74+
75+-- | Get a single row
76+get :: Array Foreign -> Statement -> Effect (Maybe Foreign)
77+get params stmt = runEffectFn2 getImpl params stmt <#> Nullable.toMaybe
78+
79+-- | Get all rows
80+all :: Array Foreign -> Statement -> Effect (Array Foreign)
81+all params stmt = runEffectFn2 allImpl params stmt
82+
83+-- | Finalize a statement
84+finalize :: Statement -> Effect Unit
85+finalize = runEffectFn1 finalizeImpl
src/Yoga/NodeSQLite/TypedQuery.pursnew file+92
@@ -0,0 +1,92 @@
1+module Yoga.NodeSQLite.TypedQuery where
2+
3+import Prelude
4+
5+import Data.Either (Either(..))
6+import Data.Map (Map)
7+import Data.Maybe (Maybe(..))
8+import Data.Traversable (traverse)
9+import Effect (Effect)
10+import Foreign (Foreign)
11+import Heterogeneous.Folding (class HFoldlWithIndex)
12+import Yoga.NodeSQLite.NodeSQLite as SQLite
13+import Yoga.SQL.Types (SQLParameter, SQLQuery, TurnIntoSQLParam, argsFor, sqlQueryToString)
14+import Unsafe.Coerce (unsafeCoerce)
15+import Yoga.JSON (class ReadForeign)
16+import Yoga.JSON as JSON
17+
18+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
19+-- Convert SQL Types to SQLite Types
20+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
21+
22+-- | Convert a SQLParameter to Foreign for SQLite
23+sqlParamToForeign :: SQLParameter -> Foreign
24+sqlParamToForeign = unsafeCoerce
25+
26+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
27+-- Type-Safe Query Execution
28+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
29+
30+-- | Execute a typed SQL query and parse results with yoga-json
31+executeSql
32+ :: forall @params @result
33+ . HFoldlWithIndex TurnIntoSQLParam (Map String SQLParameter) { | params } (Map String SQLParameter)
34+ => ReadForeign result
35+ => SQLQuery params
36+ -> { | params }
37+ -> SQLite.Database
38+ -> Effect (Either String (Array result))
39+executeSql sqlQuery params db = do
40+ let
41+ sql = sqlQueryToString sqlQuery
42+ sqlParams = argsFor sqlQuery params
43+ foreignParams = map sqlParamToForeign sqlParams
44+ stmt <- SQLite.prepare sql db
45+ rows <- SQLite.all foreignParams stmt
46+ SQLite.finalize stmt
47+ pure $ traverse parseRow rows
48+ where
49+ parseRow :: Foreign -> Either String result
50+ parseRow row = case (JSON.read row :: Either _ result) of
51+ Left errors -> Left (show errors)
52+ Right parsed -> Right parsed
53+
54+-- | Execute a typed SQL query and return a single row
55+executeSqlOne
56+ :: forall @params @result
57+ . HFoldlWithIndex TurnIntoSQLParam (Map String SQLParameter) { | params } (Map String SQLParameter)
58+ => ReadForeign result
59+ => SQLQuery params
60+ -> { | params }
61+ -> SQLite.Database
62+ -> Effect (Either String (Maybe result))
63+executeSqlOne sqlQuery params db = do
64+ let
65+ sql = sqlQueryToString sqlQuery
66+ sqlParams = argsFor sqlQuery params
67+ foreignParams = map sqlParamToForeign sqlParams
68+ stmt <- SQLite.prepare sql db
69+ maybeRow <- SQLite.get foreignParams stmt
70+ SQLite.finalize stmt
71+ case maybeRow of
72+ Nothing -> pure $ Right Nothing
73+ Just row -> pure $ case (JSON.read row :: Either _ result) of
74+ Left errors -> Left (show errors)
75+ Right parsed -> Right (Just parsed)
76+
77+-- | Execute a mutation (INSERT/UPDATE/DELETE)
78+executeMutation
79+ :: forall @params
80+ . HFoldlWithIndex TurnIntoSQLParam (Map String SQLParameter) { | params } (Map String SQLParameter)
81+ => SQLQuery params
82+ -> { | params }
83+ -> SQLite.Database
84+ -> Effect Unit
85+executeMutation sqlQuery params db = do
86+ let
87+ sql = sqlQueryToString sqlQuery
88+ sqlParams = argsFor sqlQuery params
89+ foreignParams = map sqlParamToForeign sqlParams
90+ stmt <- SQLite.prepare sql db
91+ SQLite.run foreignParams stmt
92+ SQLite.finalize stmt
test/Main.pursnew file+27
@@ -0,0 +1,27 @@
1+module Test.NodeSQLite.Main where
2+
3+import Prelude
4+
5+import Effect (Effect)
6+import Effect.Aff (Aff, launchAff_)
7+import Effect.Class (liftEffect)
8+import Test.Spec (Spec, before, describe, it)
9+import Test.Spec.Assertions (shouldEqual)
10+import Test.Spec.Reporter.Console (consoleReporter)
11+import Test.Spec.Runner (runSpec)
12+import Yoga.NodeSQLite.NodeSQLite as NodeSQLite
13+
14+setupNodeSQLite :: Aff NodeSQLite.Database
15+setupNodeSQLite = liftEffect $ NodeSQLite.open (NodeSQLite.DatabasePath ":memory:")
16+
17+spec :: Spec Unit
18+spec = before setupNodeSQLite do
19+ describe "Yoga.NodeSQLite FFI" do
20+ describe "Basic Operations" do
21+ it "creates table and inserts data" \db -> do
22+ liftEffect $ NodeSQLite.exec "CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT)" db
23+ liftEffect $ NodeSQLite.exec "INSERT INTO test (name) VALUES ('Charlie')" db
24+ 1 `shouldEqual` 1
25+
26+main :: Effect Unit
27+main = launchAff_ $ runSpec [ consoleReporter ] spec