@gblikas/querykit 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/.cursor/BUGBOT.md +65 -2
  2. package/.husky/pre-commit +3 -3
  3. package/README.md +510 -1
  4. package/dist/index.d.ts +36 -3
  5. package/dist/index.js +20 -3
  6. package/dist/parser/index.d.ts +1 -0
  7. package/dist/parser/index.js +1 -0
  8. package/dist/parser/input-parser.d.ts +215 -0
  9. package/dist/parser/input-parser.js +493 -0
  10. package/dist/parser/parser.d.ts +114 -1
  11. package/dist/parser/parser.js +716 -0
  12. package/dist/parser/types.d.ts +432 -0
  13. package/dist/virtual-fields/index.d.ts +5 -0
  14. package/dist/virtual-fields/index.js +21 -0
  15. package/dist/virtual-fields/resolver.d.ts +17 -0
  16. package/dist/virtual-fields/resolver.js +107 -0
  17. package/dist/virtual-fields/types.d.ts +160 -0
  18. package/dist/virtual-fields/types.js +5 -0
  19. package/examples/qk-next/app/page.tsx +190 -86
  20. package/examples/qk-next/package.json +1 -1
  21. package/package.json +2 -2
  22. package/src/adapters/drizzle/index.ts +3 -3
  23. package/src/index.ts +77 -8
  24. package/src/parser/divergence.test.ts +357 -0
  25. package/src/parser/index.ts +2 -1
  26. package/src/parser/input-parser.test.ts +770 -0
  27. package/src/parser/input-parser.ts +697 -0
  28. package/src/parser/parse-with-context-suggestions.test.ts +360 -0
  29. package/src/parser/parse-with-context-validation.test.ts +447 -0
  30. package/src/parser/parse-with-context.test.ts +325 -0
  31. package/src/parser/parser.ts +872 -0
  32. package/src/parser/token-consistency.test.ts +341 -0
  33. package/src/parser/types.ts +545 -23
  34. package/src/virtual-fields/index.ts +6 -0
  35. package/src/virtual-fields/integration.test.ts +338 -0
  36. package/src/virtual-fields/resolver.ts +165 -0
  37. package/src/virtual-fields/types.ts +203 -0
  38. package/src/virtual-fields/virtual-fields.test.ts +831 -0
  39. package/examples/qk-next/pnpm-lock.yaml +0 -5623
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.QueryParser = exports.QueryParseError = void 0;
4
4
  const liqe_1 = require("liqe");
5
+ const input_parser_1 = require("./input-parser");
5
6
  /**
6
7
  * Error thrown when query parsing fails
7
8
  */
@@ -355,5 +356,720 @@ class QueryParser {
355
356
  : field;
356
357
  return this.options.fieldMappings[normalizedField] ?? normalizedField;
357
358
  }
