Coverage for /var/srv/projects/api.amasfac.comuna18.com/tmp/venv/lib/python3.9/site-packages/django/contrib/postgres/search.py: 38%

194 statements  

« prev     ^ index     » next       coverage.py v6.4.4, created at 2023-07-17 14:22 -0600

1import psycopg2 

2 

3from django.db.models import ( 

4 CharField, 

5 Expression, 

6 Field, 

7 FloatField, 

8 Func, 

9 Lookup, 

10 TextField, 

11 Value, 

12) 

13from django.db.models.expressions import CombinedExpression 

14from django.db.models.functions import Cast, Coalesce 

15 

16 

17class SearchVectorExact(Lookup): 

18 lookup_name = "exact" 

19 

20 def process_rhs(self, qn, connection): 

21 if not isinstance(self.rhs, (SearchQuery, CombinedSearchQuery)): 

22 config = getattr(self.lhs, "config", None) 

23 self.rhs = SearchQuery(self.rhs, config=config) 

24 rhs, rhs_params = super().process_rhs(qn, connection) 

25 return rhs, rhs_params 

26 

27 def as_sql(self, qn, connection): 

28 lhs, lhs_params = self.process_lhs(qn, connection) 

29 rhs, rhs_params = self.process_rhs(qn, connection) 

30 params = lhs_params + rhs_params 

31 return "%s @@ %s" % (lhs, rhs), params 

32 

33 

34class SearchVectorField(Field): 

35 def db_type(self, connection): 

36 return "tsvector" 

37 

38 

39class SearchQueryField(Field): 

40 def db_type(self, connection): 

41 return "tsquery" 

42 

43 

44class SearchConfig(Expression): 

45 def __init__(self, config): 

46 super().__init__() 

47 if not hasattr(config, "resolve_expression"): 

48 config = Value(config) 

49 self.config = config 

50 

51 @classmethod 

52 def from_parameter(cls, config): 

53 if config is None or isinstance(config, cls): 

54 return config 

55 return cls(config) 

56 

57 def get_source_expressions(self): 

58 return [self.config] 

59 

60 def set_source_expressions(self, exprs): 

61 (self.config,) = exprs 

62 

63 def as_sql(self, compiler, connection): 

64 sql, params = compiler.compile(self.config) 

65 return "%s::regconfig" % sql, params 

66 

67 

68class SearchVectorCombinable: 

69 ADD = "||" 

70 

71 def _combine(self, other, connector, reversed): 

72 if not isinstance(other, SearchVectorCombinable): 

73 raise TypeError( 

74 "SearchVector can only be combined with other SearchVector " 

75 "instances, got %s." % type(other).__name__ 

76 ) 

77 if reversed: 

78 return CombinedSearchVector(other, connector, self, self.config) 

79 return CombinedSearchVector(self, connector, other, self.config) 

80 

81 

82class SearchVector(SearchVectorCombinable, Func): 

83 function = "to_tsvector" 

84 arg_joiner = " || ' ' || " 

85 output_field = SearchVectorField() 

86 

87 def __init__(self, *expressions, config=None, weight=None): 

88 super().__init__(*expressions) 

89 self.config = SearchConfig.from_parameter(config) 

90 if weight is not None and not hasattr(weight, "resolve_expression"): 

91 weight = Value(weight) 

92 self.weight = weight 

93 

94 def resolve_expression( 

95 self, query=None, allow_joins=True, reuse=None, summarize=False, for_save=False 

96 ): 

97 resolved = super().resolve_expression( 

98 query, allow_joins, reuse, summarize, for_save 

99 ) 

100 if self.config: 

101 resolved.config = self.config.resolve_expression( 

102 query, allow_joins, reuse, summarize, for_save 

103 ) 

104 return resolved 

105 

106 def as_sql(self, compiler, connection, function=None, template=None): 

107 clone = self.copy() 

108 clone.set_source_expressions( 

109 [ 

110 Coalesce( 

111 expression 

112 if isinstance(expression.output_field, (CharField, TextField)) 

113 else Cast(expression, TextField()), 

114 Value(""), 

115 ) 

116 for expression in clone.get_source_expressions() 

117 ] 

118 ) 

