- 📘 Documentation
sqlgg- 🧭 Contents
- 🆕 Feature Review
- 🛣️ Roadmap
Features are listed from latest to earliest, with detailed descriptions and examples.
Added: May 2025
Extends column-level customization to INSERT and UPDATE statements using the same custom modules as query parameters.
CREATE TABLE users (
-- [sqlgg] module=UserId
id INT AUTO_INCREMENT PRIMARY KEY,
-- [sqlgg] module=UserName
name VARCHAR(255) NOT NULL,
-- [sqlgg] module=UserEmail
email VARCHAR(255) UNIQUE,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
-- @create_user
INSERT INTO users (name, email)
VALUES (@name, @email);
-- @update_user_email
UPDATE users
SET email = @new_email
WHERE id = @user_id;
-- @update_user_profile
UPDATE users
SET name = @name, email = @email
WHERE id = @user_id;let create_user db ~name ~email =
let set_params stmt =
let p = T.start_params stmt 2 in
T.set_param_Text p (UserName.set_param name);
T.set_param_Text p (UserEmail.set_param email);
T.finish_params p
in
T.execute db
"INSERT INTO users (name, email) VALUES (?, ?)"
set_params
let update_user_email db ~new_email ~user_id =
let set_params stmt =
let p = T.start_params stmt 2 in
T.set_param_Text p (UserEmail.set_param new_email);
T.set_param_Int p (UserId.set_param user_id);
T.finish_params p
in
T.execute db
"UPDATE users SET email = ? WHERE id = ?"
set_params
let update_user_profile db ~name ~email ~user_id =
let set_params stmt =
let p = T.start_params stmt 3 in
T.set_param_Text p (UserName.set_param name);
T.set_param_Text p (UserEmail.set_param email);
T.set_param_Int p (UserId.set_param user_id);
T.finish_params p
in
T.execute db
"UPDATE users SET name = ?, email = ? WHERE id = ?"
set_params
Uses the same set_param functions as for query parameters - no additional implementation needed.
→ See Column-level Customization for Query Parameters for module implementation details
→ PR #199
Added: May 2025
Extends the existing column-level customization to query parameters (@param), using the same [sqlgg] annotations from table definitions. Now custom modules are consistently applied to both input parameters and output columns.
| Form | Example |
|---|---|
col = @param |
WHERE id = @id |
col IN @param |
WHERE name IN @names |
(cols...) IN @param |
WHERE (id, type) IN @pairs |
Note: Column metadata propagation applies in any SQL clause (SELECT, WHERE, HAVING, JOIN, etc.), not just in WHERE. The customization works whenever a column reference appears in the expression, as metadata is propagated from the column definition to the parameter.
CREATE TABLE example (
-- [sqlgg] module=ExampleId
id INT AUTO_INCREMENT PRIMARY KEY,
-- [sqlgg] module=Name
name VARCHAR(255) NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE example2 (
id INT AUTO_INCREMENT PRIMARY KEY,
-- [sqlgg] module=Name2
name_2 VARCHAR(255) NOT NULL
);
-- @complex_query
SELECT example2.id, name_2
FROM example
JOIN example2 ON example.id = example2.id
WHERE name IN @name AND example2.name_2 IN @name_2 AND example.id = @id;let complex_query db ~name ~name_2 ~id callback =
let invoke_callback stmt =
callback
~id:(T.get_column_Int stmt 0)
~name_2:(Name2.get_column (T.get_column_string stmt 1))
in
let set_params stmt =
let p = T.start_params stmt (1 + (match name with [] -> 0 | _ :: _ -> 0) + (match name_2 with [] -> 0 | _ :: _ -> 0)) in
(* ... params handling ... *)
T.set_param_int64 p (ExampleId.set_param id);
T.finish_params p
in
T.select db
("SELECT example2.id, name_2 FROM example JOIN example2 ON example.id = example2.id WHERE ... ")
set_params invoke_callback
module ExampleId = struct
(* ... *)
(* Transforms domain type to SQL int64 type *)
let set_param (x : custom_id_type) : int64 =
(* Convert from domain type to SQL type *)
Int64.of_int (CustomId.to_int x)
end
module Name = struct
(* ... *)
(* Transforms domain type to SQL string type *)
let set_param (x : user_name_type) : string = Fun.id (* for example *)
endTo use this feature, you must ensure that the specified module implements:
-
For standard cases (when only
module=ModuleNameis specified):ModuleName.set_param- For converting domain types to SQL types
-
For custom function names (when both
module=ModuleNameandset_param=custom_nameare specified):ModuleName.custom_name- Custom function for parameter conversion
All together:
| Direction | Function | Type Signature | Example |
|---|---|---|---|
| SQL → Domain | get_column |
sql_type -> domain_type |
int64 -> user_id |
| Domain → SQL | set_param |
domain_type -> sql_type |
user_id -> int64 |
Note: Column-based customization for parameters in
INSERTandUPDATEstatements (e.g., for the right side ofSET column = @param) is not yet supported and will be enhanced in future releases.
→ PR #198
Added: May 2025
Provides custom mapping for column values in SELECT queries using simple -- [sqlgg] annotations in table definitions.
| Annotation | Description |
|---|---|
module=Module_name |
(Optional) Specifies a module to wrap column value access |
get_column=function_name |
(Optional) Specifies a custom function within the module |
Both annotations are optional:
- With no annotations: Default trait functions are used directly (
T.get_column_Int, etc.) - With only
module: UsesModule_name.get_columnorModule_name.get_column_nullable - With both: Uses the custom function specified (
Module_name.function_name)
CREATE TABLE table_37 (
-- [sqlgg] module=HelloWorld
col_1 INT PRIMARY KEY,
-- [sqlgg] module=Abcdefg
-- [sqlgg] get_column=test
col_2 INT NOT NULL
);
CREATE TABLE table_38 (
-- [sqlgg] module=FooBar
col_3 INT PRIMARY KEY,
col_4 TEXT NOT NULL
);And query
-- @deeply_nested_example
SELECT
(
SELECT MAX(x.col_val)
FROM (
SELECT col_1 as col_val,
1 + 3 as aaaaa,
1 + col_1 as bbb
FROM table_37
WHERE col_1 > (
SELECT MIN(col_1)
FROM table_37
)
) as x
) as deeply_nested_query,
col_2,
col_3,
col_4
FROM table_37
LEFT JOIN table_38
ON table_37.col_1 = table_38.col_3;callback
~deeply_nested_query:(HelloWorld.get_column_nullable (T.get_column_int64_nullable stmt 0))
~col_2:(Abcdefg.test (T.get_column_int64 stmt 1))
~col_3:(FooBar.get_column_nullable (T.get_column_int64_nullable stmt 2))
~col_4:(T.get_column_Text_nullable stmt 3)
col_1usesHelloWorld.get_column_nullableto wrap the standard gettercol_2usesAbcdefg.testas specified by the custom function namecol_3usesFooBar.get_column_nullablewith the default function namecol_4uses the defaultT.get_column_Text_nullable(no annotation)
| Case | Supported | Notes |
|---|---|---|
SELECT column results |
✅ | Custom column accessors are generated based on annotations |
@param in WHERE etc. |
❌ | Parameters currently use default T.set_param_* behavior |
INSERT / UPDATE |
❌ | Not yet supported — future extension planned |
Currently, customization applies only to SELECT results. Parameter handling (@param) and other DML operations(INSERT, UPDATE) still rely on default trait functions defined in the module Sqlgg (T : Sqlgg_traits.M) implementation.
To use this feature, you must ensure that the specified module implements:
-
For standard cases (when only
module=ModuleNameis specified):ModuleName.get_column- For non-nullable valuesModuleName.get_column_nullable- For nullable values
-
For custom function names (when both
module=ModuleNameandget_column=custom_nameare specified):ModuleName.custom_name- For non-nullable valuesModuleName.custom_name_nullable- For nullable values
The function signatures must match the corresponding SQL type. For example, for an INT column:
(* Standard function names *)
val get_column : int64 -> 'b
val get_column_nullable : int64 option -> 'b
(* Custom function names *)
val custom_name : int64 -> 'b
val custom_name_nullable : int64 option -> 'bWhere 'b is your desired domain-specific output type.
module HelloWorld = struct
(* For non-nullable INT values *)
let get_column (x : int64) : int =
Printf.printf "Processing value: %Ld\n" x;
Int64.to_int x
(* For nullable INT values *)
let get_column_nullable (x : int64 option) : int option =
match x with
| None ->
Printf.printf "Received NULL value\n";
None
| Some value ->
Printf.printf "Processing value: %Ld\n" value;
Some (Int64.to_int value)
end
module Abcdefg = struct
(* Custom function name used in annotation for INT column *)
let test (x : int64) : string =
Printf.printf "Custom test function: %Ld\n" x;
Int64.to_string x
endDefault trait function signatures (click to expand)
(* Core getter functions in trait module *)
val get_column_bool : row -> int -> bool
val get_column_bool_nullable : row -> int -> bool option
val get_column_int64 : row -> int -> int64
val get_column_int64_nullable : row -> int -> int64 option
val get_column_float : row -> int -> float
val get_column_float_nullable : row -> int -> float option
val get_column_decimal : row -> int -> float
val get_column_decimal_nullable : row -> int -> float option
val get_column_datetime : row -> int -> string
val get_column_datetime_nullable : row -> int -> string option
(* These functions are mapped to SQL types in the trait *)
val get_column_Bool : row -> int -> Bool.t
val get_column_Int : row -> int -> Int.t
val get_column_Text : row -> int -> Text.t
val get_column_Any : row -> int -> Any.t
val get_column_Float : row -> int -> Float.t
val get_column_Decimal : row -> int -> Decimal.t
val get_column_Datetime : row -> int -> Datetime.t
val get_column_Bool_nullable : row -> int -> Bool.t option
val get_column_Int_nullable : row -> int -> Int.t option
val get_column_Text_nullable : row -> int -> Text.t option
val get_column_Any_nullable : row -> int -> Any.t option
val get_column_Float_nullable : row -> int -> Float.t option
val get_column_Decimal_nullable : row -> int -> Decimal.t option
val get_column_Datetime_nullable : row -> int -> Datetime.t option→ PR #192
Added: April 2025
Use {expr}?? syntax to conditionally include values or fall back to SQL DEFAULT.
INSERT INTO `tbl`
SET `field_name` = { CONCAT(@param, 'string') }??- When
@paramis provided: The expression inside braces is used - When
@paramis empty: SQLDEFAULTkeyword is inserted
Multiple parameters are treated as tuples:
INSERT INTO `tbl`
SET `field_name` = { CONCAT(@param1, @param2) }??DEFAULT value defined in schema, otherwise SQL errors will occur.
option type.
→ PR #189
Added: April 2025
Create reusable query fragments with Common Table Expressions (CTE) using include: reuse and &name syntax.
| Annotation | Behavior |
|---|---|
include: reuse |
Query is reusable but no function is generated |
include: reuse-and-execute |
Query is both reusable and directly callable |
include: executable |
Normal behavior (default) |
-- @abcd | include: reuse
SELECT
1 as y,
4 + @five as y1
FROM (
SELECT 1 AS id, 'l' AS creator_name
UNION ALL
SELECT 2, 'k'
) AS x
WHERE @param { None { TRUE } | Some { FALSE } };
-- @test2
WITH x AS &abcd
SELECT 1 + @test - @test2 + @test5 + x.y1 as y2
FROM x;In this example:
@abcddefines a reusable query fragment&abcdembeds that query as a CTE namedx- Parameters are supported in reused queries
- Type inference works across both queries
→ PR #174
Added: March 2025
Provides type-safe handling of SQL ENUMs and string literal union types with inference and validation.
SELECT @param { A { 'A' } | B { 'B' } | C { 'C' } }This defines an enum-like union type: 'A' | 'B' | 'C'
| Type | Description |
|---|---|
| 🔒 Closed union | Fixed set of literal values (like enums) |
| 🔓 Open union | Built dynamically from literals or other unions |
With schema:
CREATE TABLE tbl (
status_enum ENUM('A', 'B', 'C') NOT NULL,
status_text TEXT NOT NULL
);- ✅ Enum values can be used as text (
Enum <: Text) - ❌ Text values cannot be treated as enums
-- ✅ Valid: assigning Enum to Text
INSERT INTO tbl (status_text)
VALUES (@param { A { 'A' } | B { 'B' } });
-- ✅ Valid: using enum in string operations
INSERT INTO tbl (status_text)
SELECT CONCAT(
@param { A { 'A' } | B { 'B' } },
'_suffix'
);
-- ✅ Valid: direct enum value
INSERT INTO tbl (status_enum) VALUES ('A');
-- ❌ Invalid: CONCAT() returns Text, not Enum
INSERT INTO tbl (status_enum)
SELECT CONCAT(@param { A { 'A' } | B { 'B' } }, '_suffix');When enabled:
- 🧪 Parameters for enum fields are type-checked
- 🧠 Corresponding enum types are inferred
- 🔁 For OCaml only, enums can map to polymorphic variants
CREATE TABLE some_table (
id INT PRIMARY KEY AUTO_INCREMENT,
status ENUM('pending', 'sending', 'sent', 'cancelled') NOT NULL,
status_b ENUM('a', 'b', 'c', 'd') NOT NULL
);
-- @name | include: executable
INSERT INTO some_table (
status,
status_b
) VALUES (
'pending',
@status_b
);Generated OCaml code with type-safe-enums enabled:
module Enum_status_b = T.Make_enum(struct
type t = [ `A | `B | `C | `D ]
let inj = function
| "a" -> `A | "b" -> `B | "c" -> `C | "d" -> `D
| s -> failwith ("Invalid enum value: " ^ s)
let proj = function
| `A -> "a" | `B -> "b" | `C -> "c" | `D -> "d"
end)
let insert_some_table db ~status_b =
let set_params stmt =
let p = T.start_params stmt 1 in
Enum_status_b.set_param p status_b;
T.finish_params p
in
T.execute db "INSERT INTO some_table (status, status_b) VALUES ('pending', ?)" set_params🔐 All SQL string values are type-checked against the field's declared enum in the database.
🧬 The
type-safe-enumsflag affects only OCaml code generation, where enum parameters can be mapped to polymorphic variants.
→ PR #152
Added: February 2025
Support for using VALUES as a table source in JOINs and other contexts.
- Inline values:
VALUES ROW(1, 'foo'), ROW(2, 'bar') - Parameterized values:
VALUES @param
-- Using inline ROWs
SELECT p.id, x.a
FROM products p
JOIN ( VALUES ROW(1, 'foo'), ROW(2, 'bar') ) AS x (a, b)
ON p.name = x.b;
-- Using parameters
SELECT p.id, x.a
FROM products p
JOIN ( VALUES @param ) AS x (a, b)
ON p.name = x.b;@param is an empty list, a fallback query is generated: SELECT %cols% WHERE FALSE
📘 Reference: MySQL 8.4 VALUES Syntax
→ PR #148
Added: January 2025
Use {expr}? syntax to conditionally include WHERE clauses based on optional parameters.
SELECT *
FROM test19
LEFT JOIN test20 ON test20.c = @test20a
WHERE { c = @choice2 }?
GROUP BY b;If parameter @choice2 is:
- Present/Non-empty → generates
WHERE c = ? - Empty/Missing → generates
WHERE TRUE
Note: The exact representation of "empty" vs "non-empty" depends on the target language. Currently only OCaml is supported, where this is implemented using the
optiontype. No polymorphic variant is introduced to avoid unnecessary wrapping.
| Syntax | When empty | Suitable for |
|---|---|---|
{expr}? |
Inserts TRUE |
WHERE clauses |
{expr}?? |
Inserts DEFAULT |
INSERT/UPDATE |
→ PR #142
Coming soon