+1
-1
driver-postgresql/repodb_postgresql.ml
+1
-1
driver-postgresql/repodb_postgresql.ml
···
26
26
27
27
let value_to_string (v : Repodb.Driver.Value.t) : string =
28
28
match v with
29
-
| Repodb.Driver.Value.Null -> ""
29
+
| Repodb.Driver.Value.Null -> Postgresql.null
30
30
| Repodb.Driver.Value.Int n -> string_of_int n
31
31
| Repodb.Driver.Value.Int64 n -> Int64.to_string n
32
32
| Repodb.Driver.Value.Float f -> string_of_float f
+182
driver-postgresql/test_integration.ml
+182
driver-postgresql/test_integration.ml
···
3432
3432
("concurrent_domains", `Slow, test_pool_concurrent_domains);
3433
3433
]
3434
3434
3435
+
let test_date_insert_and_query =
3436
+
with_db (fun conn ->
3437
+
let _ =
3438
+
Repodb_postgresql.exec conn "DROP TABLE IF EXISTS events" ~params:[||]
3439
+
in
3440
+
let _ =
3441
+
Repodb_postgresql.exec conn
3442
+
"CREATE TABLE events (id SERIAL PRIMARY KEY, name TEXT, event_date \
3443
+
DATE)"
3444
+
~params:[||]
3445
+
in
3446
+
let date : Ptime.date = (2024, 6, 15) in
3447
+
let date_value = Repodb.Types.to_value Repodb.Types.pdate date in
3448
+
let insert =
3449
+
Repodb_postgresql.exec conn
3450
+
"INSERT INTO events (name, event_date) VALUES ($1, $2)"
3451
+
~params:[| Repodb.Driver.Value.text "Conference"; date_value |]
3452
+
in
3453
+
(match insert with
3454
+
| Error e -> Alcotest.fail (Repodb_postgresql.error_message e)
3455
+
| Ok () -> ());
3456
+
match
3457
+
Repodb_postgresql.query conn
3458
+
"SELECT event_date FROM events WHERE name = $1"
3459
+
~params:[| Repodb.Driver.Value.text "Conference" |]
3460
+
with
3461
+
| Error e -> Alcotest.fail (Repodb_postgresql.error_message e)
3462
+
| Ok [] -> Alcotest.fail "expected row"
3463
+
| Ok (row :: _) -> (
3464
+
let raw_value = Repodb.Driver.row_get_idx row 0 in
3465
+
match Repodb.Types.of_value Repodb.Types.pdate raw_value with
3466
+
| Error e -> Alcotest.fail ("failed to decode date: " ^ e)
3467
+
| Ok (y, m, d) ->
3468
+
Alcotest.(check int) "year" 2024 y;
3469
+
Alcotest.(check int) "month" 6 m;
3470
+
Alcotest.(check int) "day" 15 d))
3471
+
3472
+
let test_date_comparison =
3473
+
with_db (fun conn ->
3474
+
let _ =
3475
+
Repodb_postgresql.exec conn "DROP TABLE IF EXISTS events" ~params:[||]
3476
+
in
3477
+
let _ =
3478
+
Repodb_postgresql.exec conn
3479
+
"CREATE TABLE events (id SERIAL PRIMARY KEY, name TEXT, event_date \
3480
+
DATE)"
3481
+
~params:[||]
3482
+
in
3483
+
let dates =
3484
+
[
3485
+
((2024, 1, 10), "Event A");
3486
+
((2024, 6, 15), "Event B");
3487
+
((2024, 12, 25), "Event C");
3488
+
]
3489
+
in
3490
+
List.iter
3491
+
(fun (date, name) ->
3492
+
let date_value = Repodb.Types.to_value Repodb.Types.pdate date in
3493
+
let _ =
3494
+
Repodb_postgresql.exec conn
3495
+
"INSERT INTO events (name, event_date) VALUES ($1, $2)"
3496
+
~params:[| Repodb.Driver.Value.text name; date_value |]
3497
+
in
3498
+
())
3499
+
dates;
3500
+
match
3501
+
Repodb_postgresql.query conn
3502
+
"SELECT name FROM events WHERE event_date > $1 ORDER BY event_date"
3503
+
~params:[| Repodb.Driver.Value.text "2024-06-01" |]
3504
+
with
3505
+
| Error e -> Alcotest.fail (Repodb_postgresql.error_message e)
3506
+
| Ok rows ->
3507
+
Alcotest.(check int) "two events after June 1" 2 (List.length rows);
3508
+
Alcotest.(check string)
3509
+
"first is Event B" "Event B"
3510
+
(Repodb.Driver.row_text (List.nth rows 0) 0);
3511
+
Alcotest.(check string)
3512
+
"second is Event C" "Event C"
3513
+
(Repodb.Driver.row_text (List.nth rows 1) 0))
3514
+
3515
+
let test_date_null_handling =
3516
+
with_db (fun conn ->
3517
+
let _ =
3518
+
Repodb_postgresql.exec conn "DROP TABLE IF EXISTS events" ~params:[||]
3519
+
in
3520
+
let _ =
3521
+
Repodb_postgresql.exec conn
3522
+
"CREATE TABLE events (id SERIAL PRIMARY KEY, name TEXT, event_date \
3523
+
DATE)"
3524
+
~params:[||]
3525
+
in
3526
+
let _ =
3527
+
Repodb_postgresql.exec conn
3528
+
"INSERT INTO events (name, event_date) VALUES ($1, $2)"
3529
+
~params:
3530
+
[| Repodb.Driver.Value.text "TBD Event"; Repodb.Driver.Value.null |]
3531
+
in
3532
+
match
3533
+
Repodb_postgresql.query_one conn
3534
+
"SELECT event_date FROM events WHERE name = $1"
3535
+
~params:[| Repodb.Driver.Value.text "TBD Event" |]
3536
+
with
3537
+
| Error e -> Alcotest.fail (Repodb_postgresql.error_message e)
3538
+
| Ok None -> Alcotest.fail "expected row"
3539
+
| Ok (Some row) -> (
3540
+
let raw_value = Repodb.Driver.row_get_idx row 0 in
3541
+
match
3542
+
Repodb.Types.of_value
3543
+
(Repodb.Types.option Repodb.Types.pdate)
3544
+
raw_value
3545
+
with
3546
+
| Error e -> Alcotest.fail ("failed to decode: " ^ e)
3547
+
| Ok None -> ()
3548
+
| Ok (Some _) -> Alcotest.fail "expected None for NULL date"))
3549
+
3550
+
let test_date_roundtrip =
3551
+
with_db (fun conn ->
3552
+
let _ =
3553
+
Repodb_postgresql.exec conn "DROP TABLE IF EXISTS events" ~params:[||]
3554
+
in
3555
+
let _ =
3556
+
Repodb_postgresql.exec conn
3557
+
"CREATE TABLE events (id SERIAL PRIMARY KEY, event_date DATE)"
3558
+
~params:[||]
3559
+
in
3560
+
let test_dates =
3561
+
[ (2024, 1, 1); (2024, 12, 31); (1999, 6, 15); (2030, 2, 28) ]
3562
+
in
3563
+
List.iter
3564
+
(fun date ->
3565
+
let date_value = Repodb.Types.to_value Repodb.Types.pdate date in
3566
+
let _ =
3567
+
Repodb_postgresql.exec conn
3568
+
"INSERT INTO events (event_date) VALUES ($1)"
3569
+
~params:[| date_value |]
3570
+
in
3571
+
())
3572
+
test_dates;
3573
+
match
3574
+
Repodb_postgresql.query conn "SELECT event_date FROM events ORDER BY id"
3575
+
~params:[||]
3576
+
with
3577
+
| Error e -> Alcotest.fail (Repodb_postgresql.error_message e)
3578
+
| Ok rows ->
3579
+
List.iter2
3580
+
(fun expected_date row ->
3581
+
let raw_value = Repodb.Driver.row_get_idx row 0 in
3582
+
match Repodb.Types.of_value Repodb.Types.pdate raw_value with
3583
+
| Error e -> Alcotest.fail ("decode failed: " ^ e)
3584
+
| Ok actual_date ->
3585
+
Alcotest.(check (triple int int int))
3586
+
"date roundtrip" expected_date actual_date)
3587
+
test_dates rows)
3588
+
3589
+
let test_date_current_date =
3590
+
with_db (fun conn ->
3591
+
match
3592
+
Repodb_postgresql.query_one conn "SELECT CURRENT_DATE" ~params:[||]
3593
+
with
3594
+
| Error e -> Alcotest.fail (Repodb_postgresql.error_message e)
3595
+
| Ok None -> Alcotest.fail "expected row"
3596
+
| Ok (Some row) -> (
3597
+
let raw_value = Repodb.Driver.row_get_idx row 0 in
3598
+
match Repodb.Types.of_value Repodb.Types.pdate raw_value with
3599
+
| Error e -> Alcotest.fail ("failed to decode CURRENT_DATE: " ^ e)
3600
+
| Ok (y, m, d) ->
3601
+
Alcotest.(check bool)
3602
+
"year reasonable" true
3603
+
(y >= 2024 && y <= 2100);
3604
+
Alcotest.(check bool) "month valid" true (m >= 1 && m <= 12);
3605
+
Alcotest.(check bool) "day valid" true (d >= 1 && d <= 31)))
3606
+
3607
+
let date_tests =
3608
+
[
3609
+
("insert_and_query", `Quick, test_date_insert_and_query);
3610
+
("comparison", `Quick, test_date_comparison);
3611
+
("null_handling", `Quick, test_date_null_handling);
3612
+
("roundtrip", `Quick, test_date_roundtrip);
3613
+
("current_date", `Quick, test_date_current_date);
3614
+
]
3615
+
3435
3616
let () =
3436
3617
Alcotest.run "repodb-postgresql"
3437
3618
[
···
3447
3628
("Multi", multi_tests);
3448
3629
("Query-Repo", query_repo_tests);
3449
3630
("Pool", pool_tests);
3631
+
("Date", date_tests);
3450
3632
]
+149
driver-sqlite/test_integration.ml
+149
driver-sqlite/test_integration.ml
···
3127
3127
("concurrent_domains", `Quick, test_pool_concurrent_domains);
3128
3128
]
3129
3129
3130
+
let test_date_insert_and_query =
3131
+
with_db (fun conn ->
3132
+
let _ =
3133
+
Repodb_sqlite.exec conn
3134
+
"CREATE TABLE events (id INTEGER PRIMARY KEY, name TEXT, event_date \
3135
+
TEXT)"
3136
+
~params:[||]
3137
+
in
3138
+
let date : Ptime.date = (2024, 6, 15) in
3139
+
let date_value = Repodb.Types.to_value Repodb.Types.pdate date in
3140
+
let insert =
3141
+
Repodb_sqlite.exec conn
3142
+
"INSERT INTO events (name, event_date) VALUES (?, ?)"
3143
+
~params:[| Repodb.Driver.Value.text "Conference"; date_value |]
3144
+
in
3145
+
(match insert with
3146
+
| Error e -> Alcotest.fail (Repodb_sqlite.error_message e)
3147
+
| Ok () -> ());
3148
+
match
3149
+
Repodb_sqlite.query conn "SELECT event_date FROM events WHERE name = ?"
3150
+
~params:[| Repodb.Driver.Value.text "Conference" |]
3151
+
with
3152
+
| Error e -> Alcotest.fail (Repodb_sqlite.error_message e)
3153
+
| Ok [] -> Alcotest.fail "expected row"
3154
+
| Ok (row :: _) -> (
3155
+
let raw_value = Repodb.Driver.row_get_idx row 0 in
3156
+
match Repodb.Types.of_value Repodb.Types.pdate raw_value with
3157
+
| Error e -> Alcotest.fail ("failed to decode date: " ^ e)
3158
+
| Ok (y, m, d) ->
3159
+
Alcotest.(check int) "year" 2024 y;
3160
+
Alcotest.(check int) "month" 6 m;
3161
+
Alcotest.(check int) "day" 15 d))
3162
+
3163
+
let test_date_comparison =
3164
+
with_db (fun conn ->
3165
+
let _ =
3166
+
Repodb_sqlite.exec conn
3167
+
"CREATE TABLE events (id INTEGER PRIMARY KEY, name TEXT, event_date \
3168
+
TEXT)"
3169
+
~params:[||]
3170
+
in
3171
+
let dates =
3172
+
[
3173
+
((2024, 1, 10), "Event A");
3174
+
((2024, 6, 15), "Event B");
3175
+
((2024, 12, 25), "Event C");
3176
+
]
3177
+
in
3178
+
List.iter
3179
+
(fun (date, name) ->
3180
+
let date_value = Repodb.Types.to_value Repodb.Types.pdate date in
3181
+
let _ =
3182
+
Repodb_sqlite.exec conn
3183
+
"INSERT INTO events (name, event_date) VALUES (?, ?)"
3184
+
~params:[| Repodb.Driver.Value.text name; date_value |]
3185
+
in
3186
+
())
3187
+
dates;
3188
+
match
3189
+
Repodb_sqlite.query conn
3190
+
"SELECT name FROM events WHERE event_date > ? ORDER BY event_date"
3191
+
~params:[| Repodb.Driver.Value.text "2024-06-01" |]
3192
+
with
3193
+
| Error e -> Alcotest.fail (Repodb_sqlite.error_message e)
3194
+
| Ok rows ->
3195
+
Alcotest.(check int) "two events after June 1" 2 (List.length rows);
3196
+
Alcotest.(check string)
3197
+
"first is Event B" "Event B"
3198
+
(Repodb.Driver.row_text (List.nth rows 0) 0);
3199
+
Alcotest.(check string)
3200
+
"second is Event C" "Event C"
3201
+
(Repodb.Driver.row_text (List.nth rows 1) 0))
3202
+
3203
+
let test_date_null_handling =
3204
+
with_db (fun conn ->
3205
+
let _ =
3206
+
Repodb_sqlite.exec conn
3207
+
"CREATE TABLE events (id INTEGER PRIMARY KEY, name TEXT, event_date \
3208
+
TEXT)"
3209
+
~params:[||]
3210
+
in
3211
+
let _ =
3212
+
Repodb_sqlite.exec conn
3213
+
"INSERT INTO events (name, event_date) VALUES (?, ?)"
3214
+
~params:
3215
+
[| Repodb.Driver.Value.text "TBD Event"; Repodb.Driver.Value.null |]
3216
+
in
3217
+
match
3218
+
Repodb_sqlite.query_one conn
3219
+
"SELECT event_date FROM events WHERE name = ?"
3220
+
~params:[| Repodb.Driver.Value.text "TBD Event" |]
3221
+
with
3222
+
| Error e -> Alcotest.fail (Repodb_sqlite.error_message e)
3223
+
| Ok None -> Alcotest.fail "expected row"
3224
+
| Ok (Some row) -> (
3225
+
let raw_value = Repodb.Driver.row_get_idx row 0 in
3226
+
match
3227
+
Repodb.Types.of_value
3228
+
(Repodb.Types.option Repodb.Types.pdate)
3229
+
raw_value
3230
+
with
3231
+
| Error e -> Alcotest.fail ("failed to decode: " ^ e)
3232
+
| Ok None -> ()
3233
+
| Ok (Some _) -> Alcotest.fail "expected None for NULL date"))
3234
+
3235
+
let test_date_roundtrip =
3236
+
with_db (fun conn ->
3237
+
let _ =
3238
+
Repodb_sqlite.exec conn
3239
+
"CREATE TABLE events (id INTEGER PRIMARY KEY, event_date TEXT)"
3240
+
~params:[||]
3241
+
in
3242
+
let test_dates =
3243
+
[ (2024, 1, 1); (2024, 12, 31); (1999, 6, 15); (2030, 2, 28) ]
3244
+
in
3245
+
List.iter
3246
+
(fun date ->
3247
+
let date_value = Repodb.Types.to_value Repodb.Types.pdate date in
3248
+
let _ =
3249
+
Repodb_sqlite.exec conn "INSERT INTO events (event_date) VALUES (?)"
3250
+
~params:[| date_value |]
3251
+
in
3252
+
())
3253
+
test_dates;
3254
+
match
3255
+
Repodb_sqlite.query conn "SELECT event_date FROM events ORDER BY id"
3256
+
~params:[||]
3257
+
with
3258
+
| Error e -> Alcotest.fail (Repodb_sqlite.error_message e)
3259
+
| Ok rows ->
3260
+
List.iter2
3261
+
(fun expected_date row ->
3262
+
let raw_value = Repodb.Driver.row_get_idx row 0 in
3263
+
match Repodb.Types.of_value Repodb.Types.pdate raw_value with
3264
+
| Error e -> Alcotest.fail ("decode failed: " ^ e)
3265
+
| Ok actual_date ->
3266
+
Alcotest.(check (triple int int int))
3267
+
"date roundtrip" expected_date actual_date)
3268
+
test_dates rows)
3269
+
3270
+
let date_tests =
3271
+
[
3272
+
("insert_and_query", `Quick, test_date_insert_and_query);
3273
+
("comparison", `Quick, test_date_comparison);
3274
+
("null_handling", `Quick, test_date_null_handling);
3275
+
("roundtrip", `Quick, test_date_roundtrip);
3276
+
]
3277
+
3130
3278
let () =
3131
3279
Alcotest.run "repodb-sqlite"
3132
3280
[
···
3141
3289
("Multi", multi_tests);
3142
3290
("Query-Repo", query_repo_tests);
3143
3291
("Pool", pool_tests);
3292
+
("Date", date_tests);
3144
3293
]
+93
test/test_types.ml
+93
test/test_types.ml
···
30
30
"ptime maps to TIMESTAMPTZ" "TIMESTAMPTZ"
31
31
(Types.sql_type_name Types.ptime)
32
32
33
+
let test_sql_type_name_pdate () =
34
+
Alcotest.(check string)
35
+
"pdate maps to DATE" "DATE"
36
+
(Types.sql_type_name Types.pdate)
37
+
38
+
let test_sql_type_name_pdate_sqlite () =
39
+
Alcotest.(check string)
40
+
"pdate maps to TEXT for SQLite" "TEXT"
41
+
(Types.sql_type_name_for_sqlite Types.pdate)
42
+
33
43
let test_sql_type_name_uuid () =
34
44
Alcotest.(check string)
35
45
"uuid maps to UUID" "UUID"
···
71
81
"custom type name" "CUSTOM_TYPE"
72
82
(Types.sql_type_name custom)
73
83
84
+
let test_pdate_to_value () =
85
+
let date : Ptime.date = (2024, 1, 15) in
86
+
let value = Types.to_value Types.pdate date in
87
+
match value with
88
+
| Driver.Value.Text s ->
89
+
Alcotest.(check string) "date serializes to YYYY-MM-DD" "2024-01-15" s
90
+
| _ -> Alcotest.fail "expected Text value"
91
+
92
+
let test_pdate_to_value_single_digit_month_day () =
93
+
let date : Ptime.date = (2024, 3, 5) in
94
+
let value = Types.to_value Types.pdate date in
95
+
match value with
96
+
| Driver.Value.Text s ->
97
+
Alcotest.(check string) "single digit month/day padded" "2024-03-05" s
98
+
| _ -> Alcotest.fail "expected Text value"
99
+
100
+
let test_pdate_of_value () =
101
+
let value = Driver.Value.Text "2024-01-15" in
102
+
match Types.of_value Types.pdate value with
103
+
| Ok (y, m, d) ->
104
+
Alcotest.(check int) "year" 2024 y;
105
+
Alcotest.(check int) "month" 1 m;
106
+
Alcotest.(check int) "day" 15 d
107
+
| Error e -> Alcotest.fail ("failed to parse date: " ^ e)
108
+
109
+
let test_pdate_of_value_unpadded () =
110
+
let value = Driver.Value.Text "2024-3-5" in
111
+
match Types.of_value Types.pdate value with
112
+
| Ok (y, m, d) ->
113
+
Alcotest.(check int) "year" 2024 y;
114
+
Alcotest.(check int) "month" 3 m;
115
+
Alcotest.(check int) "day" 5 d
116
+
| Error e -> Alcotest.fail ("failed to parse date: " ^ e)
117
+
118
+
let test_pdate_roundtrip () =
119
+
let original : Ptime.date = (2023, 12, 31) in
120
+
let value = Types.to_value Types.pdate original in
121
+
match Types.of_value Types.pdate value with
122
+
| Ok result ->
123
+
Alcotest.(check (triple int int int))
124
+
"roundtrip preserves date" original result
125
+
| Error e -> Alcotest.fail ("roundtrip failed: " ^ e)
126
+
127
+
let test_pdate_of_value_invalid () =
128
+
let value = Driver.Value.Text "not-a-date" in
129
+
match Types.of_value Types.pdate value with
130
+
| Ok _ -> Alcotest.fail "should have failed on invalid date"
131
+
| Error _ -> ()
132
+
133
+
let test_pdate_of_value_null () =
134
+
let value = Driver.Value.Null in
135
+
match Types.of_value Types.pdate value with
136
+
| Ok _ -> Alcotest.fail "should have failed on NULL"
137
+
| Error _ -> ()
138
+
139
+
let test_pdate_option_null () =
140
+
let value = Driver.Value.Null in
141
+
match Types.of_value (Types.option Types.pdate) value with
142
+
| Ok None -> ()
143
+
| Ok (Some _) -> Alcotest.fail "expected None for NULL"
144
+
| Error e -> Alcotest.fail ("unexpected error: " ^ e)
145
+
146
+
let test_pdate_option_some () =
147
+
let value = Driver.Value.Text "2024-06-15" in
148
+
match Types.of_value (Types.option Types.pdate) value with
149
+
| Ok (Some (y, m, d)) ->
150
+
Alcotest.(check int) "year" 2024 y;
151
+
Alcotest.(check int) "month" 6 m;
152
+
Alcotest.(check int) "day" 15 d
153
+
| Ok None -> Alcotest.fail "expected Some date"
154
+
| Error e -> Alcotest.fail ("unexpected error: " ^ e)
155
+
74
156
let tests =
75
157
[
76
158
("sql_type_name int", `Quick, test_sql_type_name_int);
···
79
161
("sql_type_name bool", `Quick, test_sql_type_name_bool);
80
162
("sql_type_name float", `Quick, test_sql_type_name_float);
81
163
("sql_type_name ptime", `Quick, test_sql_type_name_ptime);
164
+
("sql_type_name pdate", `Quick, test_sql_type_name_pdate);
165
+
("sql_type_name pdate sqlite", `Quick, test_sql_type_name_pdate_sqlite);
82
166
("sql_type_name uuid", `Quick, test_sql_type_name_uuid);
83
167
("sql_type_name json", `Quick, test_sql_type_name_json);
84
168
("sql_type_name option", `Quick, test_sql_type_name_option);
···
86
170
("is_nullable option", `Quick, test_is_nullable_option);
87
171
("is_nullable non-option", `Quick, test_is_nullable_non_option);
88
172
("custom type", `Quick, test_custom_type);
173
+
("pdate to_value", `Quick, test_pdate_to_value);
174
+
("pdate to_value padded", `Quick, test_pdate_to_value_single_digit_month_day);
175
+
("pdate of_value", `Quick, test_pdate_of_value);
176
+
("pdate of_value unpadded", `Quick, test_pdate_of_value_unpadded);
177
+
("pdate roundtrip", `Quick, test_pdate_roundtrip);
178
+
("pdate of_value invalid", `Quick, test_pdate_of_value_invalid);
179
+
("pdate of_value null", `Quick, test_pdate_of_value_null);
180
+
("pdate option null", `Quick, test_pdate_option_null);
181
+
("pdate option some", `Quick, test_pdate_option_some);
89
182
]