search-input-query-parser 0.5.1 → 0.6.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.
@@ -86,6 +86,27 @@ const fieldValueToSql = (field, value, state) => {
86
86
  // Rest of the function remains the same...
87
87
  switch (schema === null || schema === void 0 ? void 0 : schema.type) {
88
88
  case "date":
89
+ // Handle year format (YYYY)
90
+ if (/^\d{4}$/.test(cleanedValue)) {
91
+ const year = cleanedValue;
92
+ const [param1, state1] = nextParam(state);
93
+ const [param2, state2] = nextParam(state1);
94
+ return [
95
+ `${field} BETWEEN ${param1} AND ${param2}`,
96
+ addValue(addValue(state2, `${year}-01-01`), `${year}-12-31`),
97
+ ];
98
+ }
99
+ // Handle year-month format (YYYY-MM)
100
+ if (/^\d{4}-\d{2}$/.test(cleanedValue)) {
101
+ const [year, month] = cleanedValue.split('-');
102
+ const lastDay = new Date(Number(year), Number(month), 0).getDate();
103
+ const [param1, state1] = nextParam(state);
104
+ const [param2, state2] = nextParam(state1);
105
+ return [
106
+ `${field} BETWEEN ${param1} AND ${param2}`,
107
+ addValue(addValue(state2, `${year}-${month}-01`), `${year}-${month}-${lastDay}`),
108
+ ];
109
+ }
89
110
  return [
90
111
  `${field} = ${paramName}`,
91
112
  addValue(newState, cleanedValue),
@@ -120,10 +141,36 @@ const rangeToSql = (field, operator, value, value2, state) => {
120
141
  ];
121
142
  }
122
143
  const [paramName, newState] = nextParam(state);
123
- const val = isDateField ? value : Number(value);
144
+ let val = value;
145
+ // Handle date shorthand formats in comparison operators
146
+ if (isDateField) {
147
+ // Year format (YYYY)
148
+ if (/^\d{4}$/.test(value)) {
149
+ if (operator === ">" || operator === ">=") {
150
+ val = `${value}-01-01`;
151
+ }
152
+ else if (operator === "<" || operator === "<=") {
153
+ val = `${value}-12-31`;
154
+ }
155
+ }
156
+ // Month format (YYYY-MM)
157
+ else if (/^\d{4}-\d{2}$/.test(value)) {
158
+ const [year, month] = value.split('-');
159
+ if (operator === ">" || operator === ">=") {
160
+ val = `${year}-${month}-01`;
161
+ }
162
+ else if (operator === "<" || operator === "<=") {
163
+ const lastDay = new Date(Number(year), Number(month), 0).getDate();
164
+ val = `${year}-${month}-${lastDay}`;
165
+ }
166
+ }
167
+ }
168
+ else {
169
+ val = value; // Keep as string, will be converted to number later
170
+ }
124
171
  return [
125
172
  `${field} ${operator} ${paramName}`,
126
- addValue(newState, val),
173
+ addValue(newState, isDateField ? val : Number(val)),
127
174
  ];
128
175
  };
129
176
  const inExpressionToSql = (field, values, state) => {
@@ -109,6 +109,27 @@ const fieldValueToSql = (field, value, state) => {
109
109
  const baseValue = hasWildcard ? cleanedValue.slice(0, -1) : cleanedValue;
110
110
  switch (schema === null || schema === void 0 ? void 0 : schema.type) {
111
111
  case "date": {
112
+ // Handle year format (YYYY)
113
+ if (/^\d{4}$/.test(cleanedValue)) {
114
+ const year = cleanedValue;
115
+ const [param1, state1] = nextParam(state);
116
+ const [param2, state2] = nextParam(state1);
117
+ return [
118
+ `${field} @@@ '[' || ${param1} || ' TO ' || ${param2} || ']'`,
119
+ addValue(addValue(state2, `${year}-01-01`), `${year}-12-31`),
120
+ ];
121
+ }
122
+ // Handle year-month format (YYYY-MM)
123
+ if (/^\d{4}-\d{2}$/.test(cleanedValue)) {
124
+ const [year, month] = cleanedValue.split('-');
125
+ const lastDay = new Date(Number(year), Number(month), 0).getDate();
126
+ const [param1, state1] = nextParam(state);
127
+ const [param2, state2] = nextParam(state1);
128
+ return [
129
+ `${field} @@@ '[' || ${param1} || ' TO ' || ${param2} || ']'`,
130
+ addValue(addValue(state2, `${year}-${month}-01`), `${year}-${month}-${lastDay}`),
131
+ ];
132
+ }
112
133
  // Use parameter binding for dates
113
134
  const [dateParam, dateState] = nextParam(state);
114
135
  return [
@@ -142,11 +163,37 @@ const rangeToSql = (field, operator, value, value2, state) => {
142
163
  }
143
164
  else {
144
165
  const [paramName, newState] = nextParam(state);
166
+ let val = value;
167
+ // Handle date shorthand formats in comparison operators
168
+ if (isDateField) {
169
+ // Year format (YYYY)
170
+ if (/^\d{4}$/.test(value)) {
171
+ if (operator === ">" || operator === ">=") {
172
+ val = `${value}-01-01`;
173
+ }
174
+ else if (operator === "<" || operator === "<=") {
175
+ val = `${value}-12-31`;
176
+ }
177
+ }
178
+ // Month format (YYYY-MM)
179
+ else if (/^\d{4}-\d{2}$/.test(value)) {
180
+ const [year, month] = value.split('-');
181
+ if (operator === ">" || operator === ">=") {
182
+ val = `${year}-${month}-01`;
183
+ }
184
+ else if (operator === "<" || operator === "<=") {
185
+ const lastDay = new Date(Number(year), Number(month), 0).getDate();
186
+ val = `${year}-${month}-${lastDay}`;
187
+ }
188
+ }
189
+ }
190
+ else {
191
+ val = value; // Keep as string, will be converted to number later
192
+ }
145
193
  const rangeOp = operator.replace(">=", ">=").replace("<=", "<=");
146
- const val = isDateField ? value : Number(value);
147
194
  return [
148
195
  `${field} @@@ '${rangeOp}' || ${paramName}`,
149
- addValue(newState, val),
196
+ addValue(newState, isDateField ? val : Number(val)),
150
197
  ];
151
198
  }
152
199
  };
@@ -78,6 +78,27 @@ const fieldValueToSql = (field, value, state) => {
78
78
  // Rest of the function remains the same...
79
79
  switch (schema === null || schema === void 0 ? void 0 : schema.type) {
80
80
  case "date":
81
+ // Handle year format (YYYY)
82
+ if (/^\d{4}$/.test(cleanedValue)) {
83
+ const year = cleanedValue;
84
+ const [param1, state1] = nextParam(state);
85
+ const [param2, state2] = nextParam(state1);
86
+ return [
87
+ `${field} BETWEEN ${param1} AND ${param2}`,
88
+ addValue(addValue(state2, `${year}-01-01`), `${year}-12-31`),
89
+ ];
90
+ }
91
+ // Handle year-month format (YYYY-MM)
92
+ if (/^\d{4}-\d{2}$/.test(cleanedValue)) {
93
+ const [year, month] = cleanedValue.split('-');
94
+ const lastDay = new Date(Number(year), Number(month), 0).getDate();
95
+ const [param1, state1] = nextParam(state);
96
+ const [param2, state2] = nextParam(state1);
97
+ return [
98
+ `${field} BETWEEN ${param1} AND ${param2}`,
99
+ addValue(addValue(state2, `${year}-${month}-01`), `${year}-${month}-${lastDay}`),
100
+ ];
101
+ }
81
102
  return [
82
103
  `${field} = ${paramName}`,
83
104
  addValue(newState, cleanedValue),
@@ -112,10 +133,36 @@ const rangeToSql = (field, operator, value, value2, state) => {
112
133
  ];
113
134
  }
114
135
  const [paramName, newState] = nextParam(state);
115
- const val = isDateField ? value : Number(value);
136
+ let val = value;
137
+ // Handle date shorthand formats in comparison operators
138
+ if (isDateField) {
139
+ // Year format (YYYY)
140
+ if (/^\d{4}$/.test(value)) {
141
+ if (operator === ">" || operator === ">=") {
142
+ val = `${value}-01-01`;
143
+ }
144
+ else if (operator === "<" || operator === "<=") {
145
+ val = `${value}-12-31`;
146
+ }
147
+ }
148
+ // Month format (YYYY-MM)
149
+ else if (/^\d{4}-\d{2}$/.test(value)) {
150
+ const [year, month] = value.split('-');
151
+ if (operator === ">" || operator === ">=") {
152
+ val = `${year}-${month}-01`;
153
+ }
154
+ else if (operator === "<" || operator === "<=") {
155
+ const lastDay = new Date(Number(year), Number(month), 0).getDate();
156
+ val = `${year}-${month}-${lastDay}`;
157
+ }
158
+ }
159
+ }
160
+ else {
161
+ val = value; // Keep as string, will be converted to number later
162
+ }
116
163
  return [
117
164
  `${field} ${operator} ${paramName}`,
118
- addValue(newState, val),
165
+ addValue(newState, isDateField ? val : Number(val)),
119
166
  ];
120
167
  };
121
168
  const inExpressionToSql = (field, values, state) => {
@@ -208,6 +208,17 @@ const validateFieldValue = (expr, allowedFields, errors, schemas) => {
208
208
  const dateValidator = (dateStr) => {
209
209
  if (!dateStr)
210
210
  return true;
211
+ // Check for year format (YYYY)
212
+ if (/^\d{4}$/.test(dateStr)) {
213
+ return true;
214
+ }
215
+ // Check for year-month format (YYYY-MM)
216
+ if (/^\d{4}-\d{2}$/.test(dateStr)) {
217
+ const [year, month] = dateStr.split('-').map(Number);
218
+ // Validate month is between 1-12
219
+ return month >= 1 && month <= 12;
220
+ }
221
+ // Check for full date format (YYYY-MM-DD)
211
222
  if (!/^\d{4}-\d{2}-\d{2}$/.test(dateStr)) {
212
223
  return false;
213
224
  }
@@ -215,6 +226,7 @@ const validateFieldValue = (expr, allowedFields, errors, schemas) => {
215
226
  return (!isNaN(date.getTime()) &&
216
227
  dateStr === date.toISOString().split("T")[0]);
217
228
  };
229
+ // Handle date range with shorthand formats
218
230
  if (value.includes("..")) {
219
231
  const [start, end] = value.split("..");
220
232
  if (!dateValidator(start) || !dateValidator(end)) {
@@ -83,6 +83,27 @@ const fieldValueToSql = (field, value, state) => {
83
83
  // Rest of the function remains the same...
84
84
  switch (schema === null || schema === void 0 ? void 0 : schema.type) {
85
85
  case "date":
86
+ // Handle year format (YYYY)
87
+ if (/^\d{4}$/.test(cleanedValue)) {
88
+ const year = cleanedValue;
89
+ const [param1, state1] = nextParam(state);
90
+ const [param2, state2] = nextParam(state1);
91
+ return [
92
+ `${field} BETWEEN ${param1} AND ${param2}`,
93
+ addValue(addValue(state2, `${year}-01-01`), `${year}-12-31`),
94
+ ];
95
+ }
96
+ // Handle year-month format (YYYY-MM)
97
+ if (/^\d{4}-\d{2}$/.test(cleanedValue)) {
98
+ const [year, month] = cleanedValue.split('-');
99
+ const lastDay = new Date(Number(year), Number(month), 0).getDate();
100
+ const [param1, state1] = nextParam(state);
101
+ const [param2, state2] = nextParam(state1);
102
+ return [
103
+ `${field} BETWEEN ${param1} AND ${param2}`,
104
+ addValue(addValue(state2, `${year}-${month}-01`), `${year}-${month}-${lastDay}`),
105
+ ];
106
+ }
86
107
  return [
87
108
  `${field} = ${paramName}`,
88
109
  addValue(newState, cleanedValue),
@@ -117,10 +138,36 @@ const rangeToSql = (field, operator, value, value2, state) => {
117
138
  ];
118
139
  }
119
140
  const [paramName, newState] = nextParam(state);
120
- const val = isDateField ? value : Number(value);
141
+ let val = value;
142
+ // Handle date shorthand formats in comparison operators
143
+ if (isDateField) {
144
+ // Year format (YYYY)
145
+ if (/^\d{4}$/.test(value)) {
146
+ if (operator === ">" || operator === ">=") {
147
+ val = `${value}-01-01`;
148
+ }
149
+ else if (operator === "<" || operator === "<=") {
150
+ val = `${value}-12-31`;
151
+ }
152
+ }
153
+ // Month format (YYYY-MM)
154
+ else if (/^\d{4}-\d{2}$/.test(value)) {
155
+ const [year, month] = value.split('-');
156
+ if (operator === ">" || operator === ">=") {
157
+ val = `${year}-${month}-01`;
158
+ }
159
+ else if (operator === "<" || operator === "<=") {
160
+ const lastDay = new Date(Number(year), Number(month), 0).getDate();
161
+ val = `${year}-${month}-${lastDay}`;
162
+ }
163
+ }
164
+ }
165
+ else {
166
+ val = value; // Keep as string, will be converted to number later
167
+ }
121
168
  return [
122
169
  `${field} ${operator} ${paramName}`,
123
- addValue(newState, val),
170
+ addValue(newState, isDateField ? val : Number(val)),
124
171
  ];
125
172
  };
126
173
  const inExpressionToSql = (field, values, state) => {
@@ -106,6 +106,27 @@ const fieldValueToSql = (field, value, state) => {
106
106
  const baseValue = hasWildcard ? cleanedValue.slice(0, -1) : cleanedValue;
107
107
  switch (schema === null || schema === void 0 ? void 0 : schema.type) {
108
108
  case "date": {
109
+ // Handle year format (YYYY)
110
+ if (/^\d{4}$/.test(cleanedValue)) {
111
+ const year = cleanedValue;
112
+ const [param1, state1] = nextParam(state);
113
+ const [param2, state2] = nextParam(state1);
114
+ return [
115
+ `${field} @@@ '[' || ${param1} || ' TO ' || ${param2} || ']'`,
116
+ addValue(addValue(state2, `${year}-01-01`), `${year}-12-31`),
117
+ ];
118
+ }
119
+ // Handle year-month format (YYYY-MM)
120
+ if (/^\d{4}-\d{2}$/.test(cleanedValue)) {
121
+ const [year, month] = cleanedValue.split('-');
122
+ const lastDay = new Date(Number(year), Number(month), 0).getDate();
123
+ const [param1, state1] = nextParam(state);
124
+ const [param2, state2] = nextParam(state1);
125
+ return [
126
+ `${field} @@@ '[' || ${param1} || ' TO ' || ${param2} || ']'`,
127
+ addValue(addValue(state2, `${year}-${month}-01`), `${year}-${month}-${lastDay}`),
128
+ ];
129
+ }
109
130
  // Use parameter binding for dates
110
131
  const [dateParam, dateState] = nextParam(state);
111
132
  return [
@@ -139,11 +160,37 @@ const rangeToSql = (field, operator, value, value2, state) => {
139
160
  }
140
161
  else {
141
162
  const [paramName, newState] = nextParam(state);
163
+ let val = value;
164
+ // Handle date shorthand formats in comparison operators
165
+ if (isDateField) {
166
+ // Year format (YYYY)
167
+ if (/^\d{4}$/.test(value)) {
168
+ if (operator === ">" || operator === ">=") {
169
+ val = `${value}-01-01`;
170
+ }
171
+ else if (operator === "<" || operator === "<=") {
172
+ val = `${value}-12-31`;
173
+ }
174
+ }
175
+ // Month format (YYYY-MM)
176
+ else if (/^\d{4}-\d{2}$/.test(value)) {
177
+ const [year, month] = value.split('-');
178
+ if (operator === ">" || operator === ">=") {
179
+ val = `${year}-${month}-01`;
180
+ }
181
+ else if (operator === "<" || operator === "<=") {
182
+ const lastDay = new Date(Number(year), Number(month), 0).getDate();
183
+ val = `${year}-${month}-${lastDay}`;
184
+ }
185
+ }
186
+ }
187
+ else {
188
+ val = value; // Keep as string, will be converted to number later
189
+ }
142
190
  const rangeOp = operator.replace(">=", ">=").replace("<=", "<=");
143
- const val = isDateField ? value : Number(value);
144
191
  return [
145
192
  `${field} @@@ '${rangeOp}' || ${paramName}`,
146
- addValue(newState, val),
193
+ addValue(newState, isDateField ? val : Number(val)),
147
194
  ];
148
195
  }
149
196
  };
@@ -75,6 +75,27 @@ const fieldValueToSql = (field, value, state) => {
75
75
  // Rest of the function remains the same...
76
76
  switch (schema === null || schema === void 0 ? void 0 : schema.type) {
77
77
  case "date":
78
+ // Handle year format (YYYY)
79
+ if (/^\d{4}$/.test(cleanedValue)) {
80
+ const year = cleanedValue;
81
+ const [param1, state1] = nextParam(state);
82
+ const [param2, state2] = nextParam(state1);
83
+ return [
84
+ `${field} BETWEEN ${param1} AND ${param2}`,
85
+ addValue(addValue(state2, `${year}-01-01`), `${year}-12-31`),
86
+ ];
87
+ }
88
+ // Handle year-month format (YYYY-MM)
89
+ if (/^\d{4}-\d{2}$/.test(cleanedValue)) {
90
+ const [year, month] = cleanedValue.split('-');
91
+ const lastDay = new Date(Number(year), Number(month), 0).getDate();
92
+ const [param1, state1] = nextParam(state);
93
+ const [param2, state2] = nextParam(state1);
94
+ return [
95
+ `${field} BETWEEN ${param1} AND ${param2}`,
96
+ addValue(addValue(state2, `${year}-${month}-01`), `${year}-${month}-${lastDay}`),
97
+ ];
98
+ }
78
99
  return [
79
100
  `${field} = ${paramName}`,
80
101
  addValue(newState, cleanedValue),
@@ -109,10 +130,36 @@ const rangeToSql = (field, operator, value, value2, state) => {
109
130
  ];
110
131
  }
111
132
  const [paramName, newState] = nextParam(state);
112
- const val = isDateField ? value : Number(value);
133
+ let val = value;
134
+ // Handle date shorthand formats in comparison operators
135
+ if (isDateField) {
136
+ // Year format (YYYY)
137
+ if (/^\d{4}$/.test(value)) {
138
+ if (operator === ">" || operator === ">=") {
139
+ val = `${value}-01-01`;
140
+ }
141
+ else if (operator === "<" || operator === "<=") {
142
+ val = `${value}-12-31`;
143
+ }
144
+ }
145
+ // Month format (YYYY-MM)
146
+ else if (/^\d{4}-\d{2}$/.test(value)) {
147
+ const [year, month] = value.split('-');
148
+ if (operator === ">" || operator === ">=") {
149
+ val = `${year}-${month}-01`;
150
+ }
151
+ else if (operator === "<" || operator === "<=") {
152
+ const lastDay = new Date(Number(year), Number(month), 0).getDate();
153
+ val = `${year}-${month}-${lastDay}`;
154
+ }
155
+ }
156
+ }
157
+ else {
158
+ val = value; // Keep as string, will be converted to number later
159
+ }
113
160
  return [
114
161
  `${field} ${operator} ${paramName}`,
115
- addValue(newState, val),
162
+ addValue(newState, isDateField ? val : Number(val)),
116
163
  ];
117
164
  };
118
165
  const inExpressionToSql = (field, values, state) => {
@@ -205,6 +205,17 @@ const validateFieldValue = (expr, allowedFields, errors, schemas) => {
205
205
  const dateValidator = (dateStr) => {
206
206
  if (!dateStr)
207
207
  return true;
208
+ // Check for year format (YYYY)
209
+ if (/^\d{4}$/.test(dateStr)) {
210
+ return true;
211
+ }
212
+ // Check for year-month format (YYYY-MM)
213
+ if (/^\d{4}-\d{2}$/.test(dateStr)) {
214
+ const [year, month] = dateStr.split('-').map(Number);
215
+ // Validate month is between 1-12
216
+ return month >= 1 && month <= 12;
217
+ }
218
+ // Check for full date format (YYYY-MM-DD)
208
219
  if (!/^\d{4}-\d{2}-\d{2}$/.test(dateStr)) {
209
220
  return false;
210
221
  }
@@ -212,6 +223,7 @@ const validateFieldValue = (expr, allowedFields, errors, schemas) => {
212
223
  return (!isNaN(date.getTime()) &&
213
224
  dateStr === date.toISOString().split("T")[0]);
214
225
  };
226
+ // Handle date range with shorthand formats
215
227
  if (value.includes("..")) {
216
228
  const [start, end] = value.split("..");
217
229
  if (!dateValidator(start) || !dateValidator(end)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "search-input-query-parser",
3
- "version": "0.5.1",
3
+ "version": "0.6.0",
4
4
  "description": "A parser for advanced search query syntax with field:value support",
5
5
  "keywords": [
6
6
  "search",
@@ -432,6 +432,44 @@ describe("Search Query Parser", () => {
432
432
  "date:2023-12-31..2024-01-01"
433
433
  );
434
434
  });
435
+
436
+ test("parses date shorthand formats", () => {
437
+ // Year shorthand
438
+ testSchemaQuery(
439
+ "date:2024",
440
+ "date:2024"
441
+ );
442
+
443
+ // Month shorthand
444
+ testSchemaQuery(
445
+ "date:2024-02",
446
+ "date:2024-02"
447
+ );
448
+
449
+ // Shorthand with comparison operators
450
+ testSchemaQuery(
451
+ "date:>=2024",
452
+ "date:>=2024"
453
+ );
454
+
455
+ testSchemaQuery(
456
+ "date:<2024-03",
457
+ "date:<2024-03"
458
+ );
459
+
460
+ // Multiple date shorthands in one query
461
+ testSchemaQuery(
462
+ "date:>=2024 AND date:<2025",
463
+ "(date:>=2024 AND date:<2025)"
464
+ );
465
+
466
+ // Mix of shorthand and full date formats
467
+ testSchemaQuery(
468
+ "date:2024 OR date:2024-01-15",
469
+ "(date:2024 OR date:2024-01-15)"
470
+ );
471
+ });
472
+
435
473
  describe("Wildcard Pattern Support", () => {
436
474
  test("parses simple wildcard patterns", () => {
437
475
  testValidQuery("test*", "test*");
@@ -764,7 +802,6 @@ describe("Search Query Parser", () => {
764
802
  length: 11,
765
803
  },
766
804
  ]);
767
-
768
805
  testSchemaErrorQuery("date:2024-13-01..2024-12-31", [
769
806
  {
770
807
  message: "Invalid date format",
@@ -775,6 +812,48 @@ describe("Search Query Parser", () => {
775
812
  ]);
776
813
  });
777
814
 
815
+ test("validates date shorthand formats", () => {
816
+ // Invalid year format
817
+ testSchemaErrorQuery("date:202", [
818
+ {
819
+ message: "Invalid date format",
820
+ code: SearchQueryErrorCode.VALUE_DATE_FORMAT_INVALID,
821
+ position: 5,
822
+ length: 3,
823
+ },
824
+ ]);
825
+
826
+ // Invalid month format
827
+ testSchemaErrorQuery("date:2024-13", [
828
+ {
829
+ message: "Invalid date format",
830
+ code: SearchQueryErrorCode.VALUE_DATE_FORMAT_INVALID,
831
+ position: 5,
832
+ length: 7,
833
+ },
834
+ ]);
835
+
836
+ // Invalid month value
837
+ testSchemaErrorQuery("date:2024-00", [
838
+ {
839
+ message: "Invalid date format",
840
+ code: SearchQueryErrorCode.VALUE_DATE_FORMAT_INVALID,
841
+ position: 5,
842
+ length: 7,
843
+ },
844
+ ]);
845
+
846
+ // Invalid comparison with shorthand
847
+ testSchemaErrorQuery("date:>invalid-year", [
848
+ {
849
+ message: "Invalid date format",
850
+ code: SearchQueryErrorCode.VALUE_DATE_FORMAT_INVALID,
851
+ position: 5,
852
+ length: 13,
853
+ },
854
+ ]);
855
+ });
856
+
778
857
  test("handles invalid boolean values", () => {
779
858
  testSchemaErrorQuery("in_stock:maybe", [
780
859
  {
@@ -148,6 +148,29 @@ const fieldValueToSql = (
148
148
  // Rest of the function remains the same...
149
149
  switch (schema?.type) {
150
150
  case "date":
151
+ // Handle year format (YYYY)
152
+ if (/^\d{4}$/.test(cleanedValue)) {
153
+ const year = cleanedValue;
154
+ const [param1, state1] = nextParam(state);
155
+ const [param2, state2] = nextParam(state1);
156
+ return [
157
+ `${field} BETWEEN ${param1} AND ${param2}`,
158
+ addValue(addValue(state2, `${year}-01-01`), `${year}-12-31`),
159
+ ];
160
+ }
161
+
162
+ // Handle year-month format (YYYY-MM)
163
+ if (/^\d{4}-\d{2}$/.test(cleanedValue)) {
164
+ const [year, month] = cleanedValue.split('-');
165
+ const lastDay = new Date(Number(year), Number(month), 0).getDate();
166
+ const [param1, state1] = nextParam(state);
167
+ const [param2, state2] = nextParam(state1);
168
+ return [
169
+ `${field} BETWEEN ${param1} AND ${param2}`,
170
+ addValue(addValue(state2, `${year}-${month}-01`), `${year}-${month}-${lastDay}`),
171
+ ];
172
+ }
173
+
151
174
  return [
152
175
  `${field} = ${paramName}`,
153
176
  addValue(newState, cleanedValue),
@@ -194,10 +217,35 @@ const rangeToSql = (
194
217
  }
195
218
 
196
219
  const [paramName, newState] = nextParam(state);
197
- const val = isDateField ? value : Number(value);
220
+ let val = value;
221
+
222
+ // Handle date shorthand formats in comparison operators
223
+ if (isDateField) {
224
+ // Year format (YYYY)
225
+ if (/^\d{4}$/.test(value)) {
226
+ if (operator === ">" || operator === ">=") {
227
+ val = `${value}-01-01`;
228
+ } else if (operator === "<" || operator === "<=") {
229
+ val = `${value}-12-31`;
230
+ }
231
+ }
232
+ // Month format (YYYY-MM)
233
+ else if (/^\d{4}-\d{2}$/.test(value)) {
234
+ const [year, month] = value.split('-');
235
+ if (operator === ">" || operator === ">=") {
236
+ val = `${year}-${month}-01`;
237
+ } else if (operator === "<" || operator === "<=") {
238
+ const lastDay = new Date(Number(year), Number(month), 0).getDate();
239
+ val = `${year}-${month}-${lastDay}`;
240
+ }
241
+ }
242
+ } else {
243
+ val = value; // Keep as string, will be converted to number later
244
+ }
245
+
198
246
  return [
199
247
  `${field} ${operator} ${paramName}`,
200
- addValue(newState, val),
248
+ addValue(newState, isDateField ? val : Number(val)),
201
249
  ];
202
250
  };
203
251
 
@@ -0,0 +1,62 @@
1
+ import { describe, expect, test } from "@jest/globals";
2
+ import { searchStringToParadeDbSql } from "./search-query-to-paradedb-sql";
3
+ import { FieldSchema } from "./parser";
4
+
5
+ describe("ParadeDB SQL Converter", () => {
6
+ const schemas: FieldSchema[] = [
7
+ { name: "title", type: "string" },
8
+ { name: "description", type: "string" },
9
+ { name: "content", type: "string" },
10
+ { name: "price", type: "number" },
11
+ { name: "date", type: "date" },
12
+ { name: "in_stock", type: "boolean" },
13
+ ];
14
+
15
+ const searchableColumns = ["title", "description", "content"];
16
+
17
+ const testParadeDBConversion = (
18
+ query: string,
19
+ expectedSql: string,
20
+ expectedValues: any[]
21
+ ) => {
22
+ const result = searchStringToParadeDbSql(
23
+ query,
24
+ searchableColumns,
25
+ schemas
26
+ );
27
+ expect(result.text).toBe(expectedSql);
28
+ expect(result.values).toEqual(expectedValues);
29
+ };
30
+
31
+ describe("ParadeDB Date Handling", () => {
32
+ test("handles date year shorthand format", () => {
33
+ testParadeDBConversion(
34
+ "date:2024",
35
+ "date @@@ '[' || $1 || ' TO ' || $2 || ']'",
36
+ ["2024-01-01", "2024-12-31"]
37
+ );
38
+ });
39
+
40
+ test("handles date month shorthand format", () => {
41
+ testParadeDBConversion(
42
+ "date:2024-02",
43
+ "date @@@ '[' || $1 || ' TO ' || $2 || ']'",
44
+ ["2024-02-01", "2024-02-29"] // 2024 is a leap year
45
+ );
46
+
47
+ testParadeDBConversion(
48
+ "date:2023-04",
49
+ "date @@@ '[' || $1 || ' TO ' || $2 || ']'",
50
+ ["2023-04-01", "2023-04-30"]
51
+ );
52
+ });
53
+
54
+ test("handles date shorthand formats with comparison operators", () => {
55
+ testParadeDBConversion(
56
+ "date:>=2024 AND date:<2025",
57
+ "(date @@@ '>=' || $1 AND date @@@ '<' || $2)",
58
+ ["2024-01-01", "2025-12-31"]
59
+ );
60
+ });
61
+ });
62
+ });
@@ -174,6 +174,29 @@ const fieldValueToSql = (
174
174
 
175
175
  switch (schema?.type) {
176
176
  case "date": {
177
+ // Handle year format (YYYY)
178
+ if (/^\d{4}$/.test(cleanedValue)) {
179
+ const year = cleanedValue;
180
+ const [param1, state1] = nextParam(state);
181
+ const [param2, state2] = nextParam(state1);
182
+ return [
183
+ `${field} @@@ '[' || ${param1} || ' TO ' || ${param2} || ']'`,
184
+ addValue(addValue(state2, `${year}-01-01`), `${year}-12-31`),
185
+ ];
186
+ }
187
+
188
+ // Handle year-month format (YYYY-MM)
189
+ if (/^\d{4}-\d{2}$/.test(cleanedValue)) {
190
+ const [year, month] = cleanedValue.split('-');
191
+ const lastDay = new Date(Number(year), Number(month), 0).getDate();
192
+ const [param1, state1] = nextParam(state);
193
+ const [param2, state2] = nextParam(state1);
194
+ return [
195
+ `${field} @@@ '[' || ${param1} || ' TO ' || ${param2} || ']'`,
196
+ addValue(addValue(state2, `${year}-${month}-01`), `${year}-${month}-${lastDay}`),
197
+ ];
198
+ }
199
+
177
200
  // Use parameter binding for dates
178
201
  const [dateParam, dateState] = nextParam(state);
179
202
  return [
@@ -214,11 +237,36 @@ const rangeToSql = (
214
237
  ];
215
238
  } else {
216
239
  const [paramName, newState] = nextParam(state);
240
+ let val = value;
241
+
242
+ // Handle date shorthand formats in comparison operators
243
+ if (isDateField) {
244
+ // Year format (YYYY)
245
+ if (/^\d{4}$/.test(value)) {
246
+ if (operator === ">" || operator === ">=") {
247
+ val = `${value}-01-01`;
248
+ } else if (operator === "<" || operator === "<=") {
249
+ val = `${value}-12-31`;
250
+ }
251
+ }
252
+ // Month format (YYYY-MM)
253
+ else if (/^\d{4}-\d{2}$/.test(value)) {
254
+ const [year, month] = value.split('-');
255
+ if (operator === ">" || operator === ">=") {
256
+ val = `${year}-${month}-01`;
257
+ } else if (operator === "<" || operator === "<=") {
258
+ const lastDay = new Date(Number(year), Number(month), 0).getDate();
259
+ val = `${year}-${month}-${lastDay}`;
260
+ }
261
+ }
262
+ } else {
263
+ val = value; // Keep as string, will be converted to number later
264
+ }
265
+
217
266
  const rangeOp = operator.replace(">=", ">=").replace("<=", "<=");
218
- const val = isDateField ? value : Number(value);
219
267
  return [
220
268
  `${field} @@@ '${rangeOp}' || ${paramName}`,
221
- addValue(newState, val),
269
+ addValue(newState, isDateField ? val : Number(val)),
222
270
  ];
223
271
  }
224
272
  };
@@ -320,6 +320,36 @@ describe("Search Query to SQL Converter", () => {
320
320
  );
321
321
  testIlikeConversion("amount:<-10", "amount < $1", [-10]);
322
322
  });
323
+
324
+ test("handles date year shorthand format", () => {
325
+ testIlikeConversion(
326
+ "date:2024",
327
+ "date BETWEEN $1 AND $2",
328
+ ["2024-01-01", "2024-12-31"]
329
+ );
330
+ });
331
+
332
+ test("handles date month shorthand format", () => {
333
+ testIlikeConversion(
334
+ "date:2024-02",
335
+ "date BETWEEN $1 AND $2",
336
+ ["2024-02-01", "2024-02-29"] // 2024 is a leap year
337
+ );
338
+
339
+ testIlikeConversion(
340
+ "date:2023-04",
341
+ "date BETWEEN $1 AND $2",
342
+ ["2023-04-01", "2023-04-30"]
343
+ );
344
+ });
345
+
346
+ test("handles date shorthand formats with comparison operators", () => {
347
+ testIlikeConversion(
348
+ "date:>=2024 AND date:<2025",
349
+ "(date >= $1 AND date < $2)",
350
+ ["2024-01-01", "2025-12-31"]
351
+ );
352
+ });
323
353
  });
324
354
 
325
355
  describe("tsvector Search Type", () => {
@@ -373,6 +403,36 @@ describe("Search Query to SQL Converter", () => {
373
403
  ["boots", "winter gear"]
374
404
  );
375
405
  });
406
+
407
+ test("handles date year shorthand format with tsvector", () => {
408
+ testTsvectorConversion(
409
+ "date:2024",
410
+ "date BETWEEN $1 AND $2",
411
+ ["2024-01-01", "2024-12-31"]
412
+ );
413
+ });
414
+
415
+ test("handles date month shorthand format with tsvector", () => {
416
+ testTsvectorConversion(
417
+ "date:2024-02",
418
+ "date BETWEEN $1 AND $2",
419
+ ["2024-02-01", "2024-02-29"] // 2024 is a leap year
420
+ );
421
+
422
+ testTsvectorConversion(
423
+ "date:2023-04",
424
+ "date BETWEEN $1 AND $2",
425
+ ["2023-04-01", "2023-04-30"]
426
+ );
427
+ });
428
+
429
+ test("handles date shorthand formats with comparison operators with tsvector", () => {
430
+ testTsvectorConversion(
431
+ "date:>=2024 AND date:<2025",
432
+ "(date >= $1 AND date < $2)",
433
+ ["2024-01-01", "2025-12-31"]
434
+ );
435
+ });
376
436
  });
377
437
 
378
438
  describe("paradedb Search Type", () => {
@@ -137,6 +137,29 @@ const fieldValueToSql = (
137
137
  // Rest of the function remains the same...
138
138
  switch (schema?.type) {
139
139
  case "date":
140
+ // Handle year format (YYYY)
141
+ if (/^\d{4}$/.test(cleanedValue)) {
142
+ const year = cleanedValue;
143
+ const [param1, state1] = nextParam(state);
144
+ const [param2, state2] = nextParam(state1);
145
+ return [
146
+ `${field} BETWEEN ${param1} AND ${param2}`,
147
+ addValue(addValue(state2, `${year}-01-01`), `${year}-12-31`),
148
+ ];
149
+ }
150
+
151
+ // Handle year-month format (YYYY-MM)
152
+ if (/^\d{4}-\d{2}$/.test(cleanedValue)) {
153
+ const [year, month] = cleanedValue.split('-');
154
+ const lastDay = new Date(Number(year), Number(month), 0).getDate();
155
+ const [param1, state1] = nextParam(state);
156
+ const [param2, state2] = nextParam(state1);
157
+ return [
158
+ `${field} BETWEEN ${param1} AND ${param2}`,
159
+ addValue(addValue(state2, `${year}-${month}-01`), `${year}-${month}-${lastDay}`),
160
+ ];
161
+ }
162
+
140
163
  return [
141
164
  `${field} = ${paramName}`,
142
165
  addValue(newState, cleanedValue),
@@ -182,10 +205,35 @@ const rangeToSql = (
182
205
  }
183
206
 
184
207
  const [paramName, newState] = nextParam(state);
185
- const val = isDateField ? value : Number(value);
208
+ let val = value;
209
+
210
+ // Handle date shorthand formats in comparison operators
211
+ if (isDateField) {
212
+ // Year format (YYYY)
213
+ if (/^\d{4}$/.test(value)) {
214
+ if (operator === ">" || operator === ">=") {
215
+ val = `${value}-01-01`;
216
+ } else if (operator === "<" || operator === "<=") {
217
+ val = `${value}-12-31`;
218
+ }
219
+ }
220
+ // Month format (YYYY-MM)
221
+ else if (/^\d{4}-\d{2}$/.test(value)) {
222
+ const [year, month] = value.split('-');
223
+ if (operator === ">" || operator === ">=") {
224
+ val = `${year}-${month}-01`;
225
+ } else if (operator === "<" || operator === "<=") {
226
+ const lastDay = new Date(Number(year), Number(month), 0).getDate();
227
+ val = `${year}-${month}-${lastDay}`;
228
+ }
229
+ }
230
+ } else {
231
+ val = value; // Keep as string, will be converted to number later
232
+ }
233
+
186
234
  return [
187
235
  `${field} ${operator} ${paramName}`,
188
- addValue(newState, val),
236
+ addValue(newState, isDateField ? val : Number(val)),
189
237
  ];
190
238
  };
191
239
 
@@ -254,6 +254,20 @@ const validateFieldValue = (
254
254
  if (schema.type === "date") {
255
255
  const dateValidator = (dateStr: string) => {
256
256
  if (!dateStr) return true;
257
+
258
+ // Check for year format (YYYY)
259
+ if (/^\d{4}$/.test(dateStr)) {
260
+ return true;
261
+ }
262
+
263
+ // Check for year-month format (YYYY-MM)
264
+ if (/^\d{4}-\d{2}$/.test(dateStr)) {
265
+ const [year, month] = dateStr.split('-').map(Number);
266
+ // Validate month is between 1-12
267
+ return month >= 1 && month <= 12;
268
+ }
269
+
270
+ // Check for full date format (YYYY-MM-DD)
257
271
  if (!/^\d{4}-\d{2}-\d{2}$/.test(dateStr)) {
258
272
  return false;
259
273
  }
@@ -264,6 +278,7 @@ const validateFieldValue = (
264
278
  );
265
279
  };
266
280
 
281
+ // Handle date range with shorthand formats
267
282
  if (value.includes("..")) {
268
283
  const [start, end] = value.split("..");
269
284
  if (!dateValidator(start) || !dateValidator(end)) {