gmail_search_syntax 0.1.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.
- checksums.yaml +7 -0
- data/ARCHITECTURE.md +338 -0
- data/README.md +129 -0
- data/Rakefile +11 -0
- data/SCHEMA.md +223 -0
- data/examples/alias_collision_fix.rb +43 -0
- data/examples/demo.rb +28 -0
- data/examples/gmail_message_id_demo.rb +118 -0
- data/examples/postgres_vs_sqlite.rb +55 -0
- data/examples/sql_query.rb +47 -0
- data/lib/GMAIL_SEARCH_OPERATORS.md +58 -0
- data/lib/gmail_search_syntax/ast.rb +100 -0
- data/lib/gmail_search_syntax/parser.rb +224 -0
- data/lib/gmail_search_syntax/sql_visitor.rb +496 -0
- data/lib/gmail_search_syntax/tokenizer.rb +152 -0
- data/lib/gmail_search_syntax/version.rb +3 -0
- data/lib/gmail_search_syntax.rb +34 -0
- data/test/gmail_search_syntax_test.rb +691 -0
- data/test/integration_test.rb +668 -0
- data/test/postgres_visitor_test.rb +156 -0
- data/test/sql_visitor_test.rb +346 -0
- data/test/test_helper.rb +27 -0
- data/test/tokenizer_test.rb +185 -0
- metadata +115 -0
@@ -0,0 +1,156 @@
|
|
1
|
+
require_relative "test_helper"
|
2
|
+
|
3
|
+
class PostgresVisitorTest < Minitest::Test
|
4
|
+
def parse_and_visit(query, current_user_email: nil)
|
5
|
+
ast = GmailSearchSyntax.parse!(query)
|
6
|
+
visitor = GmailSearchSyntax::PostgresVisitor.new(current_user_email: current_user_email)
|
7
|
+
visitor.visit(ast)
|
8
|
+
visitor.to_query.to_sql
|
9
|
+
end
|
10
|
+
|
11
|
+
def test_from_operator
|
12
|
+
sql, params = parse_and_visit("from:alice@example.com")
|
13
|
+
|
14
|
+
assert_includes sql, "INNER JOIN message_addresses"
|
15
|
+
assert_includes sql, "ma1.address_type = ?"
|
16
|
+
assert_includes sql, "ma1.email_address = ?"
|
17
|
+
assert_equal ["from", "cc", "bcc", "alice@example.com"], params
|
18
|
+
end
|
19
|
+
|
20
|
+
def test_subject_operator
|
21
|
+
sql, params = parse_and_visit("subject:meeting")
|
22
|
+
|
23
|
+
assert_includes sql, "m0.subject LIKE ?"
|
24
|
+
assert_equal ["%meeting%"], params
|
25
|
+
end
|
26
|
+
|
27
|
+
def test_older_than_relative_postgres_syntax
|
28
|
+
sql, params = parse_and_visit("older_than:1y")
|
29
|
+
|
30
|
+
assert_includes sql, "m0.internal_date < (NOW() - ?::interval)"
|
31
|
+
assert_equal ["1 years"], params
|
32
|
+
end
|
33
|
+
|
34
|
+
def test_newer_than_relative_postgres_syntax
|
35
|
+
sql, params = parse_and_visit("newer_than:2d")
|
36
|
+
|
37
|
+
assert_includes sql, "m0.internal_date > (NOW() - ?::interval)"
|
38
|
+
assert_equal ["2 days"], params
|
39
|
+
end
|
40
|
+
|
41
|
+
def test_older_than_7_days_postgres
|
42
|
+
sql, params = parse_and_visit("older_than:7d")
|
43
|
+
|
44
|
+
assert_includes sql, "m0.internal_date < (NOW() - ?::interval)"
|
45
|
+
assert_equal ["7 days"], params
|
46
|
+
end
|
47
|
+
|
48
|
+
def test_newer_than_3_months_postgres
|
49
|
+
sql, params = parse_and_visit("newer_than:3m")
|
50
|
+
|
51
|
+
assert_includes sql, "m0.internal_date > (NOW() - ?::interval)"
|
52
|
+
assert_equal ["3 months"], params
|
53
|
+
end
|
54
|
+
|
55
|
+
def test_older_than_2_years_postgres
|
56
|
+
sql, params = parse_and_visit("older_than:2y")
|
57
|
+
|
58
|
+
assert_includes sql, "m0.internal_date < (NOW() - ?::interval)"
|
59
|
+
assert_equal ["2 years"], params
|
60
|
+
end
|
61
|
+
|
62
|
+
def test_or_operator
|
63
|
+
sql, params = parse_and_visit("from:alice@example.com OR from:bob@example.com")
|
64
|
+
|
65
|
+
assert_includes sql, "OR"
|
66
|
+
assert_includes sql, ".email_address = ?"
|
67
|
+
assert_equal ["from", "cc", "bcc", "alice@example.com", "from", "cc", "bcc", "bob@example.com"], params
|
68
|
+
end
|
69
|
+
|
70
|
+
def test_and_operator
|
71
|
+
sql, params = parse_and_visit("from:alice@example.com AND subject:meeting")
|
72
|
+
|
73
|
+
assert_includes sql, "AND"
|
74
|
+
assert_includes sql, "ma1.email_address = ?"
|
75
|
+
assert_includes sql, "m0.subject LIKE ?"
|
76
|
+
assert_equal ["from", "cc", "bcc", "alice@example.com", "%meeting%"], params
|
77
|
+
end
|
78
|
+
|
79
|
+
def test_not_operator
|
80
|
+
sql, params = parse_and_visit("-from:spam@example.com")
|
81
|
+
|
82
|
+
assert_includes sql, "NOT"
|
83
|
+
assert_includes sql, "ma1.email_address = ?"
|
84
|
+
assert_equal ["from", "cc", "bcc", "spam@example.com"], params
|
85
|
+
end
|
86
|
+
|
87
|
+
def test_complex_query
|
88
|
+
sql, params = parse_and_visit("from:alice@example.com subject:meeting has:attachment")
|
89
|
+
|
90
|
+
assert_includes sql, "ma1.email_address = ?"
|
91
|
+
assert_includes sql, "m0.subject LIKE ?"
|
92
|
+
assert_includes sql, "m0.has_attachment = 1"
|
93
|
+
assert_equal ["from", "cc", "bcc", "alice@example.com", "%meeting%"], params
|
94
|
+
end
|
95
|
+
|
96
|
+
def test_label_operator
|
97
|
+
sql, params = parse_and_visit("label:important")
|
98
|
+
|
99
|
+
assert_includes sql, "INNER JOIN message_labels"
|
100
|
+
assert_includes sql, "INNER JOIN labels"
|
101
|
+
assert_includes sql, ".name = ?"
|
102
|
+
assert_equal ["important"], params
|
103
|
+
end
|
104
|
+
|
105
|
+
def test_is_starred
|
106
|
+
sql, params = parse_and_visit("is:starred")
|
107
|
+
|
108
|
+
assert_includes sql, "m0.is_starred = 1"
|
109
|
+
assert_equal [], params
|
110
|
+
end
|
111
|
+
|
112
|
+
def test_has_attachment
|
113
|
+
sql, params = parse_and_visit("has:attachment")
|
114
|
+
|
115
|
+
assert_includes sql, "m0.has_attachment = 1"
|
116
|
+
assert_equal [], params
|
117
|
+
end
|
118
|
+
|
119
|
+
def test_larger_operator
|
120
|
+
sql, params = parse_and_visit("larger:10M")
|
121
|
+
|
122
|
+
assert_includes sql, "m0.size_bytes > ?"
|
123
|
+
assert_equal [10 * 1024 * 1024], params
|
124
|
+
end
|
125
|
+
|
126
|
+
def test_filename_operator
|
127
|
+
sql, params = parse_and_visit("filename:report.pdf")
|
128
|
+
|
129
|
+
assert_includes sql, "INNER JOIN attachments"
|
130
|
+
assert_includes sql, ".filename = ?"
|
131
|
+
assert_equal ["report.pdf"], params
|
132
|
+
end
|
133
|
+
|
134
|
+
def test_backward_compatibility_with_older_than_and_newer_than
|
135
|
+
# Test that we properly inherit all other behavior from SQLiteVisitor
|
136
|
+
sql1, params1 = parse_and_visit("from:alice@example.com")
|
137
|
+
|
138
|
+
ast = GmailSearchSyntax.parse!("from:alice@example.com")
|
139
|
+
sqlite_visitor = GmailSearchSyntax::SQLiteVisitor.new
|
140
|
+
sqlite_visitor.visit(ast)
|
141
|
+
sql2, params2 = sqlite_visitor.to_query.to_sql
|
142
|
+
|
143
|
+
# Structure should be the same (just the relative date handling differs)
|
144
|
+
assert_equal params1, params2
|
145
|
+
# The SQL should be identical for non-date queries
|
146
|
+
assert_equal sql1, sql2
|
147
|
+
end
|
148
|
+
|
149
|
+
def test_combined_relative_dates_and_other_operators
|
150
|
+
sql, params = parse_and_visit("from:alice@example.com newer_than:7d")
|
151
|
+
|
152
|
+
assert_includes sql, "ma1.email_address = ?"
|
153
|
+
assert_includes sql, "m0.internal_date > (NOW() - ?::interval)"
|
154
|
+
assert_equal ["from", "cc", "bcc", "alice@example.com", "7 days"], params
|
155
|
+
end
|
156
|
+
end
|
@@ -0,0 +1,346 @@
|
|
1
|
+
require "test_helper"
|
2
|
+
|
3
|
+
class SqlVisitorTest < Minitest::Test
|
4
|
+
include GmailSearchSyntax::AST
|
5
|
+
|
6
|
+
def parse_and_visit(query_string, current_user_email: nil)
|
7
|
+
ast = GmailSearchSyntax.parse!(query_string)
|
8
|
+
visitor = GmailSearchSyntax::SqlVisitor.new(current_user_email: current_user_email)
|
9
|
+
visitor.visit(ast)
|
10
|
+
visitor.to_query.to_sql
|
11
|
+
end
|
12
|
+
|
13
|
+
def test_simple_from_operator
|
14
|
+
sql, params = parse_and_visit("from:amy@example.com")
|
15
|
+
|
16
|
+
assert_includes sql, "INNER JOIN message_addresses"
|
17
|
+
assert_includes sql, "address_type = ?"
|
18
|
+
assert_includes sql, "email_address = ?"
|
19
|
+
assert_equal ["from", "cc", "bcc", "amy@example.com"], params
|
20
|
+
end
|
21
|
+
|
22
|
+
def test_from_with_prefix_match
|
23
|
+
sql, params = parse_and_visit("from:amy@")
|
24
|
+
|
25
|
+
assert_includes sql, "email_address LIKE ?"
|
26
|
+
assert_equal ["from", "cc", "bcc", "amy@%"], params
|
27
|
+
end
|
28
|
+
|
29
|
+
def test_from_with_suffix_match
|
30
|
+
sql, params = parse_and_visit("from:@example.com")
|
31
|
+
|
32
|
+
assert_includes sql, "email_address LIKE ?"
|
33
|
+
assert_equal ["from", "cc", "bcc", "%@example.com"], params
|
34
|
+
end
|
35
|
+
|
36
|
+
def test_to_operator
|
37
|
+
sql, params = parse_and_visit("to:john@example.com")
|
38
|
+
|
39
|
+
assert_includes sql, "INNER JOIN message_addresses"
|
40
|
+
assert_includes sql, "address_type = ?"
|
41
|
+
assert_equal ["to", "cc", "bcc", "john@example.com"], params
|
42
|
+
end
|
43
|
+
|
44
|
+
def test_subject_operator
|
45
|
+
sql, params = parse_and_visit("subject:dinner")
|
46
|
+
|
47
|
+
assert_includes sql, "m0.subject LIKE ?"
|
48
|
+
assert_equal ["%dinner%"], params
|
49
|
+
end
|
50
|
+
|
51
|
+
def test_after_date_operator
|
52
|
+
sql, params = parse_and_visit("after:2004/04/16")
|
53
|
+
|
54
|
+
assert_includes sql, "m0.internal_date > ?"
|
55
|
+
assert_equal ["2004-04-16"], params
|
56
|
+
end
|
57
|
+
|
58
|
+
def test_before_date_operator
|
59
|
+
sql, params = parse_and_visit("before:2004/04/18")
|
60
|
+
|
61
|
+
assert_includes sql, "m0.internal_date < ?"
|
62
|
+
assert_equal ["2004-04-18"], params
|
63
|
+
end
|
64
|
+
|
65
|
+
def test_older_than_relative
|
66
|
+
sql, params = parse_and_visit("older_than:1y")
|
67
|
+
|
68
|
+
assert_includes sql, "m0.internal_date < datetime('now', ?)"
|
69
|
+
assert_equal ["-1 years"], params
|
70
|
+
end
|
71
|
+
|
72
|
+
def test_newer_than_relative
|
73
|
+
sql, params = parse_and_visit("newer_than:2d")
|
74
|
+
|
75
|
+
assert_includes sql, "m0.internal_date > datetime('now', ?)"
|
76
|
+
assert_equal ["-2 days"], params
|
77
|
+
end
|
78
|
+
|
79
|
+
def test_or_operator
|
80
|
+
sql, params = parse_and_visit("from:amy OR from:david")
|
81
|
+
|
82
|
+
assert_includes sql, "OR"
|
83
|
+
assert_equal ["from", "cc", "bcc", "amy", "from", "cc", "bcc", "david"], params
|
84
|
+
end
|
85
|
+
|
86
|
+
def test_or_operator_uses_unique_table_aliases
|
87
|
+
sql, _params = parse_and_visit("from:alice@example.com OR from:bob@example.com")
|
88
|
+
|
89
|
+
# Ensure we have two different aliases (ma1 and ma3, not reused)
|
90
|
+
# This was a bug where subvisitors would reuse the same alias counter
|
91
|
+
# (ma1 and ma3 because messages table uses fixed m0)
|
92
|
+
assert_includes sql, "message_addresses AS ma1"
|
93
|
+
assert_includes sql, "message_addresses AS ma3"
|
94
|
+
|
95
|
+
# Count occurrences of each alias in the SQL
|
96
|
+
ma1_count = sql.scan(/\bma1\b/).size
|
97
|
+
ma3_count = sql.scan(/\bma3\b/).size
|
98
|
+
|
99
|
+
# Each alias should appear multiple times (in JOIN and WHERE clauses)
|
100
|
+
assert ma1_count > 1, "ma1 should appear multiple times"
|
101
|
+
assert ma3_count > 1, "ma3 should appear multiple times"
|
102
|
+
end
|
103
|
+
|
104
|
+
def test_and_operator
|
105
|
+
sql, params = parse_and_visit("from:amy AND to:david")
|
106
|
+
|
107
|
+
assert_includes sql, "AND"
|
108
|
+
assert_equal ["from", "cc", "bcc", "amy", "to", "cc", "bcc", "david"], params
|
109
|
+
end
|
110
|
+
|
111
|
+
def test_implicit_and
|
112
|
+
sql, params = parse_and_visit("from:amy to:david")
|
113
|
+
|
114
|
+
assert_includes sql, "AND"
|
115
|
+
assert_equal ["from", "cc", "bcc", "amy", "to", "cc", "bcc", "david"], params
|
116
|
+
end
|
117
|
+
|
118
|
+
def test_negation
|
119
|
+
sql, _ = parse_and_visit("dinner -movie")
|
120
|
+
|
121
|
+
assert_includes sql, "NOT"
|
122
|
+
assert_includes sql, "m0.subject LIKE ?"
|
123
|
+
assert_includes sql, "m0.body LIKE ?"
|
124
|
+
end
|
125
|
+
|
126
|
+
def test_label_operator
|
127
|
+
sql, params = parse_and_visit("label:friends")
|
128
|
+
|
129
|
+
assert_includes sql, "INNER JOIN message_labels"
|
130
|
+
assert_includes sql, "INNER JOIN labels"
|
131
|
+
assert_includes sql, "name = ?"
|
132
|
+
assert_equal ["friends"], params
|
133
|
+
end
|
134
|
+
|
135
|
+
def test_category_operator
|
136
|
+
sql, params = parse_and_visit("category:primary")
|
137
|
+
|
138
|
+
assert_includes sql, "m0.category = ?"
|
139
|
+
assert_equal ["primary"], params
|
140
|
+
end
|
141
|
+
|
142
|
+
def test_has_attachment
|
143
|
+
sql, params = parse_and_visit("has:attachment")
|
144
|
+
|
145
|
+
assert_includes sql, "m0.has_attachment = 1"
|
146
|
+
assert_equal [], params
|
147
|
+
end
|
148
|
+
|
149
|
+
def test_has_yellow_star
|
150
|
+
sql, params = parse_and_visit('has:"yellow-star"')
|
151
|
+
|
152
|
+
assert_includes sql, "m0.has_yellow_star = 1"
|
153
|
+
assert_equal [], params
|
154
|
+
end
|
155
|
+
|
156
|
+
def test_has_userlabels
|
157
|
+
sql, params = parse_and_visit("has:userlabels")
|
158
|
+
|
159
|
+
assert_includes sql, "INNER JOIN message_labels"
|
160
|
+
assert_includes sql, "is_system_label = 0"
|
161
|
+
assert_equal [], params
|
162
|
+
end
|
163
|
+
|
164
|
+
def test_has_nouserlabels
|
165
|
+
sql, params = parse_and_visit("has:nouserlabels")
|
166
|
+
|
167
|
+
assert_includes sql, "NOT EXISTS"
|
168
|
+
assert_includes sql, "is_system_label = 0"
|
169
|
+
assert_equal [], params
|
170
|
+
end
|
171
|
+
|
172
|
+
def test_filename_extension
|
173
|
+
sql, params = parse_and_visit("filename:pdf")
|
174
|
+
|
175
|
+
assert_includes sql, "INNER JOIN attachments"
|
176
|
+
assert_includes sql, "filename LIKE ?"
|
177
|
+
assert_equal ["%.pdf", "pdf%"], params
|
178
|
+
end
|
179
|
+
|
180
|
+
def test_filename_exact
|
181
|
+
sql, params = parse_and_visit("filename:homework.txt")
|
182
|
+
|
183
|
+
assert_includes sql, "INNER JOIN attachments"
|
184
|
+
assert_includes sql, "filename = ?"
|
185
|
+
assert_equal ["homework.txt"], params
|
186
|
+
end
|
187
|
+
|
188
|
+
def test_in_inbox
|
189
|
+
sql, params = parse_and_visit("in:inbox")
|
190
|
+
|
191
|
+
assert_includes sql, "m0.in_inbox = 1"
|
192
|
+
assert_equal [], params
|
193
|
+
end
|
194
|
+
|
195
|
+
def test_in_anywhere
|
196
|
+
sql, _ = parse_and_visit("in:anywhere")
|
197
|
+
|
198
|
+
refute_includes sql, "in_inbox"
|
199
|
+
refute_includes sql, "in_archive"
|
200
|
+
end
|
201
|
+
|
202
|
+
def test_is_starred
|
203
|
+
sql, params = parse_and_visit("is:starred")
|
204
|
+
|
205
|
+
assert_includes sql, "m0.is_starred = 1"
|
206
|
+
assert_equal [], params
|
207
|
+
end
|
208
|
+
|
209
|
+
def test_is_unread
|
210
|
+
sql, params = parse_and_visit("is:unread")
|
211
|
+
|
212
|
+
assert_includes sql, "m0.is_unread = 1"
|
213
|
+
assert_equal [], params
|
214
|
+
end
|
215
|
+
|
216
|
+
def test_size_operator
|
217
|
+
sql, params = parse_and_visit("size:1000000")
|
218
|
+
|
219
|
+
assert_includes sql, "m0.size_bytes = ?"
|
220
|
+
assert_equal [1000000], params
|
221
|
+
end
|
222
|
+
|
223
|
+
def test_larger_operator_with_m_suffix
|
224
|
+
sql, params = parse_and_visit("larger:10M")
|
225
|
+
|
226
|
+
assert_includes sql, "m0.size_bytes > ?"
|
227
|
+
assert_equal [10 * 1024 * 1024], params
|
228
|
+
end
|
229
|
+
|
230
|
+
def test_smaller_operator
|
231
|
+
sql, params = parse_and_visit("smaller:1M")
|
232
|
+
|
233
|
+
assert_includes sql, "m0.size_bytes < ?"
|
234
|
+
assert_equal [1 * 1024 * 1024], params
|
235
|
+
end
|
236
|
+
|
237
|
+
def test_rfc822msgid_operator
|
238
|
+
sql, params = parse_and_visit("rfc822msgid:200503292@example.com")
|
239
|
+
|
240
|
+
assert_includes sql, "m0.rfc822_message_id = ?"
|
241
|
+
assert_equal ["200503292@example.com"], params
|
242
|
+
end
|
243
|
+
|
244
|
+
def test_plain_text_search
|
245
|
+
sql, params = parse_and_visit("meeting")
|
246
|
+
|
247
|
+
assert_includes sql, "m0.subject LIKE ?"
|
248
|
+
assert_includes sql, "m0.body LIKE ?"
|
249
|
+
assert_equal ["meeting", "%meeting%", "%meeting%"], params
|
250
|
+
end
|
251
|
+
|
252
|
+
def test_complex_query
|
253
|
+
sql, _ = parse_and_visit("from:amy subject:meeting has:attachment")
|
254
|
+
|
255
|
+
assert_includes sql, "AND"
|
256
|
+
assert_includes sql, "INNER JOIN message_addresses"
|
257
|
+
assert_includes sql, "m0.subject LIKE ?"
|
258
|
+
assert_includes sql, "m0.has_attachment = 1"
|
259
|
+
end
|
260
|
+
|
261
|
+
def test_or_with_parentheses
|
262
|
+
sql, _ = parse_and_visit("from:(amy@example.com OR bob@example.com)")
|
263
|
+
|
264
|
+
assert_includes sql, "OR"
|
265
|
+
assert_includes sql, "INNER JOIN message_addresses"
|
266
|
+
end
|
267
|
+
|
268
|
+
def test_braces_as_or
|
269
|
+
sql, _ = parse_and_visit("from:{amy@example.com bob@example.com}")
|
270
|
+
|
271
|
+
assert_includes sql, "OR"
|
272
|
+
end
|
273
|
+
|
274
|
+
def test_negation_with_operator
|
275
|
+
sql, params = parse_and_visit("-from:spam@example.com")
|
276
|
+
|
277
|
+
assert_includes sql, "NOT"
|
278
|
+
assert_includes sql, "INNER JOIN message_addresses"
|
279
|
+
assert_equal ["from", "cc", "bcc", "spam@example.com"], params
|
280
|
+
end
|
281
|
+
|
282
|
+
def test_around_operator_generates_noop
|
283
|
+
sql, params = parse_and_visit("holiday AROUND 10 vacation")
|
284
|
+
|
285
|
+
assert_includes sql, "(1 = 0)"
|
286
|
+
assert_equal [], params
|
287
|
+
end
|
288
|
+
|
289
|
+
def test_list_operator
|
290
|
+
sql, params = parse_and_visit("list:info@example.com")
|
291
|
+
|
292
|
+
assert_includes sql, "m0.mailing_list = ?"
|
293
|
+
assert_equal ["info@example.com"], params
|
294
|
+
end
|
295
|
+
|
296
|
+
def test_list_with_suffix_match
|
297
|
+
sql, params = parse_and_visit("list:@example.com")
|
298
|
+
|
299
|
+
assert_includes sql, "m0.mailing_list LIKE ?"
|
300
|
+
assert_equal ["%@example.com"], params
|
301
|
+
end
|
302
|
+
|
303
|
+
def test_deliveredto_operator
|
304
|
+
sql, params = parse_and_visit("deliveredto:username@example.com")
|
305
|
+
|
306
|
+
assert_includes sql, "INNER JOIN message_addresses"
|
307
|
+
assert_includes sql, "address_type = ?"
|
308
|
+
assert_equal ["delivered_to", "username@example.com"], params
|
309
|
+
end
|
310
|
+
|
311
|
+
def test_cc_operator
|
312
|
+
sql, params = parse_and_visit("cc:john@example.com")
|
313
|
+
|
314
|
+
assert_includes sql, "address_type = ?"
|
315
|
+
assert_equal ["cc", "john@example.com"], params
|
316
|
+
end
|
317
|
+
|
318
|
+
def test_bcc_operator
|
319
|
+
sql, params = parse_and_visit("bcc:david@example.com")
|
320
|
+
|
321
|
+
assert_includes sql, "address_type = ?"
|
322
|
+
assert_equal ["bcc", "david@example.com"], params
|
323
|
+
end
|
324
|
+
|
325
|
+
def test_from_me_with_current_user
|
326
|
+
sql, params = parse_and_visit("from:me", current_user_email: "test@example.com")
|
327
|
+
|
328
|
+
assert_includes sql, "email_address = ?"
|
329
|
+
assert_equal ["from", "cc", "bcc", "test@example.com"], params
|
330
|
+
end
|
331
|
+
|
332
|
+
def test_nested_conditions
|
333
|
+
sql, _ = parse_and_visit("from:amy (subject:meeting OR subject:call)")
|
334
|
+
|
335
|
+
assert_includes sql, "AND"
|
336
|
+
assert_includes sql, "OR"
|
337
|
+
assert_includes sql, "m0.subject LIKE ?"
|
338
|
+
end
|
339
|
+
|
340
|
+
def test_multiple_joins_with_same_table
|
341
|
+
sql, _ = parse_and_visit("from:amy to:bob")
|
342
|
+
|
343
|
+
join_count = sql.scan("INNER JOIN message_addresses").length
|
344
|
+
assert_equal 2, join_count
|
345
|
+
end
|
346
|
+
end
|
data/test/test_helper.rb
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
$LOAD_PATH.unshift File.expand_path("../lib", __dir__)
|
2
|
+
require "gmail_search_syntax"
|
3
|
+
require "minitest/autorun"
|
4
|
+
|
5
|
+
# Helper module for Gmail message ID generation
|
6
|
+
module GmailMessageIdHelper
|
7
|
+
# Generates a Gmail-style message ID from a timestamp
|
8
|
+
# Based on: https://www.metaspike.com/dates-gmail-message-id-thread-id-timestamps/
|
9
|
+
#
|
10
|
+
# Gmail Message IDs encode the timestamp in the first part (all but last 5 hex digits)
|
11
|
+
# To generate: take timestamp in milliseconds, convert to hex, append 5 random hex digits
|
12
|
+
#
|
13
|
+
# @param time [Time] The timestamp to encode in the message ID
|
14
|
+
# @param random [Random, nil] Optional Random instance for reproducible testing
|
15
|
+
# @return [String] A hexadecimal Gmail message ID (15-16 digits)
|
16
|
+
def generate_gmail_message_id(time, random: Random.new)
|
17
|
+
# Get timestamp in milliseconds since epoch
|
18
|
+
timestamp_ms = (time.to_f * 1000).to_i
|
19
|
+
# Convert timestamp to hex and append first 5 hex digits from random bytes
|
20
|
+
timestamp_hex = timestamp_ms.to_s(16)
|
21
|
+
# Generate 3 random bytes (24 bits = 6 hex digits, we'll use first 5)
|
22
|
+
# Using bytes is much faster than generating individual hex digits
|
23
|
+
random_hex = random.bytes(3).unpack1("H*")[0, 5]
|
24
|
+
|
25
|
+
timestamp_hex + random_hex
|
26
|
+
end
|
27
|
+
end
|