359
+ /**
360
+ * Parse a query string with full context information.
361
+ *
362
+ * Unlike `parse()`, this method never throws. Instead, it returns a result object
363
+ * that indicates success or failure along with rich contextual information useful
364
+ * for building search UIs.
365
+ *
366
+ * @param query The query string to parse
367
+ * @param options Optional configuration (cursor position, etc.)
368
+ * @returns Rich parse result with tokens, AST/error, and structural analysis
369
+ *
370
+ * @example
371
+ * ```typescript
372
+ * const result = parser.parseWithContext('status:done AND priority:high');
373
+ *
374
+ * if (result.success) {
375
+ * // Use result.ast for query execution
376
+ * console.log('Valid query:', result.ast);
377
+ * } else {
378
+ * // Show error to user
379
+ * console.log('Error:', result.error?.message);
380
+ * }
381
+ *
382
+ * // Always available for UI rendering
383
+ * console.log('Tokens:', result.tokens);
384
+ * console.log('Structure:', result.structure);
385
+ * ```
386
+ */
387
+ parseWithContext(query, options = {}) {
388
+ // Get tokens from input parser (always works, even for invalid input)
389
+ const tokenResult = (0, input_parser_1.parseQueryTokens)(query, options.cursorPosition);
390
+ const tokens = this.convertTokens(tokenResult.tokens);
391
+ // Analyze structure
392
+ const structure = this.analyzeStructure(query, tokens);
393
+ // Attempt full parse
394
+ let ast;
395
+ let error;
396
+ let success = false;
397
+ try {
398
+ ast = this.parse(query);
399
+ success = true;
400
+ }
401
+ catch (e) {
402
+ const errorMessage = e instanceof Error ? e.message : String(e);
403
+ error = {
404
+ message: errorMessage,
405
+ // Try to extract position from error message if available
406
+ position: this.extractErrorPosition(errorMessage),
407
+ problematicText: this.extractProblematicText(query, errorMessage)
408
+ };
409
+ }
410
+ // Determine active token
411
+ const activeToken = tokenResult.activeToken
412
+ ? this.convertSingleToken(tokenResult.activeToken)
413
+ : undefined;
414
+ // Build base result
415
+ const result = {
416
+ success,
417
+ input: query,
418
+ ast,
419
+ error,
420
+ tokens,
421
+ activeToken,
422
+ activeTokenIndex: tokenResult.activeTokenIndex,
423
+ structure
424
+ };
425
+ // Perform field validation if schema provided
426
+ if (options.schema) {
427
+ result.fieldValidation = this.validateFields(structure.referencedFields, options.schema);
428
+ }
429
+ // Perform security pre-check if security options provided
430
+ if (options.securityOptions) {
431
+ result.security = this.performSecurityCheck(structure, options.securityOptions);
432
+ }
433
+ // Generate autocomplete suggestions if cursor position provided
434
+ if (options.cursorPosition !== undefined) {
435
+ result.suggestions = this.generateAutocompleteSuggestions(query, options.cursorPosition, activeToken, options.schema);
436
+ }
437
+ // Generate error recovery suggestions if parsing failed
438
+ if (!success) {
439
+ result.recovery = this.generateErrorRecovery(query, structure);
440
+ }
441
+ return result;
442
+ }
443
+ /**
444
+ * Convert tokens from input parser format to IQueryToken format
445
+ */
446
+ convertTokens(tokens) {
447
+ return tokens.map(token => this.convertSingleToken(token));
448
+ }
449
+ /**
450
+ * Convert a single token from input parser format
451
+ */
452
+ convertSingleToken(token) {
453
+ if (token.type === 'term') {
454
+ return {
455
+ type: 'term',
456
+ key: token.key,
457
+ operator: token.operator,
458
+ value: token.value,
459
+ startPosition: token.startPosition,
460
+ endPosition: token.endPosition,
461
+ raw: token.raw
462
+ };
463
+ }
464
+ else {
465
+ return {
466
+ type: 'operator',
467
+ operator: token.operator,
468
+ startPosition: token.startPosition,
469
+ endPosition: token.endPosition,
470
+ raw: token.raw
471
+ };
472
+ }
473
+ }
474
+ /**
475
+ * Analyze the structure of a query
476
+ */
477
+ analyzeStructure(query, tokens) {
478
+ // Count parentheses
479
+ const openParens = (query.match(/\(/g) || []).length;
480
+ const closeParens = (query.match(/\)/g) || []).length;
481
+ const hasBalancedParentheses = openParens === closeParens;
482
+ // Count quotes
483
+ const singleQuotes = (query.match(/'/g) || []).length;
484
+ const doubleQuotes = (query.match(/"/g) || []).length;
485
+ const hasBalancedQuotes = singleQuotes % 2 === 0 && doubleQuotes % 2 === 0;
486
+ // Count terms and operators
487
+ const termTokens = tokens.filter(t => t.type === 'term');
488
+ const operatorTokens = tokens.filter(t => t.type === 'operator');
489
+ const clauseCount = termTokens.length;
490
+ const operatorCount = operatorTokens.length;
491
+ // Extract referenced fields
492
+ const referencedFields = [];
493
+ for (const token of termTokens) {
494
+ if (token.type === 'term' && token.key !== null) {
495
+ if (!referencedFields.includes(token.key)) {
496
+ referencedFields.push(token.key);
497
+ }
498
+ }
499
+ }
500
+ // Calculate depth (by counting max nesting in parentheses)
501
+ const depth = this.calculateDepth(query);
502
+ // Check if complete
503
+ const isComplete = (0, input_parser_1.isInputComplete)(query);
504
+ // Determine complexity
505
+ const complexity = this.determineComplexity(clauseCount, operatorCount, depth);
506
+ return {
507
+ depth,
508
+ clauseCount,
509
+ operatorCount,
510
+ hasBalancedParentheses,
511
+ hasBalancedQuotes,
512
+ isComplete,
513
+ referencedFields,
514
+ complexity
515
+ };
516
+ }
517
+ /**
518
+ * Calculate the maximum nesting depth of parentheses
519
+ */
520
+ calculateDepth(query) {
521
+ let maxDepth = 0;
522
+ let currentDepth = 0;
523
+ for (const char of query) {
524
+ if (char === '(') {
525
+ currentDepth++;
526
+ maxDepth = Math.max(maxDepth, currentDepth);
527
+ }
528
+ else if (char === ')') {
529
+ currentDepth = Math.max(0, currentDepth - 1);
530
+ }
531
+ }
532
+ // Base depth is 1 if there's any content
533
+ return query.trim().length > 0 ? Math.max(1, maxDepth) : 0;
534
+ }
535
+ /**
536
+ * Determine query complexity classification
537
+ */
538
+ determineComplexity(clauseCount, operatorCount, depth) {
539
+ // Simple: 1-2 clauses, no nesting
540
+ if (clauseCount <= 2 && depth <= 1) {
541
+ return 'simple';
542
+ }
543
+ // Complex: many clauses, deep nesting, or many operators
544
+ if (clauseCount > 5 || depth > 3 || operatorCount > 4) {
545
+ return 'complex';
546
+ }
547
+ return 'moderate';
548
+ }
549
+ /**
550
+ * Try to extract error position from error message
551
+ */
552
+ extractErrorPosition(errorMessage) {
553
+ // Try to find position indicators in error messages
554
+ // e.g., "at position 15" or "column 15"
555
+ const posMatch = errorMessage.match(/(?:position|column|offset)\s*(\d+)/i);
556
+ if (posMatch) {
557
+ return parseInt(posMatch[1], 10);
558
+ }
559
+ return undefined;
560
+ }
561
+ /**
562
+ * Try to extract the problematic text from the query based on error
563
+ */
564
+ extractProblematicText(query, errorMessage) {
565
+ // If we found a position, extract surrounding text
566
+ const position = this.extractErrorPosition(errorMessage);
567
+ if (position !== undefined && position < query.length) {
568
+ const start = Math.max(0, position - 10);
569
+ const end = Math.min(query.length, position + 10);
570
+ return query.substring(start, end);
571
+ }
572
+ // Try to find quoted text in error message
573
+ const quotedMatch = errorMessage.match(/"([^"]+)"/);
574
+ if (quotedMatch) {
575
+ return quotedMatch[1];
576
+ }
577
+ return undefined;
578
+ }
579
+ /**
580
+ * Validate fields against the provided schema
581
+ */
582
+ validateFields(referencedFields, schema) {
583
+ const schemaFields = Object.keys(schema);
584
+ const fields = [];
585
+ const unknownFields = [];
586
+ let allValid = true;
587
+ for (const field of referencedFields) {
588
+ if (field in schema) {
589
+ // Field exists in schema
590
+ fields.push({
591
+ field,
592
+ valid: true,
593
+ expectedType: schema[field].type,
594
+ allowedValues: schema[field].allowedValues
595
+ });
596
+ }
597
+ else {
598
+ // Field not in schema - try to find a suggestion
599
+ const suggestion = this.findSimilarField(field, schemaFields);
600
+ fields.push({
601
+ field,
602
+ valid: false,
603
+ reason: 'unknown_field',
604
+ suggestion
605
+ });
606
+ unknownFields.push(field);
607
+ allValid = false;
608
+ }
609
+ }
610
+ return {
611
+ valid: allValid,
612
+ fields,
613
+ unknownFields
614
+ };
615
+ }
616
+ /**
617
+ * Find a similar field name (for typo suggestions)
618
+ */
619
+ findSimilarField(field, schemaFields) {
620
+ const fieldLower = field.toLowerCase();
621
+ // First, try exact case-insensitive match
622
+ for (const schemaField of schemaFields) {
623
+ if (schemaField.toLowerCase() === fieldLower) {
624
+ return schemaField;
625
+ }
626
+ }
627
+ // Then, try to find fields that start with the same prefix
628
+ const prefix = fieldLower.substring(0, Math.min(3, fieldLower.length));
629
+ for (const schemaField of schemaFields) {
630
+ if (schemaField.toLowerCase().startsWith(prefix)) {
631
+ return schemaField;
632
+ }
633
+ }
634
+ // Try Levenshtein distance for short fields
635
+ if (field.length <= 10) {
636
+ let bestMatch;
637
+ let bestDistance = Infinity;
638
+ for (const schemaField of schemaFields) {
639
+ const distance = this.levenshteinDistance(fieldLower, schemaField.toLowerCase());
640
+ if (distance <= 2 && distance < bestDistance) {
641
+ bestDistance = distance;
642
+ bestMatch = schemaField;
643
+ }
644
+ }
645
+ if (bestMatch) {
646
+ return bestMatch;
647
+ }
648
+ }
649
+ return undefined;
650
+ }
651
+ /**
652
+ * Calculate Levenshtein distance between two strings
653
+ */
654
+ levenshteinDistance(a, b) {
655
+ const matrix = [];
656
+ for (let i = 0; i <= b.length; i++) {
657
+ matrix[i] = [i];
658
+ }
659
+ for (let j = 0; j <= a.length; j++) {
660
+ matrix[0][j] = j;
661
+ }
662
+ for (let i = 1; i <= b.length; i++) {
663
+ for (let j = 1; j <= a.length; j++) {
664
+ if (b.charAt(i - 1) === a.charAt(j - 1)) {
665
+ matrix[i][j] = matrix[i - 1][j - 1];
666
+ }
667
+ else {
668
+ matrix[i][j] = Math.min(matrix[i - 1][j - 1] + 1, // substitution
669
+ matrix[i][j - 1] + 1, // insertion
670
+ matrix[i - 1][j] + 1 // deletion
671
+ );
672
+ }
673
+ }
674
+ }
675
+ return matrix[b.length][a.length];
676
+ }
677
+ /**
678
+ * Perform security pre-check against the provided options
679
+ */
680
+ performSecurityCheck(structure, options) {
681
+ const violations = [];
682
+ const warnings = [];
683
+ // Check denied fields
684
+ if (options.denyFields && options.denyFields.length > 0) {
685
+ for (const field of structure.referencedFields) {
686
+ if (options.denyFields.includes(field)) {
687
+ violations.push({
688
+ type: 'denied_field',
689
+ message: `Field "${field}" is not allowed in queries`,
690
+ field
691
+ });
692
+ }
693
+ }
694
+ }
695
+ // Check allowed fields (if specified, only these fields are allowed)
696
+ if (options.allowedFields && options.allowedFields.length > 0) {
697
+ for (const field of structure.referencedFields) {
698
+ if (!options.allowedFields.includes(field)) {
699
+ violations.push({
700
+ type: 'field_not_allowed',
701
+ message: `Field "${field}" is not in the list of allowed fields`,
702
+ field
703
+ });
704
+ }
705
+ }
706
+ }
707
+ // Check dot notation
708
+ if (options.allowDotNotation === false) {
709
+ for (const field of structure.referencedFields) {
710
+ if (field.includes('.')) {
711
+ violations.push({
712
+ type: 'dot_notation',
713
+ message: `Dot notation is not allowed in field names: "${field}"`,
714
+ field
715
+ });
716
+ }
717
+ }
718
+ }
719
+ // Check query depth
720
+ if (options.maxQueryDepth !== undefined) {
721
+ if (structure.depth > options.maxQueryDepth) {
722
+ violations.push({
723
+ type: 'depth_exceeded',
724
+ message: `Query depth (${structure.depth}) exceeds maximum allowed (${options.maxQueryDepth})`
725
+ });
726
+ }
727
+ else if (structure.depth >= options.maxQueryDepth * 0.8) {
728
+ warnings.push({
729
+ type: 'approaching_depth_limit',
730
+ message: `Query depth (${structure.depth}) is approaching the limit (${options.maxQueryDepth})`,
731
+ current: structure.depth,
732
+ limit: options.maxQueryDepth
733
+ });
734
+ }
735
+ }
736
+ // Check clause count
737
+ if (options.maxClauseCount !== undefined) {
738
+ if (structure.clauseCount > options.maxClauseCount) {
739
+ violations.push({
740
+ type: 'clause_limit',
741
+ message: `Clause count (${structure.clauseCount}) exceeds maximum allowed (${options.maxClauseCount})`
742
+ });
743
+ }
744
+ else if (structure.clauseCount >= options.maxClauseCount * 0.8) {
745
+ warnings.push({
746
+ type: 'approaching_clause_limit',
747
+ message: `Clause count (${structure.clauseCount}) is approaching the limit (${options.maxClauseCount})`,
748
+ current: structure.clauseCount,
749
+ limit: options.maxClauseCount
750
+ });
751
+ }
752
+ }
753
+ // Add complexity warning
754
+ if (structure.complexity === 'complex') {
755
+ warnings.push({
756
+ type: 'complex_query',
757
+ message: 'This query is complex and may impact performance'
758
+ });
759
+ }
760
+ return {
761
+ passed: violations.length === 0,
762
+ violations,
763
+ warnings
764
+ };
765
+ }
766
+ /**
767
+ * Generate autocomplete suggestions based on cursor position
768
+ */
769
+ generateAutocompleteSuggestions(query, cursorPosition, activeToken, schema) {
770
+ // Determine context based on active token and position
771
+ if (!activeToken) {
772
+ // Cursor is not in a token - check if we're at the start or between tokens
773
+ if (query.trim().length === 0 || cursorPosition === 0) {
774
+ return this.suggestForEmptyContext(schema);
775
+ }
776
+ // Between tokens - suggest logical operators or new field
777
+ return this.suggestBetweenTokens(schema);
778
+ }
779
+ if (activeToken.type === 'operator') {
780
+ // Cursor is in a logical operator
781
+ return {
782
+ context: 'logical_operator',
783
+ logicalOperators: ['AND', 'OR', 'NOT'],
784
+ replaceText: activeToken.raw,
785
+ replaceRange: {
786
+ start: activeToken.startPosition,
787
+ end: activeToken.endPosition
788
+ }
789
+ };
790
+ }
791
+ // Active token is a term
792
+ const term = activeToken;
793
+ const relativePos = cursorPosition - term.startPosition;
794
+ // Determine if cursor is in key, operator, or value part
795
+ if (term.key !== null && term.operator !== null) {
796
+ const keyLength = term.key.length;
797
+ const operatorLength = term.operator.length;
798
+ if (relativePos < keyLength) {
799
+ // Cursor is in the field name
800
+ return this.suggestFields(term.key, schema);
801
+ }
802
+ else if (relativePos < keyLength + operatorLength) {
803
+ // Cursor is in the operator
804
+ return this.suggestOperators(term.key, schema);
805
+ }
806
+ else {
807
+ // Cursor is in the value
808
+ return this.suggestValues(term.key, term.value, schema);
809
+ }
810
+ }
811
+ else if (term.key !== null) {
812
+ // Only key present (incomplete)
813
+ return this.suggestFields(term.key, schema);
814
+ }
815
+ else {
816
+ // Bare value - could be a field name
817
+ return this.suggestFields(term.value || '', schema);
818
+ }
819
+ }
820
+ /**
821
+ * Suggest for empty/start context
822
+ */
823
+ suggestForEmptyContext(schema) {
824
+ return {
825
+ context: 'empty',
826
+ fields: this.getFieldSuggestions('', schema),
827
+ logicalOperators: ['NOT']
828
+ };
829
+ }
830
+ /**
831
+ * Suggest between tokens (after a complete term)
832
+ */
833
+ suggestBetweenTokens(schema) {
834
+ return {
835
+ context: 'logical_operator',
836
+ fields: this.getFieldSuggestions('', schema),
837
+ logicalOperators: ['AND', 'OR', 'NOT']
838
+ };
839
+ }
840
+ /**
841
+ * Suggest field names
842
+ */
843
+ suggestFields(partial, schema) {
844
+ return {
845
+ context: 'field',
846
+ fields: this.getFieldSuggestions(partial, schema),
847
+ replaceText: partial,
848
+ replaceRange: partial.length > 0 ? { start: 0, end: partial.length } : undefined
849
+ };
850
+ }
851
+ /**
852
+ * Get field suggestions based on partial input
853
+ */
854
+ getFieldSuggestions(partial, schema) {
855
+ if (!schema) {
856
+ return [];
857
+ }
858
+ const partialLower = partial.toLowerCase();
859
+ const suggestions = [];
860
+ for (const [field, fieldSchema] of Object.entries(schema)) {
861
+ const fieldLower = field.toLowerCase();
862
+ let score = 0;
863
+ if (partial.length === 0) {
864
+ // No partial - suggest all fields with base score
865
+ score = 50;
866
+ }
867
+ else if (fieldLower === partialLower) {
868
+ // Exact match
869
+ score = 100;
870
+ }
871
+ else if (fieldLower.startsWith(partialLower)) {
872
+ // Prefix match
873
+ score = 80 + (partial.length / field.length) * 20;
874
+ }
875
+ else if (fieldLower.includes(partialLower)) {
876
+ // Contains match
877
+ score = 60;
878
+ }
879
+ else {
880
+ // Check Levenshtein distance for typos
881
+ const distance = this.levenshteinDistance(partialLower, fieldLower);
882
+ if (distance <= 2) {
883
+ score = 40 - distance * 10;
884
+ }
885
+ }
886
+ if (score > 0) {
887
+ suggestions.push({
888
+ field,
889
+ type: fieldSchema.type,
890
+ description: fieldSchema.description,
891
+ score
892
+ });
893
+ }
894
+ }
895
+ // Sort by score descending
896
+ return suggestions.sort((a, b) => b.score - a.score);
897
+ }
898
+ /**
899
+ * Suggest operators
900
+ */
901
+ suggestOperators(field, schema) {
902
+ const fieldType = schema?.[field]?.type;
903
+ const operators = this.getOperatorSuggestions(fieldType);
904
+ return {
905
+ context: 'operator',
906
+ currentField: field,
907
+ operators
908
+ };
909
+ }
910
+ /**
911
+ * Get operator suggestions based on field type
912
+ */
913
+ getOperatorSuggestions(fieldType) {
914
+ const allOperators = [
915
+ { operator: ':', description: 'equals', applicable: true },
916
+ { operator: ':!=', description: 'not equals', applicable: true },
917
+ {
918
+ operator: ':>',
919
+ description: 'greater than',
920
+ applicable: fieldType === 'number' || fieldType === 'date'
921
+ },
922
+ {
923
+ operator: ':>=',
924
+ description: 'greater than or equal',
925
+ applicable: fieldType === 'number' || fieldType === 'date'
926
+ },
927
+ {
928
+ operator: ':<',
929
+ description: 'less than',
930
+ applicable: fieldType === 'number' || fieldType === 'date'
931
+ },
932
+ {
933
+ operator: ':<=',
934
+ description: 'less than or equal',
935
+ applicable: fieldType === 'number' || fieldType === 'date'
936
+ }
937
+ ];
938
+ // Sort applicable operators first
939
+ return allOperators.sort((a, b) => {
940
+ if (a.applicable && !b.applicable)
941
+ return -1;
942
+ if (!a.applicable && b.applicable)
943
+ return 1;
944
+ return 0;
945
+ });
946
+ }
947
+ /**
948
+ * Suggest values
949
+ */
950
+ suggestValues(field, partialValue, schema) {
951
+ const fieldSchema = schema?.[field];
952
+ const values = this.getValueSuggestions(partialValue || '', fieldSchema);
953
+ return {
954
+ context: 'value',
955
+ currentField: field,
956
+ values,
957
+ replaceText: partialValue || undefined
958
+ };
959
+ }
960
+ /**
961
+ * Get value suggestions based on schema
962
+ */
963
+ getValueSuggestions(partial, fieldSchema) {
964
+ if (!fieldSchema?.allowedValues) {
965
+ // Suggest based on type
966
+ if (fieldSchema?.type === 'boolean') {
967
+ return [
968
+ { value: true, label: 'true', score: 100 },
969
+ { value: false, label: 'false', score: 100 }
970
+ ];
971
+ }
972
+ return [];
973
+ }
974
+ const partialLower = partial.toLowerCase();
975
+ const suggestions = [];
976
+ for (const value of fieldSchema.allowedValues) {
977
+ const valueStr = String(value).toLowerCase();
978
+ let score = 0;
979
+ if (partial.length === 0) {
980
+ score = 50;
981
+ }
982
+ else if (valueStr === partialLower) {
983
+ score = 100;
984
+ }
985
+ else if (valueStr.startsWith(partialLower)) {
986
+ score = 80;
987
+ }
988
+ else if (valueStr.includes(partialLower)) {
989
+ score = 60;
990
+ }
991
+ if (score > 0) {
992
+ suggestions.push({
993
+ value,
994
+ label: String(value),
995
+ score
996
+ });
997
+ }
998
+ }
999
+ return suggestions.sort((a, b) => b.score - a.score);
1000
+ }
1001
+ /**
1002
+ * Generate error recovery suggestions
1003
+ */
1004
+ generateErrorRecovery(query, structure) {
1005
+ // Check for unclosed quotes
1006
+ const singleQuotes = (query.match(/'/g) || []).length;
1007
+ const doubleQuotes = (query.match(/"/g) || []).length;
1008
+ if (singleQuotes % 2 !== 0) {
1009
+ const lastQuotePos = query.lastIndexOf("'");
1010
+ return {
1011
+ issue: 'unclosed_quote',
1012
+ message: 'Unclosed single quote detected',
1013
+ suggestion: "Add a closing ' to complete the quoted value",
1014
+ autofix: query + "'",
1015
+ position: lastQuotePos
1016
+ };
1017
+ }
1018
+ if (doubleQuotes % 2 !== 0) {
1019
+ const lastQuotePos = query.lastIndexOf('"');
1020
+ return {
1021
+ issue: 'unclosed_quote',
1022
+ message: 'Unclosed double quote detected',
1023
+ suggestion: 'Add a closing " to complete the quoted value',
1024
+ autofix: query + '"',
1025
+ position: lastQuotePos
1026
+ };
1027
+ }
1028
+ // Check for unbalanced parentheses
1029
+ if (!structure.hasBalancedParentheses) {
1030
+ const openCount = (query.match(/\(/g) || []).length;
1031
+ const closeCount = (query.match(/\)/g) || []).length;
1032
+ if (openCount > closeCount) {
1033
+ return {
1034
+ issue: 'unclosed_parenthesis',
1035
+ message: `Missing ${openCount - closeCount} closing parenthesis`,
1036
+ suggestion: 'Add closing parenthesis to balance the expression',
1037
+ autofix: query + ')'.repeat(openCount - closeCount)
1038
+ };
1039
+ }
1040
+ else {
1041
+ return {
1042
+ issue: 'unclosed_parenthesis',
1043
+ message: `Extra ${closeCount - openCount} closing parenthesis`,
1044
+ suggestion: 'Remove extra closing parenthesis'
1045
+ };
1046
+ }
1047
+ }
1048
+ // Check for trailing operator
1049
+ const trimmed = query.trim();
1050
+ if (/\b(AND|OR|NOT)\s*$/i.test(trimmed)) {
1051
+ const match = trimmed.match(/\b(AND|OR|NOT)\s*$/i);
1052
+ return {
1053
+ issue: 'trailing_operator',
1054
+ message: `Query ends with incomplete "${match?.[1]}" operator`,
1055
+ suggestion: 'Add a condition after the operator or remove it',
1056
+ autofix: trimmed.replace(/\s*(AND|OR|NOT)\s*$/i, '').trim()
1057
+ };
1058
+ }
1059
+ // Check for missing value (field:)
1060
+ if (/:$/.test(trimmed) || /:\s*$/.test(trimmed)) {
1061
+ return {
1062
+ issue: 'missing_value',
1063
+ message: 'Field is missing a value',
1064
+ suggestion: 'Add a value after the colon'
1065
+ };
1066
+ }
1067
+ // Generic syntax error
1068
+ return {
1069
+ issue: 'syntax_error',
1070
+ message: 'Query contains a syntax error',
1071
+ suggestion: 'Check the query syntax and try again'
1072
+ };
1073
+ }
358
1074
  }
359
1075
  exports.QueryParser = QueryParser;