@@ -28,7 +28,7 @@ static const vector<string> STATA_COMMANDS = {"use", "list", "clear",
2828 " generate" , " replace" , " rename" , " sort" , " order" ,
2929 " egen" , " collapse" , " count" , " describe" , " summarize" ,
3030 " tabulate" , " head" , " tail" , " save" , " append" ,
31- " mvencode" , " reshape" , " do" };
31+ " mvencode" , " reshape" , " do" , " label " };
3232
3333// Command classification for do-file execution
3434// Transformation: modifies the CTE chain state
@@ -37,7 +37,7 @@ static const vector<string> STATA_COMMANDS = {"use", "list", "clear",
3737static bool IsTransformationCommand (const string &command) {
3838 static const vector<string> TRANSFORMATION = {" use" , " clear" , " do" , " keep" , " drop" , " generate" , " replace" ,
3939 " rename" , " sort" , " order" , " egen" , " collapse" , " mvencode" ,
40- " reshape" , " append" };
40+ " reshape" , " append" , " label " };
4141 for (auto &cmd : TRANSFORMATION ) {
4242 if (command == cmd) {
4343 return true ;
@@ -384,6 +384,96 @@ static string ProcessCommand(const StataCommand &cmd, StataDoStateInfo &state) {
384384 return " SELECT 'OK' AS status" ;
385385 }
386386
387+ if (cmd.command == " label" ) {
388+ // label variable varname "label text"
389+ // label define labelname val1 "text1" val2 "text2" ...
390+ // label values varname labelname
391+ string lower_args = StringUtil::Lower (cmd.arguments );
392+
393+ if (StringUtil::StartsWith (lower_args, " variable " )) {
394+ // label variable varname "label text"
395+ string rest = Trim (cmd.arguments .substr (9 ));
396+ // First token is the variable name
397+ idx_t space = rest.find (' ' );
398+ if (space == string::npos) {
399+ throw ParserException (" 'label variable' requires: label variable varname \" label text\" " );
400+ }
401+ string varname = Trim (rest.substr (0 , space));
402+ string label_text = ExtractQuotedString (Trim (rest.substr (space + 1 )));
403+ state.variable_labels [varname] = label_text;
404+ return " SELECT 'OK' AS status" ;
405+ }
406+
407+ if (StringUtil::StartsWith (lower_args, " define " )) {
408+ // label define labelname val1 "text1" val2 "text2" ...
409+ string rest = Trim (cmd.arguments .substr (7 ));
410+ // First token is the label name
411+ idx_t space = rest.find (' ' );
412+ if (space == string::npos) {
413+ throw ParserException (" 'label define' requires: label define labelname value \" text\" ..." );
414+ }
415+ string label_name = Trim (rest.substr (0 , space));
416+ string remaining = Trim (rest.substr (space + 1 ));
417+
418+ unordered_map<int , string> mapping;
419+ while (!remaining.empty ()) {
420+ // Parse: integer "text" pairs
421+ idx_t sp = remaining.find (' ' );
422+ if (sp == string::npos) {
423+ break ;
424+ }
425+ string val_str = Trim (remaining.substr (0 , sp));
426+ remaining = Trim (remaining.substr (sp + 1 ));
427+
428+ // Parse the quoted text
429+ string text;
430+ if (!remaining.empty () && remaining[0 ] == ' "' ) {
431+ idx_t end_quote = remaining.find (' "' , 1 );
432+ if (end_quote == string::npos) {
433+ throw ParserException (" Unmatched quote in label define" );
434+ }
435+ text = remaining.substr (1 , end_quote - 1 );
436+ remaining = Trim (remaining.substr (end_quote + 1 ));
437+ } else {
438+ // Unquoted text — take until next space
439+ idx_t sp2 = remaining.find (' ' );
440+ if (sp2 != string::npos) {
441+ text = Trim (remaining.substr (0 , sp2));
442+ remaining = Trim (remaining.substr (sp2 + 1 ));
443+ } else {
444+ text = Trim (remaining);
445+ remaining = " " ;
446+ }
447+ }
448+
449+ int val = std::stoi (val_str);
450+ mapping[val] = text;
451+ }
452+
453+ state.value_label_defs [label_name] = mapping;
454+ return " SELECT 'OK' AS status" ;
455+ }
456+
457+ if (StringUtil::StartsWith (lower_args, " values " )) {
458+ // label values varname labelname
459+ string rest = Trim (cmd.arguments .substr (7 ));
460+ auto parts = StringUtil::Split (rest, ' ' );
461+ if (parts.size () != 2 ) {
462+ throw ParserException (" 'label values' requires: label values varname labelname" );
463+ }
464+ string varname = Trim (parts[0 ]);
465+ string label_name = Trim (parts[1 ]);
466+ // Verify the label definition exists
467+ if (state.value_label_defs .find (label_name) == state.value_label_defs .end ()) {
468+ throw ParserException (" Value label '%s' not defined. Use 'label define' first." , label_name);
469+ }
470+ state.column_labels [varname] = label_name;
471+ return " SELECT 'OK' AS status" ;
472+ }
473+
474+ throw ParserException (" Unknown label subcommand. Use: label variable, label define, or label values" );
475+ }
476+
387477 if (!state.HasData ()) {
388478 throw ParserException (" No dataset in memory. Use 'use' to load data first." );
389479 }
@@ -741,7 +831,43 @@ static string ProcessCommand(const StataCommand &cmd, StataDoStateInfo &state) {
741831 }
742832
743833 if (cmd.command == " describe" ) {
744- return " SELECT column_name, column_type FROM (DESCRIBE " + state.BuildQuery (" SELECT * FROM " + prev) + " )" ;
834+ // Build a query that shows column metadata including labels
835+ // Base: column_name, column_type from DESCRIBE
836+ // Enhanced: add variable_label and value_label columns from state
837+ string base_sql = " SELECT column_name, column_type FROM (DESCRIBE " +
838+ state.BuildQuery (" SELECT * FROM " + prev) + " )" ;
839+
840+ if (state.variable_labels .empty () && state.column_labels .empty ()) {
841+ return base_sql;
842+ }
843+
844+ // Build a CASE expression for variable labels
845+ string var_label_expr = " CASE column_name" ;
846+ bool has_var_labels = false ;
847+ for (auto &[col, label] : state.variable_labels ) {
848+ string escaped = label;
849+ // Escape single quotes
850+ size_t pos = 0 ;
851+ while ((pos = escaped.find (' \' ' , pos)) != string::npos) {
852+ escaped.replace (pos, 1 , " ''" );
853+ pos += 2 ;
854+ }
855+ var_label_expr += " WHEN '" + col + " ' THEN '" + escaped + " '" ;
856+ has_var_labels = true ;
857+ }
858+ var_label_expr += " ELSE '' END AS variable_label" ;
859+
860+ // Build a CASE expression for value label names
861+ string val_label_expr = " CASE column_name" ;
862+ bool has_val_labels = false ;
863+ for (auto &[col, label_name] : state.column_labels ) {
864+ val_label_expr += " WHEN '" + col + " ' THEN '" + label_name + " '" ;
865+ has_val_labels = true ;
866+ }
867+ val_label_expr += " ELSE '' END AS value_label" ;
868+
869+ return " SELECT column_name, column_type, " + var_label_expr + " , " + val_label_expr +
870+ " FROM (DESCRIBE " + state.BuildQuery (" SELECT * FROM " + prev) + " )" ;
745871 }
746872
747873 if (cmd.command == " summarize" ) {
@@ -985,67 +1111,124 @@ static ParserExtensionParseResult stata_do_parse(ParserExtensionInfo *info, cons
9851111 return ParserExtensionParseResult (std::move (parse_data));
9861112}
9871113
1114+ // Check if a single statement needs parser_override (SQL keyword conflict or PIVOT)
1115+ static bool NeedsParserOverride (const string &stmt, StataDoStateInfo &state) {
1116+ string lower = StringUtil::Lower (stmt);
1117+ if (state.HasData () || (!state.cte_steps .empty ())) {
1118+ if (StringUtil::StartsWith (lower, " describe" ) &&
1119+ (lower.size () == 8 || lower[8 ] == ' ' )) {
1120+ return true ;
1121+ }
1122+ if (StringUtil::StartsWith (lower, " summarize" ) &&
1123+ (lower.size () == 9 || lower[9 ] == ' ' )) {
1124+ return true ;
1125+ }
1126+ if (StringUtil::StartsWith (lower, " reshape" ) && lower.find (" wide" ) != string::npos) {
1127+ return true ;
1128+ }
1129+ }
1130+ return false ;
1131+ }
1132+
9881133// ===--------------------------------------------------------------------===//
9891134// parser_override: handles commands that conflict with SQL keywords
990- // (describe, summarize) — called BEFORE the standard parser
1135+ // (describe, summarize, reshape wide) — called BEFORE the standard parser.
1136+ // Must handle full multi-statement strings by splitting on ';'.
9911137// ===--------------------------------------------------------------------===//
9921138static ParserOverrideResult stata_do_parser_override (ParserExtensionInfo *info, const string &query,
9931139 ParserOptions &options) {
994- string trimmed = Trim (query);
995- if (!trimmed.empty () && trimmed.back () == ' ;' ) {
996- trimmed.pop_back ();
997- trimmed = Trim (trimmed);
998- }
999- string lower = StringUtil::Lower (trimmed);
1000-
1001- // Only intercept describe/summarize when we have data loaded
10021140 auto &state = dynamic_cast <StataDoStateInfo &>(*info);
1003- if (!state.HasData ()) {
1004- return ParserOverrideResult ();
1005- }
10061141
1007- bool is_ours = false ;
1008- if (StringUtil::StartsWith (lower, " describe" ) &&
1009- (lower.size () == 8 || lower[8 ] == ' ' || lower[8 ] == ' ;' )) {
1010- is_ours = true ;
1011- }
1012- if (StringUtil::StartsWith (lower, " summarize" ) &&
1013- (lower.size () == 9 || lower[9 ] == ' ' || lower[9 ] == ' ;' )) {
1014- is_ours = true ;
1015- }
1016- // reshape wide needs parser_override because PIVOT generates MULTI statements
1017- if (StringUtil::StartsWith (lower, " reshape" ) && lower.find (" wide" ) != string::npos && state.HasData ()) {
1018- is_ours = true ;
1142+ // Split the full query into individual statements by ';'
1143+ auto statements_str = StringUtil::Split (query, ' ;' );
1144+
1145+ // Check if ANY statement needs our override
1146+ // We must take over the ENTIRE query if any statement is describe/summarize/reshape wide,
1147+ // because the standard parser would handle those before our parse_function sees them.
1148+ // We also take over if earlier Stata commands will load data (making later describe valid).
1149+ bool has_stata_commands = false ;
1150+ bool has_conflict_commands = false ;
1151+ for (auto &s : statements_str) {
1152+ string trimmed = Trim (s);
1153+ if (trimmed.empty ()) {
1154+ continue ;
1155+ }
1156+ string lower = StringUtil::Lower (trimmed);
1157+
1158+ // Check for SQL-conflicting commands (describe, summarize, reshape wide)
1159+ if (StringUtil::StartsWith (lower, " describe" ) || StringUtil::StartsWith (lower, " summarize" )) {
1160+ has_conflict_commands = true ;
1161+ }
1162+ if (StringUtil::StartsWith (lower, " reshape" ) && lower.find (" wide" ) != string::npos) {
1163+ has_conflict_commands = true ;
1164+ }
1165+
1166+ string cmd_name;
1167+ if (IsStataCommand (trimmed, cmd_name)) {
1168+ has_stata_commands = true ;
1169+ }
10191170 }
10201171
1021- if (!is_ours) {
1172+ // Take over if there's a SQL-conflicting command AND either Stata commands or existing state.
1173+ // When we take over, we process ALL statements ourselves (updating state for use/label/etc
1174+ // before generating SQL for describe/summarize).
1175+ bool any_needs_override = has_conflict_commands && (has_stata_commands || state.HasData ());
1176+
1177+ if (!any_needs_override) {
10221178 return ParserOverrideResult ();
10231179 }
10241180
1181+ // Process ALL statements ourselves
10251182 try {
1026- auto cmd = TokenizeCommand (query);
1027- string sql = ProcessCommand (cmd, state);
1183+ vector<unique_ptr<SQLStatement>> all_statements;
10281184
1029- // Handle __PIVOT__ marker: CREATE TEMP TABLE + restart chain
1030- if (StringUtil::StartsWith (sql, " __PIVOT__:" )) {
1031- string rest = sql.substr (10 );
1032- // Split on ||STATE|| marker
1033- idx_t state_pos = rest.find (" ||STATE||" );
1034- string pivot_sql = rest.substr (0 , state_pos);
1035- string table_name = rest.substr (state_pos + 9 );
1036-
1037- Parser parser;
1038- parser.ParseQuery (pivot_sql);
1185+ for (auto &s : statements_str) {
1186+ string trimmed = Trim (s);
1187+ if (trimmed.empty ()) {
1188+ continue ;
1189+ }
10391190
1040- // Restart chain from the temp table
1041- state.AddStep (" SELECT * FROM " + table_name);
1191+ // Check if this is a Stata command
1192+ string cmd_name;
1193+ if (IsStataCommand (trimmed, cmd_name)) {
1194+ auto cmd = TokenizeCommand (trimmed);
1195+ string sql = ProcessCommand (cmd, state);
1196+
1197+ // Handle __PIVOT__ marker
1198+ if (StringUtil::StartsWith (sql, " __PIVOT__:" )) {
1199+ string rest = sql.substr (10 );
1200+ idx_t state_pos = rest.find (" ||STATE||" );
1201+ string pivot_sql = rest.substr (0 , state_pos);
1202+ string table_name = rest.substr (state_pos + 9 );
1203+
1204+ Parser parser;
1205+ parser.ParseQuery (pivot_sql);
1206+ state.AddStep (" SELECT * FROM " + table_name);
1207+ for (auto &stmt : parser.statements ) {
1208+ all_statements.push_back (std::move (stmt));
1209+ }
1210+ continue ;
1211+ }
10421212
1043- return ParserOverrideResult (std::move (parser.statements ));
1213+ Parser parser;
1214+ parser.ParseQuery (sql);
1215+ for (auto &stmt : parser.statements ) {
1216+ all_statements.push_back (std::move (stmt));
1217+ }
1218+ } else {
1219+ // Not a Stata command — parse as regular SQL
1220+ Parser parser;
1221+ parser.ParseQuery (trimmed);
1222+ for (auto &stmt : parser.statements ) {
1223+ all_statements.push_back (std::move (stmt));
1224+ }
1225+ }
10441226 }
10451227
1046- Parser parser;
1047- parser.ParseQuery (sql);
1048- return ParserOverrideResult (std::move (parser.statements ));
1228+ if (all_statements.empty ()) {
1229+ return ParserOverrideResult ();
1230+ }
1231+ return ParserOverrideResult (std::move (all_statements));
10491232 } catch (std::exception &ex) {
10501233 return ParserOverrideResult (ex);
10511234 }
0 commit comments