119 config_sql = None 

120 config_params = [] 

121 if template is None: 

122 if clone.config: 

123 config_sql, config_params = compiler.compile(clone.config) 

124 template = "%(function)s(%(config)s, %(expressions)s)" 

125 else: 

126 template = clone.template 

127 sql, params = super(SearchVector, clone).as_sql( 

128 compiler, 

129 connection, 

130 function=function, 

131 template=template, 

132 config=config_sql, 

133 ) 

134 extra_params = [] 

135 if clone.weight: 

136 weight_sql, extra_params = compiler.compile(clone.weight) 

137 sql = "setweight({}, {})".format(sql, weight_sql) 

138 return sql, config_params + params + extra_params 

139 

140 

141class CombinedSearchVector(SearchVectorCombinable, CombinedExpression): 

142 def __init__(self, lhs, connector, rhs, config, output_field=None): 

143 self.config = config 

144 super().__init__(lhs, connector, rhs, output_field) 

145 

146 

147class SearchQueryCombinable: 

148 BITAND = "&&" 

149 BITOR = "||" 

150 

151 def _combine(self, other, connector, reversed): 

152 if not isinstance(other, SearchQueryCombinable): 

153 raise TypeError( 

154 "SearchQuery can only be combined with other SearchQuery " 

155 "instances, got %s." % type(other).__name__ 

156 ) 

157 if reversed: 

158 return CombinedSearchQuery(other, connector, self, self.config) 

159 return CombinedSearchQuery(self, connector, other, self.config) 

160 

161 # On Combinable, these are not implemented to reduce confusion with Q. In 

162 # this case we are actually (ab)using them to do logical combination so 

163 # it's consistent with other usage in Django. 

164 def __or__(self, other): 

165 return self._combine(other, self.BITOR, False) 

166 

167 def __ror__(self, other): 

168 return self._combine(other, self.BITOR, True) 

169 

170 def __and__(self, other): 

171 return self._combine(other, self.BITAND, False) 

172 

173 def __rand__(self, other): 

174 return self._combine(other, self.BITAND, True) 

175 

176 

177class SearchQuery(SearchQueryCombinable, Func): 

178 output_field = SearchQueryField() 

179 SEARCH_TYPES = { 

180 "plain": "plainto_tsquery", 

181 "phrase": "phraseto_tsquery", 

182 "raw": "to_tsquery", 

183 "websearch": "websearch_to_tsquery", 

184 } 

185 

186 def __init__( 

187 self, 

188 value, 

189 output_field=None, 

190 *, 

191 config=None, 

192 invert=False, 

193 search_type="plain", 

194 ): 

195 self.function = self.SEARCH_TYPES.get(search_type) 

196 if self.function is None: 

197 raise ValueError("Unknown search_type argument '%s'." % search_type) 

198 if not hasattr(value, "resolve_expression"): 

199 value = Value(value) 

200 expressions = (value,) 

201 self.config = SearchConfig.from_parameter(config) 

202 if self.config is not None: 

203 expressions = (self.config,) + expressions 

204 self.invert = invert 

205 super().__init__(*expressions, output_field=output_field) 

206 

207 def as_sql(self, compiler, connection, function=None, template=None): 

208 sql, params = super().as_sql(compiler, connection, function, template) 

209 if self.invert: 

210 sql = "!!(%s)" % sql 

211 return sql, params 

212 

213 def __invert__(self): 

214 clone = self.copy() 

215 clone.invert = not self.invert 

216 return clone 

217 

218 def __str__(self): 

219 result = super().__str__() 

220 return ("~%s" % result) if self.invert else result 

221 

222 

223class CombinedSearchQuery(SearchQueryCombinable, CombinedExpression): 

224 def __init__(self, lhs, connector, rhs, config, output_field=None): 

225 self.config = config 

226 super().__init__(lhs, connector, rhs, output_field) 

227 

228 def __str__(self): 

229 return "(%s)" % super().__str__() 

