@@ -29,6 +29,8 @@ pub const DatabaseBackend = enum(c_int) {
2929 postgresql = 2 ,
3030 sqlite = 3 ,
3131 redis = 4 ,
32+ quandledb = 5 ,
33+ lithoglyph = 6 ,
3234 custom = 99 ,
3335};
3436
@@ -572,6 +574,194 @@ fn runCurlPost(endpoint: [:0]const u8, body: [:0]const u8) ![]u8 {
572574 return stdout_list .toOwnedSlice (alloc );
573575}
574576
577+ // ═══════════════════════════════════════════════════════════════════════
578+ // KQL Execution (QuandleDB — via child curl process)
579+ // ═══════════════════════════════════════════════════════════════════════
580+
581+ /// Open a new QuandleDB connection by URL (e.g. "http://localhost:8081").
582+ /// Stores the URL in the slot's url_buf for later use by db_execute_kql.
583+ /// Returns slot index or negative error code:
584+ /// -1 = no slots available
585+ /// -6 = URL too long (exceeds URL_BUF_SIZE)
586+ pub export fn db_connect_quandledb (url_ptr : [* ]const u8 , url_len : usize ) c_int {
587+ mutex .lock ();
588+ defer mutex .unlock ();
589+
590+ if (url_len == 0 or url_len >= URL_BUF_SIZE ) return -6 ;
591+
592+ var free_idx : ? usize = null ;
593+ for (& connections , 0.. ) | * slot , i | {
594+ _ = slot ;
595+ if (! connections [i ].active ) {
596+ free_idx = i ;
597+ break ;
598+ }
599+ }
600+ const idx = free_idx orelse return -1 ;
601+
602+ @memcpy (connections [idx ].url_buf [0.. url_len ], url_ptr [0.. url_len ]);
603+ connections [idx ].url_len = url_len ;
604+ connections [idx ].active = true ;
605+ connections [idx ].state = .connected ;
606+ connections [idx ].backend = .quandledb ;
607+ connections [idx ].db_handle = null ;
608+ return @intCast (idx );
609+ }
610+
611+ /// Execute a KQL query against a QuandleDB connection via child curl.
612+ /// POSTs to {url}/kql/execute with the KQL query as JSON body.
613+ pub export fn db_execute_kql (slot : u8 , kql_ptr : [* ]const u8 , kql_len : usize , out_ptr : [* ]u8 , out_len : usize ) callconv (.c ) i32 {
614+ var endpoint_buf : [600 ]u8 = undefined ;
615+ var endpoint_total : usize = 0 ;
616+ var kql_buf : [8192 ]u8 = undefined ;
617+ var safe_kql_len : usize = 0 ;
618+
619+ {
620+ mutex .lock ();
621+ defer mutex .unlock ();
622+
623+ if (slot >= MAX_CONNECTIONS ) return -1 ;
624+ const idx : usize = @intCast (slot );
625+ if (! connections [idx ].active ) return -1 ;
626+ if (! isValidTransition (connections [idx ].state , .querying )) return -2 ;
627+ if (connections [idx ].url_len == 0 ) return -6 ;
628+
629+ const url_slice = connections [idx ].url_buf [0.. connections [idx ].url_len ];
630+ const suffix = "/kql/execute" ;
631+ if (url_slice .len + suffix .len >= endpoint_buf .len ) return -6 ;
632+ @memcpy (endpoint_buf [0.. url_slice .len ], url_slice );
633+ @memcpy (endpoint_buf [url_slice .len .. ][0.. suffix .len ], suffix );
634+ endpoint_total = url_slice .len + suffix .len ;
635+ endpoint_buf [endpoint_total ] = 0 ;
636+
637+ safe_kql_len = @min (kql_len , kql_buf .len - 1 );
638+ @memcpy (kql_buf [0.. safe_kql_len ], kql_ptr [0.. safe_kql_len ]);
639+ kql_buf [safe_kql_len ] = 0 ;
640+
641+ connections [idx ].state = .querying ;
642+ }
643+
644+ const child_result = runCurlPost (
645+ endpoint_buf [0.. endpoint_total :0 ],
646+ kql_buf [0.. safe_kql_len :0 ],
647+ );
648+
649+ mutex .lock ();
650+ defer mutex .unlock ();
651+
652+ const idx : usize = @intCast (slot );
653+ if (! connections [idx ].active or connections [idx ].state != .querying ) return -1 ;
654+
655+ if (child_result ) | result | {
656+ defer std .heap .page_allocator .free (result );
657+ const written = result .len ;
658+ if (written > out_len ) {
659+ connections [idx ].state = .err ;
660+ return -5 ;
661+ }
662+ @memcpy (out_ptr [0.. written ], result [0.. written ]);
663+ connections [idx ].state = .connected ;
664+ return @intCast (written );
665+ } else | _ | {
666+ connections [idx ].state = .err ;
667+ return -7 ;
668+ }
669+ }
670+
671+ // ═══════════════════════════════════════════════════════════════════════
672+ // GQL Execution (LithoGlyph — via child curl process)
673+ // ═══════════════════════════════════════════════════════════════════════
674+
675+ /// Open a new LithoGlyph connection by URL (e.g. "http://localhost:8082").
676+ /// Stores the URL in the slot's url_buf for later use by db_execute_gql.
677+ /// Returns slot index or negative error code:
678+ /// -1 = no slots available
679+ /// -6 = URL too long (exceeds URL_BUF_SIZE)
680+ pub export fn db_connect_lithoglyph (url_ptr : [* ]const u8 , url_len : usize ) c_int {
681+ mutex .lock ();
682+ defer mutex .unlock ();
683+
684+ if (url_len == 0 or url_len >= URL_BUF_SIZE ) return -6 ;
685+
686+ var free_idx : ? usize = null ;
687+ for (& connections , 0.. ) | * slot , i | {
688+ _ = slot ;
689+ if (! connections [i ].active ) {
690+ free_idx = i ;
691+ break ;
692+ }
693+ }
694+ const idx = free_idx orelse return -1 ;
695+
696+ @memcpy (connections [idx ].url_buf [0.. url_len ], url_ptr [0.. url_len ]);
697+ connections [idx ].url_len = url_len ;
698+ connections [idx ].active = true ;
699+ connections [idx ].state = .connected ;
700+ connections [idx ].backend = .lithoglyph ;
701+ connections [idx ].db_handle = null ;
702+ return @intCast (idx );
703+ }
704+
705+ /// Execute a GQL query against a LithoGlyph connection via child curl.
706+ /// POSTs to {url}/gql/execute with the GQL query as JSON body.
707+ pub export fn db_execute_gql (slot : u8 , gql_ptr : [* ]const u8 , gql_len : usize , out_ptr : [* ]u8 , out_len : usize ) callconv (.c ) i32 {
708+ var endpoint_buf : [600 ]u8 = undefined ;
709+ var endpoint_total : usize = 0 ;
710+ var gql_buf : [8192 ]u8 = undefined ;
711+ var safe_gql_len : usize = 0 ;
712+
713+ {
714+ mutex .lock ();
715+ defer mutex .unlock ();
716+
717+ if (slot >= MAX_CONNECTIONS ) return -1 ;
718+ const idx : usize = @intCast (slot );
719+ if (! connections [idx ].active ) return -1 ;
720+ if (! isValidTransition (connections [idx ].state , .querying )) return -2 ;
721+ if (connections [idx ].url_len == 0 ) return -6 ;
722+
723+ const url_slice = connections [idx ].url_buf [0.. connections [idx ].url_len ];
724+ const suffix = "/gql/execute" ;
725+ if (url_slice .len + suffix .len >= endpoint_buf .len ) return -6 ;
726+ @memcpy (endpoint_buf [0.. url_slice .len ], url_slice );
727+ @memcpy (endpoint_buf [url_slice .len .. ][0.. suffix .len ], suffix );
728+ endpoint_total = url_slice .len + suffix .len ;
729+ endpoint_buf [endpoint_total ] = 0 ;
730+
731+ safe_gql_len = @min (gql_len , gql_buf .len - 1 );
732+ @memcpy (gql_buf [0.. safe_gql_len ], gql_ptr [0.. safe_gql_len ]);
733+ gql_buf [safe_gql_len ] = 0 ;
734+
735+ connections [idx ].state = .querying ;
736+ }
737+
738+ const child_result = runCurlPost (
739+ endpoint_buf [0.. endpoint_total :0 ],
740+ gql_buf [0.. safe_gql_len :0 ],
741+ );
742+
743+ mutex .lock ();
744+ defer mutex .unlock ();
745+
746+ const idx : usize = @intCast (slot );
747+ if (! connections [idx ].active or connections [idx ].state != .querying ) return -1 ;
748+
749+ if (child_result ) | result | {
750+ defer std .heap .page_allocator .free (result );
751+ const written = result .len ;
752+ if (written > out_len ) {
753+ connections [idx ].state = .err ;
754+ return -5 ;
755+ }
756+ @memcpy (out_ptr [0.. written ], result [0.. written ]);
757+ connections [idx ].state = .connected ;
758+ return @intCast (written );
759+ } else | _ | {
760+ connections [idx ].state = .err ;
761+ return -7 ;
762+ }
763+ }
764+
575765// ═══════════════════════════════════════════════════════════════════════
576766// Standard Cartridge Interface (loader expects these 4 C-ABI symbols)
577767// ═══════════════════════════════════════════════════════════════════════
@@ -824,6 +1014,80 @@ test "verisimdb query lifecycle through state machine" {
8241014 try std .testing .expectEqual (@as (c_int , 0 ), db_disconnect (slot ));
8251015}
8261016
1017+ // ─── QuandleDB connection lifecycle tests ─────────────────────────────
1018+
1019+ test "quandledb connect stores URL and disconnects" {
1020+ db_reset ();
1021+ const url = "http://localhost:8081" ;
1022+ const slot = db_connect_quandledb (url , url .len );
1023+ try std .testing .expect (slot >= 0 );
1024+ try std .testing .expectEqual (@as (c_int , @intFromEnum (ConnState .connected )), db_state (slot ));
1025+
1026+ const idx : usize = @intCast (slot );
1027+ try std .testing .expectEqual (DatabaseBackend .quandledb , connections [idx ].backend );
1028+ try std .testing .expectEqual (url .len , connections [idx ].url_len );
1029+ try std .testing .expect (connections [idx ].db_handle == null );
1030+
1031+ try std .testing .expectEqual (@as (c_int , 0 ), db_disconnect (slot ));
1032+ try std .testing .expectEqual (@as (usize , 0 ), connections [idx ].url_len );
1033+ }
1034+
1035+ test "quandledb connect rejects empty URL" {
1036+ db_reset ();
1037+ const slot = db_connect_quandledb ("" , 0 );
1038+ try std .testing .expectEqual (@as (c_int , -6 ), slot );
1039+ }
1040+
1041+ test "quandledb query lifecycle through state machine" {
1042+ db_reset ();
1043+ const url = "http://localhost:8081" ;
1044+ const slot = db_connect_quandledb (url , url .len );
1045+ try std .testing .expect (slot >= 0 );
1046+
1047+ try std .testing .expectEqual (@as (c_int , 0 ), db_begin_query (slot ));
1048+ try std .testing .expectEqual (@as (c_int , @intFromEnum (ConnState .querying )), db_state (slot ));
1049+ try std .testing .expectEqual (@as (c_int , 0 ), db_end_query (slot ));
1050+ try std .testing .expectEqual (@as (c_int , @intFromEnum (ConnState .connected )), db_state (slot ));
1051+ try std .testing .expectEqual (@as (c_int , 0 ), db_disconnect (slot ));
1052+ }
1053+
1054+ // ─── LithoGlyph connection lifecycle tests ────────────────────────────
1055+
1056+ test "lithoglyph connect stores URL and disconnects" {
1057+ db_reset ();
1058+ const url = "http://localhost:8082" ;
1059+ const slot = db_connect_lithoglyph (url , url .len );
1060+ try std .testing .expect (slot >= 0 );
1061+ try std .testing .expectEqual (@as (c_int , @intFromEnum (ConnState .connected )), db_state (slot ));
1062+
1063+ const idx : usize = @intCast (slot );
1064+ try std .testing .expectEqual (DatabaseBackend .lithoglyph , connections [idx ].backend );
1065+ try std .testing .expectEqual (url .len , connections [idx ].url_len );
1066+ try std .testing .expect (connections [idx ].db_handle == null );
1067+
1068+ try std .testing .expectEqual (@as (c_int , 0 ), db_disconnect (slot ));
1069+ try std .testing .expectEqual (@as (usize , 0 ), connections [idx ].url_len );
1070+ }
1071+
1072+ test "lithoglyph connect rejects empty URL" {
1073+ db_reset ();
1074+ const slot = db_connect_lithoglyph ("" , 0 );
1075+ try std .testing .expectEqual (@as (c_int , -6 ), slot );
1076+ }
1077+
1078+ test "lithoglyph query lifecycle through state machine" {
1079+ db_reset ();
1080+ const url = "http://localhost:8082" ;
1081+ const slot = db_connect_lithoglyph (url , url .len );
1082+ try std .testing .expect (slot >= 0 );
1083+
1084+ try std .testing .expectEqual (@as (c_int , 0 ), db_begin_query (slot ));
1085+ try std .testing .expectEqual (@as (c_int , @intFromEnum (ConnState .querying )), db_state (slot ));
1086+ try std .testing .expectEqual (@as (c_int , 0 ), db_end_query (slot ));
1087+ try std .testing .expectEqual (@as (c_int , @intFromEnum (ConnState .connected )), db_state (slot ));
1088+ try std .testing .expectEqual (@as (c_int , 0 ), db_disconnect (slot ));
1089+ }
1090+
8271091test "verisimdb execute_vql rejects non-verisimdb slot" {
8281092 db_reset ();
8291093 // Connect with sqlite backend (has a handle, not a URL)
0 commit comments