cwist
Modern C web framework with libttak runtime
Loading...
Searching...
No Matches
orm.c
Go to the documentation of this file.
1
10#ifndef _GNU_SOURCE
11#define _GNU_SOURCE
12#endif
13#include <cwist/core/orm/orm.h>
15
16#include <stdio.h>
17#include <stdlib.h>
18#include <string.h>
19#include <strings.h>
20#include <unistd.h>
21#include <errno.h>
22#include <sys/socket.h>
23#include <stdint.h>
24#include <arpa/inet.h>
25
26/* ------------------------------------------------------------------------- */
27/* Internal ORM handle */
28/* ------------------------------------------------------------------------- */
29
33struct cwist_orm {
34 int sock_fd;
35 bool auto_commit;
36 cwist_orm_dialect_t dialect;
37};
38
40static volatile bool g_immediate_commit = false;
41
42/* ------------------------------------------------------------------------- */
43/* Framing helpers */
44/* ------------------------------------------------------------------------- */
45
50static int orm_recv_all(int fd, void *buf, size_t n) {
51 size_t total = 0;
52 char *p = (char *)buf;
53 while (total < n) {
54 ssize_t r = recv(fd, p + total, n - total, MSG_WAITALL);
55 if (r <= 0) return -1;
56 total += (size_t)r;
57 }
58 return 0;
59}
60
65static int orm_send_all(int fd, const void *buf, size_t n) {
66 size_t total = 0;
67 const char *p = (const char *)buf;
68 while (total < n) {
69 ssize_t w = send(fd, p + total, n - total, 0);
70 if (w <= 0) return -1;
71 total += (size_t)w;
72 }
73 return 0;
74}
75
88static int orm_exchange(cwist_orm_t *orm, const char *sql, char **out_payload) {
90 if (!orm || orm->sock_fd < 0 || !sql) return -1;
91
92 size_t sql_len = strlen(sql);
93 if (sql_len > UINT32_MAX - 1) return -1;
94
96 if (orm_send_all(orm->sock_fd, &net_len, sizeof(net_len)) != 0) return -1;
97 if (orm_send_all(orm->sock_fd, sql, sql_len) != 0) return -1;
98
100 if (orm_recv_all(orm->sock_fd, &net_payload, sizeof(net_payload)) != 0) return -1;
101 uint32_t payload_len = ntohl(net_payload);
102 if (payload_len > 64 * 1024 * 1024) return -1; /* sanity ceiling */
103
104 char *payload = (char *)malloc(payload_len + 1);
105 if (!payload) return -1;
106 if (payload_len > 0 && orm_recv_all(orm->sock_fd, payload, payload_len) != 0) {
107 free(payload);
108 return -1;
109 }
110 payload[payload_len] = '\0';
111 *out_payload = payload;
112 return 0;
113}
114
121static int orm_parse_status(const char *payload) {
122 if (!payload) return -1;
123 cJSON *root = cJSON_Parse(payload);
124 if (!root) return -1;
125 cJSON *status = cJSON_GetObjectItemCaseSensitive(root, "status");
126 int rc = (cJSON_IsNumber(status)) ? (int)status->valuedouble : -1;
128 return rc;
129}
130
131/* ------------------------------------------------------------------------- */
132/* Lifecycle */
133/* ------------------------------------------------------------------------- */
134
146 if (socket_fd < 0) return NULL;
147 cwist_orm_t *orm = (cwist_orm_t *)calloc(1, sizeof(*orm));
148 if (!orm) return NULL;
149 orm->sock_fd = socket_fd;
150 orm->auto_commit = g_immediate_commit;
151 orm->dialect = CWIST_ORM_SQLITE;
152 return orm;
153}
154
164 if (!orm) return;
165 if (orm->sock_fd >= 0) {
166 /* Best-effort rollback of any dangling transaction. */
167 char *dummy = NULL;
168 (void)orm_exchange(orm, "ROLLBACK;", &dummy);
169 free(dummy);
170 close(orm->sock_fd);
171 }
172 free(orm);
173}
174
175/* ------------------------------------------------------------------------- */
176/* Transaction control */
177/* ------------------------------------------------------------------------- */
178
189 g_immediate_commit = enable;
190}
191
202 if (orm) orm->dialect = dialect;
203}
204
214
224
225/* ------------------------------------------------------------------------- */
226/* Low-level SQL */
227/* ------------------------------------------------------------------------- */
228
243 if (!orm || !sql) {
244 err.error.err_i16 = CWIST_ERROR_INVALID_PARAM;
245 return err;
246 }
247
248 char *payload = NULL;
249 if (orm_exchange(orm, sql, &payload) != 0) {
250 err.error.err_i16 = CWIST_ERROR_IO;
251 return err;
252 }
253
254 int status = orm_parse_status(payload);
255 free(payload);
256
257 if (status != 0) {
258 err.error.err_i16 = CWIST_ERROR_PROTOCOL;
259 return err;
260 }
261
262 /* Auto-commit write helpers if requested. */
263 if (g_immediate_commit) {
264 const char *upper = sql;
265 while (*upper && *upper <= ' ') upper++;
266 if ((strncasecmp(upper, "INSERT", 6) == 0) || (strncasecmp(upper, "UPDATE", 6) == 0) ||
267 (strncasecmp(upper, "DELETE", 6) == 0)) {
268 char *commit_payload = NULL;
269 (void)orm_exchange(orm, "COMMIT;", &commit_payload);
271 }
272 }
273
274 err.error.err_i16 = CWIST_SUCCESS;
275 return err;
276}
277
293 if (result) *result = NULL;
294 if (!orm || !sql || !result) {
295 err.error.err_i16 = CWIST_ERROR_INVALID_PARAM;
296 return err;
297 }
298
299 char *payload = NULL;
300 if (orm_exchange(orm, sql, &payload) != 0) {
301 err.error.err_i16 = CWIST_ERROR_IO;
302 return err;
303 }
304
305 cJSON *root = cJSON_Parse(payload);
306 free(payload);
307 if (!root) {
308 err.error.err_i16 = CWIST_ERROR_PROTOCOL;
309 return err;
310 }
311
312 cJSON *status = cJSON_GetObjectItemCaseSensitive(root, "status");
313 if (!cJSON_IsNumber(status) || (int)status->valuedouble != 0) {
315 err.error.err_i16 = CWIST_ERROR_PROTOCOL;
316 return err;
317 }
318
321 if (!rows) {
322 /* Guarantee an array even on empty result. */
323 rows = cJSON_CreateArray();
324 }
325
326 *result = rows;
327 err.error.err_i16 = CWIST_SUCCESS;
328 return err;
329}
330
331/* ------------------------------------------------------------------------- */
332/* SQL value formatter */
333/* ------------------------------------------------------------------------- */
334
343static char *cwist_orm_escape_sqlite(const char *src) {
344 if (!src) return NULL;
345 size_t len = strlen(src);
346 size_t extra = 0;
347 for (size_t i = 0; i < len; i++) {
348 if (src[i] == '\'') extra++;
349 }
350 char *dst = (char *)malloc(len + extra + 1);
351 if (!dst) return NULL;
352 size_t j = 0;
353 for (size_t i = 0; i < len; i++) {
354 if (src[i] == '\'') dst[j++] = '\'';
355 dst[j++] = src[i];
356 }
357 dst[j] = '\0';
358 return dst;
359}
360
370static char *cwist_orm_quote_identifier(const cwist_orm_t *orm, const char *id) {
371 if (!orm || !id) return NULL;
372 char quote_ch = '\0';
373 char escape_ch = '\0';
374 switch (orm->dialect) {
376 quote_ch = '"';
377 escape_ch = '"';
378 break;
379 case CWIST_ORM_MYSQL:
381 case CWIST_ORM_SQLITE:
382 quote_ch = '`';
383 escape_ch = '`';
384 break;
385 default: return strdup(id);
386 }
387
388 size_t len = strlen(id);
389 size_t extra = 0;
390 for (size_t i = 0; i < len; i++) {
391 if (id[i] == quote_ch) extra++;
392 }
393 char *out = (char *)malloc(len + extra + 3);
394 if (!out) return NULL;
395 size_t j = 0;
396 out[j++] = quote_ch;
397 for (size_t i = 0; i < len; i++) {
398 if (id[i] == quote_ch) out[j++] = escape_ch;
399 out[j++] = id[i];
400 }
401 out[j++] = quote_ch;
402 out[j] = '\0';
403 return out;
404}
405
418static char *cwist_orm_json_to_sql_literal(const cwist_orm_t *orm, const cJSON *node) {
419 if (!node) return strdup("NULL");
420
421 if (cJSON_IsNull(node)) {
422 return strdup("NULL");
423 }
424 if (cJSON_IsBool(node)) {
425 if (orm && orm->dialect == CWIST_ORM_POSTGRES) {
426 return strdup(cJSON_IsTrue(node) ? "TRUE" : "FALSE");
427 }
428 return strdup(cJSON_IsTrue(node) ? "1" : "0");
429 }
430 if (cJSON_IsNumber(node)) {
431 char buf[64];
432 snprintf(buf, sizeof(buf), "%g", node->valuedouble);
433 return strdup(buf);
434 }
435 if (cJSON_IsString(node)) {
436 if (!node->valuestring) return strdup("''");
437 char *escaped = cwist_orm_escape_sqlite(node->valuestring);
438 if (!escaped) return NULL;
439 size_t elen = strlen(escaped);
440 char *quoted = (char *)malloc(elen + 3);
441 if (!quoted) {
442 free(escaped);
443 return NULL;
444 }
445 quoted[0] = '\'';
446 memcpy(quoted + 1, escaped, elen);
447 quoted[elen + 1] = '\'';
448 quoted[elen + 2] = '\0';
449 free(escaped);
450 return quoted;
451 }
452
453 /* Fallback: stringify arrays/objects as JSON text. */
454 char *raw = cJSON_PrintUnformatted((cJSON *)node);
455 if (!raw) return strdup("NULL");
456 char *escaped = cwist_orm_escape_sqlite(raw);
457 free(raw);
458 if (!escaped) return NULL;
459 size_t elen = strlen(escaped);
460 char *quoted = (char *)malloc(elen + 3);
461 if (!quoted) {
462 free(escaped);
463 return NULL;
464 }
465 quoted[0] = '\'';
466 memcpy(quoted + 1, escaped, elen);
467 quoted[elen + 1] = '\'';
468 quoted[elen + 2] = '\0';
469 free(escaped);
470 return quoted;
471}
472
473/* ------------------------------------------------------------------------- */
474/* High-level helpers */
475/* ------------------------------------------------------------------------- */
476
485static char *orm_build_insert_sql(const cwist_orm_t *orm, const char *table, const cJSON *data) {
486 if (!table || !data || !cJSON_IsObject(data)) return NULL;
487
488 int col_count = 0;
489 cJSON *item = NULL;
491 if (col_count == 0) return NULL;
492
493 char *columns = (char *)malloc(1);
494 char *values = (char *)malloc(1);
495 if (!columns || !values) {
496 free(columns);
497 free(values);
498 return NULL;
499 }
500 columns[0] = '\0';
501 values[0] = '\0';
502
503 char *qtable = cwist_orm_quote_identifier(orm, table);
504 if (!qtable) {
505 free(columns);
506 free(values);
507 return NULL;
508 }
509
510 int idx = 0;
511 cJSON_ArrayForEach(item, (cJSON *)data) {
512 char *literal = cwist_orm_json_to_sql_literal(orm, item);
513 char *qid = cwist_orm_quote_identifier(orm, item->string);
514 if (!literal || !qid) {
515 free(literal);
516 free(qid);
517 free(qtable);
518 free(columns);
519 free(values);
520 return NULL;
521 }
522
523 size_t need_col = strlen(columns) + strlen(qid) + 2;
524 char *new_col = (char *)realloc(columns, need_col);
525 if (!new_col) {
526 free(literal);
527 free(qid);
528 free(qtable);
529 free(columns);
530 free(values);
531 return NULL;
532 }
533 columns = new_col;
534
535 size_t need_val = strlen(values) + strlen(literal) + 2;
536 char *new_val = (char *)realloc(values, need_val);
537 if (!new_val) {
538 free(literal);
539 free(qid);
540 free(qtable);
541 free(columns);
542 free(values);
543 return NULL;
544 }
545 values = new_val;
546
547 if (idx > 0) {
548 strcat(columns, ",");
549 strcat(values, ",");
550 }
551 strcat(columns, qid);
553 free(literal);
554 free(qid);
555 idx++;
556 }
557
558 size_t sql_len = strlen(qtable) + strlen(columns) + strlen(values) + 32;
559 char *sql = (char *)malloc(sql_len);
560 if (!sql) {
561 free(qtable);
562 free(columns);
563 free(values);
564 return NULL;
565 }
566 snprintf(sql, sql_len, "INSERT INTO %s (%s) VALUES (%s)", qtable, columns, values);
567 free(qtable);
568 free(columns);
569 free(values);
570 return sql;
571}
572
588 if (!orm || !table || !data || !cJSON_IsObject(data)) {
589 err.error.err_i16 = CWIST_ERROR_INVALID_PARAM;
590 return err;
591 }
592
593 char *sql = orm_build_insert_sql(orm, table, data);
594 if (!sql) {
595 err.error.err_i16 = CWIST_ERROR_NOMEM;
596 return err;
597 }
598
599 size_t len = strlen(sql);
600 char *final = (char *)realloc(sql, len + 2);
601 if (!final) {
602 free(sql);
603 err.error.err_i16 = CWIST_ERROR_NOMEM;
604 return err;
605 }
606 final[len] = ';';
607 final[len + 1] = '\0';
608
609 err = cwist_orm_exec(orm, final);
610 free(final);
611 return err;
612}
613
630 const char *where_clause) {
632 if (!orm || !table || !data || !cJSON_IsObject(data)) {
633 err.error.err_i16 = CWIST_ERROR_INVALID_PARAM;
634 return err;
635 }
636
637 char *set_clause = (char *)malloc(1);
638 if (!set_clause) {
639 err.error.err_i16 = CWIST_ERROR_NOMEM;
640 return err;
641 }
642 set_clause[0] = '\0';
643
644 char *qtable = cwist_orm_quote_identifier(orm, table);
645 if (!qtable) {
647 err.error.err_i16 = CWIST_ERROR_NOMEM;
648 return err;
649 }
650
651 int idx = 0;
652 cJSON *item = NULL;
653 cJSON_ArrayForEach(item, (cJSON *)data) {
654 char *literal = cwist_orm_json_to_sql_literal(orm, item);
655 char *qid = cwist_orm_quote_identifier(orm, item->string);
656 if (!literal || !qid) {
657 free(literal);
658 free(qid);
659 free(qtable);
661 err.error.err_i16 = CWIST_ERROR_NOMEM;
662 return err;
663 }
664 size_t need = strlen(set_clause) + strlen(qid) + strlen(literal) + 4;
665 char *new_set = (char *)realloc(set_clause, need);
666 if (!new_set) {
667 free(literal);
668 free(qid);
669 free(qtable);
671 err.error.err_i16 = CWIST_ERROR_NOMEM;
672 return err;
673 }
675 if (idx > 0) strcat(set_clause, ",");
677 strcat(set_clause, "=");
679 free(literal);
680 free(qid);
681 idx++;
682 }
683
684 if (idx == 0) {
685 free(qtable);
687 err.error.err_i16 = CWIST_ERROR_INVALID_PARAM;
688 return err;
689 }
690
691 size_t sql_len =
693 char *sql = (char *)malloc(sql_len);
694 if (!sql) {
695 free(qtable);
697 err.error.err_i16 = CWIST_ERROR_NOMEM;
698 return err;
699 }
700
701 if (where_clause && *where_clause) {
702 snprintf(sql, sql_len, "UPDATE %s SET %s WHERE %s;", qtable, set_clause, where_clause);
703 } else {
704 snprintf(sql, sql_len, "UPDATE %s SET %s;", qtable, set_clause);
705 }
706 free(qtable);
708
710 free(sql);
711 return err;
712}
713
729 if (!orm || !table) {
730 err.error.err_i16 = CWIST_ERROR_INVALID_PARAM;
731 return err;
732 }
733
734 char *qtable = cwist_orm_quote_identifier(orm, table);
735 if (!qtable) {
736 err.error.err_i16 = CWIST_ERROR_NOMEM;
737 return err;
738 }
739
740 size_t sql_len = strlen(qtable) + 32 + (where_clause ? strlen(where_clause) : 0);
741 char *sql = (char *)malloc(sql_len);
742 if (!sql) {
743 free(qtable);
744 err.error.err_i16 = CWIST_ERROR_NOMEM;
745 return err;
746 }
747
748 if (where_clause && *where_clause) {
749 snprintf(sql, sql_len, "DELETE FROM %s WHERE %s;", qtable, where_clause);
750 } else {
751 snprintf(sql, sql_len, "DELETE FROM %s;", qtable);
752 }
753 free(qtable);
754
756 free(sql);
757 return err;
758}
759
773cwist_error_t cwist_orm_select(cwist_orm_t *orm, const char *table, const char *columns,
774 const char *where_clause, cJSON **result) {
776 *result = NULL;
777 if (!orm || !table || !columns) {
778 err.error.err_i16 = CWIST_ERROR_INVALID_PARAM;
779 return err;
780 }
781
782 char *qtable = cwist_orm_quote_identifier(orm, table);
783 if (!qtable) {
784 err.error.err_i16 = CWIST_ERROR_NOMEM;
785 return err;
786 }
787
788 size_t sql_len =
789 strlen(columns) + strlen(qtable) + 32 + (where_clause ? strlen(where_clause) : 0);
790 char *sql = (char *)malloc(sql_len);
791 if (!sql) {
792 free(qtable);
793 err.error.err_i16 = CWIST_ERROR_NOMEM;
794 return err;
795 }
796
797 if (where_clause && *where_clause) {
798 snprintf(sql, sql_len, "SELECT %s FROM %s WHERE %s;", columns, qtable, where_clause);
799 } else {
800 snprintf(sql, sql_len, "SELECT %s FROM %s;", columns, qtable);
801 }
802 free(qtable);
803
804 err = cwist_orm_query(orm, sql, result);
805 free(sql);
806 return err;
807}
808
809/* ------------------------------------------------------------------ */
810/* _Generic helpers: RETURNING INSERT */
811/* ------------------------------------------------------------------ */
812
818static char *orm_build_returning_sql(const cwist_orm_t *orm, const char *table, const cJSON *data,
819 const char *returning_col) {
820 char *base = orm_build_insert_sql(orm, table, data);
821 if (!base) return NULL;
822
823 char *qret = cwist_orm_quote_identifier(orm, returning_col);
824 if (!qret) {
825 free(base);
826 return NULL;
827 }
828
829 const char *fmt;
830 switch (orm->dialect) {
832 case CWIST_ORM_MARIADB: fmt = "%s RETURNING %s;"; break;
833 case CWIST_ORM_MYSQL: fmt = "%s; SELECT LAST_INSERT_ID() AS %s;"; break;
834 case CWIST_ORM_SQLITE:
835 default: fmt = "%s; SELECT last_insert_rowid() AS %s;"; break;
836 }
837 /* Size from the format itself: a fixed +16 truncated the SQLite and
838 * MySQL suffixes, so the INSERT ran but the SELECT never did. Raw
839 * malloc like every SQL buffer in this file (the caller frees it). */
840 int need = snprintf(NULL, 0, fmt, base, qret);
841 char *sql = need >= 0 ? (char *)malloc((size_t)need + 1) : NULL;
842 if (!sql) {
843 free(base);
844 free(qret);
845 return NULL;
846 }
847 snprintf(sql, (size_t)need + 1, fmt, base, qret);
848 free(base);
849 free(qret);
850 return sql;
851}
852
873 const cJSON *data, const char *returning_col,
874 cJSON **out) {
876 if (out) *out = NULL;
877 if (!orm || !table || !data || !returning_col || !out) {
878 err.error.err_i16 = CWIST_ERROR_INVALID_PARAM;
879 return err;
880 }
881
882 char *sql = orm_build_returning_sql(orm, table, data, returning_col);
883 if (!sql) {
884 err.error.err_i16 = CWIST_ERROR_NOMEM;
885 return err;
886 }
887
888 cJSON *rows = NULL;
889 err = cwist_orm_query(orm, sql, &rows);
890 free(sql);
891 if (err.error.err_i16 != 0) {
892 if (rows) cJSON_Delete(rows);
893 return err;
894 }
895
896 cJSON *first = cJSON_GetArrayItem(rows, 0);
897 if (!first) {
898 cJSON_Delete(rows);
899 err.error.err_i16 = CWIST_ERROR_PROTOCOL;
900 return err;
901 }
902
904 if (!val) {
905 cJSON_Delete(rows);
906 err.error.err_i16 = CWIST_ERROR_PROTOCOL;
907 return err;
908 }
909
910 *out = cJSON_Duplicate(val, 1);
911 cJSON_Delete(rows);
912 if (!*out) {
913 err.error.err_i16 = CWIST_ERROR_NOMEM;
914 return err;
915 }
916 err.error.err_i16 = 0;
917 return err;
918}
919
933 const char *returning_col, int *out) {
934 cJSON *val = NULL;
936 if (err.error.err_i16 != 0) return err;
937 if (cJSON_IsNumber(val)) {
938 *out = (int)val->valuedouble;
939 } else {
940 err.error.err_i16 = CWIST_ERROR_PROTOCOL;
941 }
943 return err;
944}
945
959 const cJSON *data, const char *returning_col,
960 long *out) {
961 cJSON *val = NULL;
963 if (err.error.err_i16 != 0) return err;
964 if (cJSON_IsNumber(val)) {
965 *out = (long)val->valuedouble;
966 } else {
967 err.error.err_i16 = CWIST_ERROR_PROTOCOL;
968 }
970 return err;
971}
972
986 const cJSON *data, const char *returning_col,
987 long long *out) {
988 cJSON *val = NULL;
990 if (err.error.err_i16 != 0) return err;
991 if (cJSON_IsNumber(val)) {
992 *out = (long long)val->valuedouble;
993 } else {
994 err.error.err_i16 = CWIST_ERROR_PROTOCOL;
995 }
997 return err;
998}
999
1000/* ------------------------------------------------------------------ */
1001/* _Generic helpers: SELECT single scalar */
1002/* ------------------------------------------------------------------ */
1003
1009static char *orm_build_select_one_sql(const cwist_orm_t *orm, const char *table,
1010 const char *column, const char *where_clause) {
1011 char *qtable = cwist_orm_quote_identifier(orm, table);
1012 char *qcol = cwist_orm_quote_identifier(orm, column);
1013 if (!qtable || !qcol) {
1014 free(qtable);
1015 free(qcol);
1016 return NULL;
1017 }
1018
1019 size_t sql_len = strlen(qcol) + strlen(qtable) + 48 + (where_clause ? strlen(where_clause) : 0);
1020 char *sql = (char *)malloc(sql_len);
1021 if (!sql) {
1022 free(qtable);
1023 free(qcol);
1024 return NULL;
1025 }
1026
1027 if (where_clause && *where_clause) {
1028 snprintf(sql, sql_len, "SELECT %s FROM %s WHERE %s LIMIT 1;", qcol, qtable, where_clause);
1029 } else {
1030 snprintf(sql, sql_len, "SELECT %s FROM %s LIMIT 1;", qcol, qtable);
1031 }
1032 free(qtable);
1033 free(qcol);
1034 return sql;
1035}
1036
1054 const char *where_clause, cJSON **out) {
1056 if (out) *out = NULL;
1057 if (!orm || !table || !column || !out) {
1058 err.error.err_i16 = CWIST_ERROR_INVALID_PARAM;
1059 return err;
1060 }
1061
1062 char *sql = orm_build_select_one_sql(orm, table, column, where_clause);
1063 if (!sql) {
1064 err.error.err_i16 = CWIST_ERROR_NOMEM;
1065 return err;
1066 }
1067
1068 cJSON *rows = NULL;
1069 err = cwist_orm_query(orm, sql, &rows);
1070 free(sql);
1071 if (err.error.err_i16 != 0) {
1072 if (rows) cJSON_Delete(rows);
1073 return err;
1074 }
1075
1076 cJSON *first = cJSON_GetArrayItem(rows, 0);
1077 if (!first) {
1078 cJSON_Delete(rows);
1079 err.error.err_i16 = CWIST_ERROR_PROTOCOL;
1080 return err;
1081 }
1082
1084 if (!val) {
1085 cJSON_Delete(rows);
1086 err.error.err_i16 = CWIST_ERROR_PROTOCOL;
1087 return err;
1088 }
1089
1090 *out = cJSON_Duplicate(val, 1);
1091 cJSON_Delete(rows);
1092 if (!*out) {
1093 err.error.err_i16 = CWIST_ERROR_NOMEM;
1094 return err;
1095 }
1096 err.error.err_i16 = 0;
1097 return err;
1098}
1099
1113 const char *where_clause, int *out) {
1114 cJSON *val = NULL;
1116 if (err.error.err_i16 != 0) return err;
1117 if (cJSON_IsNumber(val)) {
1118 *out = (int)val->valuedouble;
1119 } else {
1120 err.error.err_i16 = CWIST_ERROR_PROTOCOL;
1121 }
1123 return err;
1124}
1125
1139 const char *where_clause, long *out) {
1140 cJSON *val = NULL;
1142 if (err.error.err_i16 != 0) return err;
1143 if (cJSON_IsNumber(val)) {
1144 *out = (long)val->valuedouble;
1145 } else {
1146 err.error.err_i16 = CWIST_ERROR_PROTOCOL;
1147 }
1149 return err;
1150}
1151
1165 const char *where_clause, long long *out) {
1166 cJSON *val = NULL;
1168 if (err.error.err_i16 != 0) return err;
1169 if (cJSON_IsNumber(val)) {
1170 *out = (long long)val->valuedouble;
1171 } else {
1172 err.error.err_i16 = CWIST_ERROR_PROTOCOL;
1173 }
1175 return err;
1176}
1177
1191 const char *where_clause, double *out) {
1192 cJSON *val = NULL;
1194 if (err.error.err_i16 != 0) return err;
1195 if (cJSON_IsNumber(val)) {
1196 *out = val->valuedouble;
1197 } else {
1198 err.error.err_i16 = CWIST_ERROR_PROTOCOL;
1199 }
1201 return err;
1202}
1203
1219 const char *where_clause, char **out) {
1220 cJSON *val = NULL;
1222 if (err.error.err_i16 != 0) return err;
1223 if (cJSON_IsString(val)) {
1224 *out = strdup(val->valuestring);
1225 if (!*out) err.error.err_i16 = CWIST_ERROR_NOMEM;
1226 } else {
1227 err.error.err_i16 = CWIST_ERROR_PROTOCOL;
1228 }
1230 return err;
1231}
1232
1233/* ------------------------------------------------------------------ */
1234/* Exact 64-bit integers (#354) */
1235/* ------------------------------------------------------------------ */
1236
1237/* The typed helpers above read values through cJSON numbers, which are
1238 * doubles. The worker sends canonical integers as raw JSON digits, so the
1239 * exact path takes them from the response text instead: cJSON validates the
1240 * response, then a small scanner finds the same cell in the raw text. */
1241
1242static const char *orm_json_ws(const char *p) {
1243 while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++;
1244 return p;
1245}
1246
1248static const char *orm_json_skip_string(const char *p) {
1249 for (p++; *p && *p != '"'; p++) {
1250 if (*p == '\\' && !*++p) return NULL;
1251 }
1252 return *p == '"' ? p + 1 : NULL;
1253}
1254
1256static const char *orm_json_skip_value(const char *p) {
1257 p = orm_json_ws(p);
1258 if (*p == '"') return orm_json_skip_string(p);
1259 if (*p == '{' || *p == '[') {
1260 int depth = 0;
1261 while (*p) {
1262 if (*p == '"') {
1263 p = orm_json_skip_string(p);
1264 if (!p) return NULL;
1265 continue;
1266 }
1267 if (*p == '{' || *p == '[') {
1268 depth++;
1269 } else if (*p == '}' || *p == ']') {
1270 if (--depth == 0) return p + 1;
1271 }
1272 p++;
1273 }
1274 return NULL;
1275 }
1276 const char *start = p;
1277 while (*p && *p != ',' && *p != '}' && *p != ']' && *p != ' ' && *p != '\t' && *p != '\n' &&
1278 *p != '\r')
1279 p++;
1280 return p > start ? p : NULL;
1281}
1282
1291static const char *orm_json_object_get(const char *p, const char *key) {
1292 size_t key_len = strlen(key);
1293 p = orm_json_ws(p);
1294 if (*p != '{') return NULL;
1295 p = orm_json_ws(p + 1);
1296 while (*p == '"') {
1297 const char *ks = p;
1298 const char *ke = orm_json_skip_string(p);
1299 if (!ke) return NULL;
1300 p = orm_json_ws(ke);
1301 if (*p != ':') return NULL;
1302 p = orm_json_ws(p + 1);
1303 size_t n = (size_t)(ke - ks - 2);
1304 if (n == key_len && !memchr(ks + 1, '\\', n) && memcmp(ks + 1, key, n) == 0) return p;
1305 p = orm_json_skip_value(p);
1306 if (!p) return NULL;
1307 p = orm_json_ws(p);
1308 if (*p != ',') return NULL;
1309 p = orm_json_ws(p + 1);
1310 }
1311 return NULL;
1312}
1313
1320static bool orm_parse_exact_i64(const char *p, int64_t *out) {
1321 const char *s = p;
1322 const char *e;
1323 if (*p == '"') {
1324 e = orm_json_skip_string(p);
1325 if (!e) return false;
1326 s = p + 1;
1327 e--; /* closing quote */
1328 } else {
1329 e = orm_json_skip_value(p);
1330 if (!e) return false;
1331 }
1332 size_t n = (size_t)(e - s);
1333 size_t i = (n > 0 && s[0] == '-') ? 1 : 0;
1334 if (i >= n || n > 20) return false;
1335 if (s[i] == '0' && (n - i > 1 || i == 1)) return false;
1336 for (size_t k = i; k < n; k++) {
1337 if (s[k] < '0' || s[k] > '9') return false;
1338 }
1339 char buf[24];
1340 memcpy(buf, s, n);
1341 buf[n] = '\0';
1342 errno = 0;
1343 long long v = strtoll(buf, NULL, 10);
1344 if (errno == ERANGE) return false;
1345 *out = (int64_t)v;
1346 return true;
1347}
1348
1354static cwist_error_t orm_query_first_i64(cwist_orm_t *orm, const char *sql, const char *key,
1355 int64_t *out) {
1357 char *payload = NULL;
1358 if (orm_exchange(orm, sql, &payload) != 0) {
1359 err.error.err_i16 = CWIST_ERROR_IO;
1360 return err;
1361 }
1362
1363 /* cJSON checks the status and that the cell is a number or a string. */
1364 err.error.err_i16 = CWIST_ERROR_PROTOCOL;
1365 cJSON *root = cJSON_Parse(payload);
1366 cJSON *status = cJSON_GetObjectItemCaseSensitive(root, "status");
1369 bool shape_ok = cJSON_IsNumber(status) && (int)status->valuedouble == 0 &&
1372
1373 if (shape_ok) {
1374 const char *rows = orm_json_object_get(payload, "rows");
1375 const char *row = rows && *rows == '[' ? orm_json_ws(rows + 1) : NULL;
1376 const char *cell = row ? orm_json_object_get(row, key) : NULL;
1377 int64_t v;
1378 if (cell && orm_parse_exact_i64(cell, &v)) {
1379 *out = v;
1380 err.error.err_i16 = CWIST_SUCCESS;
1381 }
1382 }
1383 free(payload);
1384 return err;
1385}
1386
1388 const char *where_clause, int64_t *out) {
1390 if (!orm || !table || !column || !out) {
1391 err.error.err_i16 = CWIST_ERROR_INVALID_PARAM;
1392 return err;
1393 }
1394 char *sql = orm_build_select_one_sql(orm, table, column, where_clause);
1395 if (!sql) {
1396 err.error.err_i16 = CWIST_ERROR_NOMEM;
1397 return err;
1398 }
1399 err = orm_query_first_i64(orm, sql, column, out);
1400 free(sql);
1401 return err;
1402}
1403
1405 const cJSON *data, const char *returning_col,
1406 int64_t *out) {
1408 if (!orm || !table || !data || !returning_col || !out) {
1409 err.error.err_i16 = CWIST_ERROR_INVALID_PARAM;
1410 return err;
1411 }
1412 char *sql = orm_build_returning_sql(orm, table, data, returning_col);
1413 if (!sql) {
1414 err.error.err_i16 = CWIST_ERROR_NOMEM;
1415 return err;
1416 }
1417 err = orm_query_first_i64(orm, sql, returning_col, out);
1418 free(sql);
1419 return err;
1420}
1421
1422/* ------------------------------------------------------------------ */
1423/* Column-typed result sets (#354) */
1424/* ------------------------------------------------------------------ */
1425
1429static int orm_b64_value(unsigned char c) {
1430 if (c >= 'A' && c <= 'Z') return c - 'A';
1431 if (c >= 'a' && c <= 'z') return c - 'a' + 26;
1432 if (c >= '0' && c <= '9') return c - '0' + 52;
1433 if (c == '+') return 62;
1434 if (c == '/') return 63;
1435 return -1;
1436}
1437
1443static unsigned char *orm_b64_decode(const char *in, size_t *out_len) {
1444 size_t n = strlen(in);
1445 if (n % 4 != 0) {
1446 *out_len = SIZE_MAX;
1447 return NULL;
1448 }
1449 size_t pad = 0;
1450 if (n > 0 && in[n - 1] == '=') pad++;
1451 if (n > 1 && in[n - 2] == '=') pad++;
1452 size_t cap = n / 4 * 3 - pad;
1453 unsigned char *out = (unsigned char *)malloc(cap ? cap : 1);
1454 if (!out) {
1455 *out_len = SIZE_MAX;
1456 return NULL;
1457 }
1458 size_t o = 0;
1459 for (size_t i = 0; i < n; i += 4) {
1460 int v[4];
1461 int chars = 3;
1462 for (int k = 0; k < 4; k++) {
1463 char c = in[i + k];
1464 if (c == '=') {
1465 if (i + 4 != n || k < 2 || (k == 2 && in[i + 3] != '=')) {
1466 free(out);
1467 *out_len = SIZE_MAX;
1468 return NULL;
1469 }
1470 v[k] = 0;
1471 chars--;
1472 } else {
1473 v[k] = orm_b64_value((unsigned char)c);
1474 if (v[k] < 0) {
1475 free(out);
1476 *out_len = SIZE_MAX;
1477 return NULL;
1478 }
1479 }
1480 }
1481 unsigned triple = ((unsigned)v[0] << 18) | ((unsigned)v[1] << 12) |
1482 ((unsigned)v[2] << 6) | (unsigned)v[3];
1483 if (o < cap) out[o++] = (unsigned char)(triple >> 16);
1484 if (o < cap) out[o++] = (unsigned char)(triple >> 8);
1485 if (o < cap) out[o++] = (unsigned char)triple;
1486 }
1487 *out_len = cap;
1488 return out;
1489}
1490
1499static int orm_fill_typed_cell(cwist_orm_value_t *v, const cJSON *cell) {
1500 const cJSON *tag = cJSON_GetObjectItemCaseSensitive(cell, "t");
1501 if (!cJSON_IsString(tag) || !tag->valuestring) return CWIST_ERROR_PROTOCOL;
1502 const char *t = tag->valuestring;
1503 const cJSON *payload = cJSON_GetObjectItemCaseSensitive(cell, "v");
1504
1505 if (strcmp(t, "null") == 0) {
1506 v->type = CWIST_ORM_TYPE_NULL;
1507 return CWIST_SUCCESS;
1508 }
1509 if (strcmp(t, "int") == 0) {
1510 if (!cJSON_IsString(payload) || !payload->valuestring) return CWIST_ERROR_PROTOCOL;
1511 const char *s = payload->valuestring;
1512 if (!*s) return CWIST_ERROR_PROTOCOL;
1513 errno = 0;
1514 char *endptr = NULL;
1515 long long val = strtoll(s, &endptr, 10);
1516 if (errno == ERANGE || !endptr || *endptr != '\0') return CWIST_ERROR_PROTOCOL;
1517 v->type = CWIST_ORM_TYPE_INTEGER;
1518 v->i64 = (int64_t)val;
1519 return CWIST_SUCCESS;
1520 }
1521 if (strcmp(t, "real") == 0) {
1522 if (!cJSON_IsNumber(payload)) return CWIST_ERROR_PROTOCOL;
1523 v->type = CWIST_ORM_TYPE_REAL;
1524 v->real = payload->valuedouble;
1525 return CWIST_SUCCESS;
1526 }
1527 if (strcmp(t, "text") == 0) {
1528 if (!cJSON_IsString(payload) || !payload->valuestring) return CWIST_ERROR_PROTOCOL;
1529 v->type = CWIST_ORM_TYPE_TEXT;
1530 v->text = strdup(payload->valuestring);
1531 return v->text ? CWIST_SUCCESS : CWIST_ERROR_NOMEM;
1532 }
1533 if (strcmp(t, "blob") == 0) {
1534 if (!cJSON_IsString(payload) || !payload->valuestring) return CWIST_ERROR_PROTOCOL;
1535 size_t len = 0;
1536 unsigned char *bytes = orm_b64_decode(payload->valuestring, &len);
1537 if (len == SIZE_MAX) return CWIST_ERROR_PROTOCOL;
1538 v->type = CWIST_ORM_TYPE_BLOB;
1539 v->blob = len ? bytes : NULL;
1540 v->blob_len = len;
1541 if (!len) free(bytes);
1542 return CWIST_SUCCESS;
1543 }
1544 return CWIST_ERROR_PROTOCOL;
1545}
1546
1565 cwist_orm_result_t **result) {
1567 if (result) *result = NULL;
1568 if (!orm || !sql || !result) {
1569 err.error.err_i16 = CWIST_ERROR_INVALID_PARAM;
1570 return err;
1571 }
1572
1573 char *payload = NULL;
1574 if (orm_exchange(orm, sql, &payload) != 0) {
1575 err.error.err_i16 = CWIST_ERROR_IO;
1576 return err;
1577 }
1578
1579 err.error.err_i16 = CWIST_ERROR_PROTOCOL;
1580 cJSON *root = cJSON_Parse(payload);
1581 free(payload);
1582 if (!root) return err;
1583
1584 cwist_orm_result_t *res = NULL;
1585
1586 cJSON *status = cJSON_GetObjectItemCaseSensitive(root, "status");
1587 if (!cJSON_IsNumber(status) || (int)status->valuedouble != 0) goto done;
1588
1590 cJSON *columns = cJSON_GetObjectItemCaseSensitive(typed, "columns");
1592 if (!cJSON_IsArray(columns) || !cJSON_IsArray(trows)) goto done;
1593
1594 res = (cwist_orm_result_t *)calloc(1, sizeof(*res));
1595 if (!res) {
1596 err.error.err_i16 = CWIST_ERROR_NOMEM;
1597 goto done;
1598 }
1599
1600 res->column_count = (size_t)cJSON_GetArraySize(columns);
1601 res->row_count = (size_t)cJSON_GetArraySize(trows);
1602
1603 res->columns = (char **)calloc(res->column_count ? res->column_count : 1, sizeof(char *));
1604 if (!res->columns) {
1605 err.error.err_i16 = CWIST_ERROR_NOMEM;
1606 goto fail;
1607 }
1608 for (size_t i = 0; i < res->column_count; i++) {
1609 cJSON *name = cJSON_GetArrayItem(columns, (int)i);
1610 if (!cJSON_IsString(name) || !name->valuestring) goto fail;
1611 res->columns[i] = strdup(name->valuestring);
1612 if (!res->columns[i]) {
1613 err.error.err_i16 = CWIST_ERROR_NOMEM;
1614 goto fail;
1615 }
1616 }
1617
1618 if (res->row_count > 0) {
1619 if (res->column_count == 0 ||
1620 res->row_count > SIZE_MAX / res->column_count / sizeof(cwist_orm_value_t))
1621 goto fail;
1622 res->cells = (cwist_orm_value_t *)calloc(
1623 res->row_count * res->column_count, sizeof(cwist_orm_value_t));
1624 if (!res->cells) {
1625 err.error.err_i16 = CWIST_ERROR_NOMEM;
1626 goto fail;
1627 }
1628 }
1629
1630 size_t idx = 0;
1631 cJSON *trow = NULL;
1632 cJSON_ArrayForEach(trow, trows) {
1633 if (!cJSON_IsArray(trow) || (size_t)cJSON_GetArraySize(trow) != res->column_count)
1634 goto fail;
1635 for (size_t c = 0; c < res->column_count; c++, idx++) {
1636 int rc = orm_fill_typed_cell(&res->cells[idx], cJSON_GetArrayItem(trow, (int)c));
1637 if (rc != CWIST_SUCCESS) {
1638 err.error.err_i16 = rc;
1639 goto fail;
1640 }
1641 }
1642 }
1643
1644 *result = res;
1645 err.error.err_i16 = CWIST_SUCCESS;
1646 goto done;
1647
1648fail:
1650done:
1652 return err;
1653}
1654
1664 if (!result) return;
1665 if (result->cells) {
1666 size_t total = result->row_count * result->column_count;
1667 for (size_t i = 0; i < total; i++) {
1668 free(result->cells[i].text);
1669 free(result->cells[i].blob);
1670 }
1671 }
1672 if (result->columns) {
1673 for (size_t i = 0; i < result->column_count; i++) free(result->columns[i]);
1674 }
1675 free(result->columns);
1676 free(result->cells);
1677 free(result);
1678}
1679
1680size_t cwist_orm_result_column_index(const cwist_orm_result_t *result, const char *column) {
1681 if (!result || !column) return result ? result->column_count : 0;
1682 for (size_t i = 0; i < result->column_count; i++) {
1683 if (result->columns[i] && strcmp(result->columns[i], column) == 0) return i;
1684 }
1685 return result->column_count;
1686}
1687
1689 size_t column) {
1690 if (!result || !result->cells || row >= result->row_count || column >= result->column_count)
1691 return NULL;
1692 return &result->cells[row * result->column_count + column];
1693}
Memory allocation wrappers backed by libttak.
@ CWIST_ERR_INT16
used when checking common errcodes in Unix/Linux
Definition cwist_err.h:16
cwist_error_t make_error(cwist_errtype_t type)
Creates an error object initialized with the specified type.
Definition error.c:14
cwist_error_t cwist_orm_select_one_int(cwist_orm_t *orm, const char *table, const char *column, const char *where_clause, int *out)
Select a single column value from the first matching row as an int.
Definition orm.c:1112
cwist_error_t cwist_orm_query(cwist_orm_t *orm, const char *sql, cJSON **result)
Execute a SELECT-style query and return the result rows.
Definition orm.c:291
const cwist_orm_value_t * cwist_orm_result_cell(const cwist_orm_result_t *result, size_t row, size_t column)
Read one cell of a typed result set.
Definition orm.c:1688
cwist_error_t cwist_orm_select_one_json(cwist_orm_t *orm, const char *table, const char *column, const char *where_clause, cJSON **out)
Select a single column value from the first matching row as JSON.
Definition orm.c:1053
cwist_error_t cwist_orm_insert_returning_i64(cwist_orm_t *orm, const char *table, const cJSON *data, const char *returning_col, int64_t *out)
Insert a row and return an integer column exactly as an int64_t.
Definition orm.c:1404
cwist_error_t cwist_orm_select_one_double(cwist_orm_t *orm, const char *table, const char *column, const char *where_clause, double *out)
Select a single column value from the first matching row as a double.
Definition orm.c:1190
cwist_error_t cwist_orm_select(cwist_orm_t *orm, const char *table, const char *columns, const char *where_clause, cJSON **result)
Select rows from a table.
Definition orm.c:773
cwist_error_t cwist_orm_insert_returning_json(cwist_orm_t *orm, const char *table, const cJSON *data, const char *returning_col, cJSON **out)
Insert a row and return a JSON value from the new row.
Definition orm.c:872
cwist_error_t cwist_orm_delete(cwist_orm_t *orm, const char *table, const char *where_clause)
Delete rows from a table.
Definition orm.c:727
void cwist_orm_use_dialect(cwist_orm_t *orm, cwist_orm_dialect_t dialect)
Select the SQL dialect used for query generation.
Definition orm.c:201
cwist_error_t cwist_orm_insert_returning_int(cwist_orm_t *orm, const char *table, const cJSON *data, const char *returning_col, int *out)
Insert a row and return a column value as an int.
Definition orm.c:932
cwist_error_t cwist_orm_insert_returning_llong(cwist_orm_t *orm, const char *table, const cJSON *data, const char *returning_col, long long *out)
Insert a row and return a column value as a long long.
Definition orm.c:985
cwist_orm_t * cwist_orm_open_socket(int socket_fd)
Wrap a connected socket descriptor in an ORM session.
Definition orm.c:145
cwist_error_t cwist_orm_select_one_llong(cwist_orm_t *orm, const char *table, const char *column, const char *where_clause, long long *out)
Select a single column value from the first matching row as a long long.
Definition orm.c:1164
cwist_error_t cwist_orm_select_one_i64(cwist_orm_t *orm, const char *table, const char *column, const char *where_clause, int64_t *out)
Select a single integer cell exactly as an int64_t.
Definition orm.c:1387
void cwist_orm_immediate_commit(bool enable)
Set the global immediate-commit default for new ORM sessions.
Definition orm.c:188
cwist_error_t cwist_orm_select_one_string(cwist_orm_t *orm, const char *table, const char *column, const char *where_clause, char **out)
Select a single column value from the first matching row as a string.
Definition orm.c:1218
cwist_error_t cwist_orm_exec(cwist_orm_t *orm, const char *sql)
Execute a single SQL statement, discarding any result rows.
Definition orm.c:241
void cwist_orm_close_socket(cwist_orm_t *orm)
Close an ORM session and release its socket.
Definition orm.c:163
cwist_error_t cwist_orm_insert(cwist_orm_t *orm, const char *table, const cJSON *data)
Insert a JSON object into a table.
Definition orm.c:586
void cwist_orm_result_free(cwist_orm_result_t *result)
Release a result set from cwist_orm_query_typed().
Definition orm.c:1663
cwist_error_t cwist_orm_commit(cwist_orm_t *orm)
Commit the current transaction.
Definition orm.c:211
cwist_error_t cwist_orm_update(cwist_orm_t *orm, const char *table, const cJSON *data, const char *where_clause)
Update rows of a table with values from a JSON object.
Definition orm.c:629
cwist_error_t cwist_orm_rollback(cwist_orm_t *orm)
Roll back the current transaction.
Definition orm.c:221
cwist_error_t cwist_orm_insert_returning_long(cwist_orm_t *orm, const char *table, const cJSON *data, const char *returning_col, long *out)
Insert a row and return a column value as a long.
Definition orm.c:958
cwist_error_t cwist_orm_query_typed(cwist_orm_t *orm, const char *sql, cwist_orm_result_t **result)
Execute a SELECT-style query and return column-typed rows.
Definition orm.c:1564
size_t cwist_orm_result_column_index(const cwist_orm_result_t *result, const char *column)
Find a column index by name in a typed result set.
Definition orm.c:1680
cwist_error_t cwist_orm_select_one_long(cwist_orm_t *orm, const char *table, const char *column, const char *where_clause, long *out)
Select a single column value from the first matching row as a long.
Definition orm.c:1138
Tiny ORM over a SQL socket stream.
@ CWIST_ORM_TYPE_BLOB
Definition orm.h:284
@ CWIST_ORM_TYPE_INTEGER
Definition orm.h:281
@ CWIST_ORM_TYPE_NULL
Definition orm.h:280
@ CWIST_ORM_TYPE_TEXT
Definition orm.h:283
@ CWIST_ORM_TYPE_REAL
Definition orm.h:282
@ CWIST_ORM_SQLITE
Definition orm.h:40
@ CWIST_ORM_MYSQL
Definition orm.h:42
@ CWIST_ORM_POSTGRES
Definition orm.h:41
@ CWIST_ORM_MARIADB
Definition orm.h:43
enum cwist_orm_dialect cwist_orm_dialect_t
Supported SQL dialects for query generation.
struct cwist_orm cwist_orm_t
Opaque ORM session handle.
Definition orm.h:36
size_t cap
Definition otel.c:56
Integrated error structure used throughout the system.
Definition cwist_err.h:72
Column-typed result set produced by cwist_orm_query_typed().
Definition orm.h:314
cwist_orm_value_t * cells
Definition orm.h:318
char ** columns
Definition orm.h:317
size_t row_count
Definition orm.h:316
size_t column_count
Definition orm.h:315
One cell of a column-typed result set.
Definition orm.h:297
char * text
Definition orm.h:301
unsigned char * blob
Definition orm.h:302
#define CWIST_ERROR_INVALID_PARAM
Definition cwist_err.h:153
#define CWIST_ERROR_PROTOCOL
Definition cwist_err.h:154
#define CWIST_ERROR_IO
Definition cwist_err.h:151
#define CWIST_ERROR_NOMEM
Definition cwist_err.h:152
#define CWIST_SUCCESS
Definition cwist_err.h:149