230 

231 

232class SearchRank(Func): 

233 function = "ts_rank" 

234 output_field = FloatField() 

235 

236 def __init__( 

237 self, 

238 vector, 

239 query, 

240 weights=None, 

241 normalization=None, 

242 cover_density=False, 

243 ): 

244 if not hasattr(vector, "resolve_expression"): 

245 vector = SearchVector(vector) 

246 if not hasattr(query, "resolve_expression"): 

247 query = SearchQuery(query) 

248 expressions = (vector, query) 

249 if weights is not None: 

250 if not hasattr(weights, "resolve_expression"): 

251 weights = Value(weights) 

252 expressions = (weights,) + expressions 

253 if normalization is not None: 

254 if not hasattr(normalization, "resolve_expression"): 

255 normalization = Value(normalization) 

256 expressions += (normalization,) 

257 if cover_density: 

258 self.function = "ts_rank_cd" 

259 super().__init__(*expressions) 

260 

261 

262class SearchHeadline(Func): 

263 function = "ts_headline" 

264 template = "%(function)s(%(expressions)s%(options)s)" 

265 output_field = TextField() 

266 

267 def __init__( 

268 self, 

269 expression, 

270 query, 

271 *, 

272 config=None, 

273 start_sel=None, 

274 stop_sel=None, 

275 max_words=None, 

276 min_words=None, 

277 short_word=None, 

278 highlight_all=None, 

279 max_fragments=None, 

280 fragment_delimiter=None, 

281 ): 

282 if not hasattr(query, "resolve_expression"): 

283 query = SearchQuery(query) 

284 options = { 

285 "StartSel": start_sel, 

286 "StopSel": stop_sel, 

287 "MaxWords": max_words, 

288 "MinWords": min_words, 

289 "ShortWord": short_word, 

290 "HighlightAll": highlight_all, 

291 "MaxFragments": max_fragments, 

292 "FragmentDelimiter": fragment_delimiter, 

293 } 

294 self.options = { 

295 option: value for option, value in options.items() if value is not None 

296 } 

297 expressions = (expression, query) 

298 if config is not None: 

299 config = SearchConfig.from_parameter(config) 

300 expressions = (config,) + expressions 

301 super().__init__(*expressions) 

302 

303 def as_sql(self, compiler, connection, function=None, template=None): 

304 options_sql = "" 

305 options_params = [] 

306 if self.options: 

307 # getquoted() returns a quoted bytestring of the adapted value. 

308 options_params.append( 

309 ", ".join( 

310 "%s=%s" 

311 % ( 

312 option, 

313 psycopg2.extensions.adapt(value).getquoted().decode(), 

314 ) 

315 for option, value in self.options.items() 

316 ) 

317 ) 

318 options_sql = ", %s" 

319 sql, params = super().as_sql( 

320 compiler, 

321 connection, 

322 function=function, 

323 template=template, 

324 options=options_sql, 

325 ) 

326 return sql, params + options_params 

327 

328 

329SearchVectorField.register_lookup(SearchVectorExact) 

330 

331 

332class TrigramBase(Func): 

333 output_field = FloatField() 

334 

335 def __init__(self, expression, string, **extra): 

336 if not hasattr(string, "resolve_expression"): 

337 string = Value(string) 

338 super().__init__(expression, string, **extra) 

339 

340 

341class TrigramWordBase(Func): 

342 output_field = FloatField() 

343 

344 def __init__(self, string, expression, **extra): 

345 if not hasattr(string, "resolve_expression"): 

346 string = Value(string) 

347 super().__init__(string, expression, **extra) 

348 

349 

350class TrigramSimilarity(TrigramBase): 

351 function = "SIMILARITY" 

352 

353 

354class TrigramDistance(TrigramBase): 

355 function = "" 

356 arg_joiner = " <-> " 

357 

358 

359class TrigramWordDistance(TrigramWordBase): 

360 function = "" 

361 arg_joiner = " <<-> " 

362 

363 

364class TrigramWordSimilarity(TrigramWordBase): 

365 function = "WORD_SIMILARITY"