Browse Source
[ADD] First implementation of base_trgm_search for creating indexes and allowing the sql parameter %.
pull/462/head
[ADD] First implementation of base_trgm_search for creating indexes and allowing the sql parameter %.
pull/462/head
Christoph Giesel
9 years ago
5 changed files with 276 additions and 0 deletions
-
2base_trgm_search/__init__.py
-
17base_trgm_search/__openerp__.py
-
2base_trgm_search/models/__init__.py
-
209base_trgm_search/models/trgm_index.py
-
46base_trgm_search/views/trgm_index.xml
@ -0,0 +1,2 @@ |
|||
# -*- coding: utf-8 -*- |
|||
from . import models |
@ -0,0 +1,17 @@ |
|||
# -*- coding: utf-8 -*- |
|||
{ |
|||
'name': "PostgreSQL Trigram Search", |
|||
'summary': """PostgreSQL Trigram Search""", |
|||
'category': 'Uncategorized', |
|||
'version': '8.0.1.0.0', |
|||
'website': 'https://odoo-community.org/', |
|||
'author': 'Daniel Reis, Odoo Community Association (OCA)', |
|||
'license': 'AGPL-3', |
|||
'depends': [ |
|||
'base', |
|||
], |
|||
'data': [ |
|||
'views/trgm_index.xml', |
|||
], |
|||
'installable': True, |
|||
} |
@ -0,0 +1,2 @@ |
|||
# -*- coding: utf-8 -*- |
|||
from . import trgm_index |
@ -0,0 +1,209 @@ |
|||
# -*- coding: utf-8 -*- |
|||
import logging |
|||
|
|||
from openerp import api, exceptions, fields, models |
|||
from openerp.osv import expression |
|||
|
|||
from psycopg2.extensions import AsIs |
|||
|
|||
_logger = logging.getLogger(__name__) |
|||
|
|||
|
|||
class TrgmIndex(models.Model): |
|||
|
|||
"""Model for Trigram Index.""" |
|||
|
|||
_name = 'trgm.index' |
|||
_rec_name = 'field_id' |
|||
|
|||
field_id = fields.Many2one( |
|||
comodel_name='ir.model.fields', |
|||
string='Field', |
|||
required=True, |
|||
help='You can either select a field of type "text" or "char".' |
|||
) |
|||
|
|||
index_name = fields.Char( |
|||
string='Index Name', |
|||
readonly=True, |
|||
help='The index name is automatically generated like ' |
|||
'fieldname_indextype_idx. If the index already exists and the ' |
|||
'index is located in the same table then this index is resused. ' |
|||
'If the index is located in another table then a number is added ' |
|||
'at the end of the index name.' |
|||
) |
|||
|
|||
index_type = fields.Selection( |
|||
selection=[('gin', 'GIN'), ('gist', 'GiST')], |
|||
string='Index Type', |
|||
default='gin', |
|||
required=True, |
|||
help='Cite from PostgreSQL documentation: "As a rule of thumb, a ' |
|||
'GIN index is faster to search than a GiST index, but slower to ' |
|||
'build or update; so GIN is better suited for static data and ' |
|||
'GiST for often-updated data."' |
|||
) |
|||
|
|||
@api.model |
|||
def _trgm_extension_exists(self): |
|||
self.env.cr.execute(""" |
|||
SELECT name, installed_version |
|||
FROM pg_available_extensions |
|||
WHERE name = 'pg_trgm' |
|||
LIMIT 1; |
|||
""") |
|||
|
|||
extension = self.env.cr.fetchone() |
|||
if extension is None: |
|||
return 'missing' |
|||
|
|||
if extension[1] is None: |
|||
return 'uninstalled' |
|||
|
|||
return 'installed' |
|||
|
|||
@api.model |
|||
def _is_postgres_superuser(self): |
|||
self.env.cr.execute("SHOW is_superuser;") |
|||
superuser = self.env.cr.fetchone() |
|||
return superuser is not None and superuser[0] == 'on' or False |
|||
|
|||
@api.model |
|||
def _install_trgm_extension(self): |
|||
extension = self._trgm_extension_exists() |
|||
if extension == 'missing': |
|||
_logger.warning('To use pg_trgm you have to install the ' |
|||
'postgres-contrib module.') |
|||
elif extension == 'uninstalled': |
|||
if self._is_postgres_superuser(): |
|||
self.env.cr.execute("CREATE EXTENSION IF NOT EXISTS pg_trgm;") |
|||
return True |
|||
else: |
|||
_logger.warning('To use pg_trgm you have to create the ' |
|||
'extension pg_trgm in your database or you ' |
|||
'have to be the superuser.') |
|||
else: |
|||
return True |
|||
return False |
|||
|
|||
@api.model |
|||
def _auto_init(self): |
|||
res = super(TrgmIndex, self)._auto_init() |
|||
self._install_trgm_extension() |
|||
return res |
|||
|
|||
@api.model |
|||
def get_not_used_index(self, index_name, table_name, inc=1): |
|||
if inc > 1: |
|||
new_index_name = index_name + str(inc) |
|||
else: |
|||
new_index_name = index_name |
|||
self.env.cr.execute(""" |
|||
SELECT tablename, indexname |
|||
FROM pg_indexes |
|||
WHERE indexname = %(index)s; |
|||
""", {'index': new_index_name}) |
|||
|
|||
indexes = self.env.cr.fetchone() |
|||
if indexes is not None and indexes[0] == table_name: |
|||
return True, index_name |
|||
elif indexes is not None: |
|||
return self.get_not_used_index(index_name, table_name, |
|||
inc + 1) |
|||
|
|||
return False, new_index_name |
|||
|
|||
@api.multi |
|||
def create_index(self): |
|||
self.ensure_one() |
|||
|
|||
if not self._install_trgm_extension(): |
|||
raise exceptions.UserError( |
|||
'The pg_trgm extension does not exists or cannot be ' |
|||
'installed.') |
|||
|
|||
table_name = self.env[self.field_id.model_id.model]._table |
|||
column_name = self.field_id.name |
|||
index_type = self.index_type |
|||
index_name = '%s_%s_idx' % (column_name, index_type) |
|||
index_exists, index_name = self.get_not_used_index( |
|||
index_name, table_name) |
|||
|
|||
if not index_exists: |
|||
self.env.cr.execute(""" |
|||
CREATE INDEX %(index)s |
|||
ON %(table)s |
|||
USING %(indextype)s (%(column)s %(indextype)s_trgm_ops); |
|||
""", { |
|||
'table': AsIs(table_name), |
|||
'index': AsIs(index_name), |
|||
'column': AsIs(column_name), |
|||
'indextype': AsIs(index_type) |
|||
}) |
|||
return index_name |
|||
|
|||
@api.model |
|||
def index_exists(self, model_name, field_name): |
|||
field = self.env['ir.model.fields'].search([ |
|||
('model', '=', model_name), ('name', '=', field_name)], limit=1) |
|||
|
|||
if not field: |
|||
return False |
|||
|
|||
trgm_index = self.search([('field_id', '=', field.id)], limit=1) |
|||
return bool(trgm_index) |
|||
|
|||
@api.model |
|||
def create(self, vals): |
|||
rec = super(TrgmIndex, self).create(vals) |
|||
rec.index_name = rec.create_index() |
|||
return rec |
|||
|
|||
@api.multi |
|||
def unlink(self): |
|||
for rec in self: |
|||
self.env.cr.execute(""" |
|||
DROP INDEX IF EXISTS %(index)s; |
|||
""", { |
|||
'index': AsIs(rec.index_name), |
|||
}) |
|||
return super(TrgmIndex, self).unlink() |
|||
|
|||
|
|||
def patch_leaf_trgm(method): |
|||
def decorate_leaf_to_sql(self, eleaf): |
|||
model = eleaf.model |
|||
leaf = eleaf.leaf |
|||
left, operator, right = leaf |
|||
table_alias = '"%s"' % (eleaf.generate_alias()) |
|||
|
|||
if operator == '%': |
|||
sql_operator = '%%' |
|||
params = [] |
|||
|
|||
if left in model._columns: |
|||
format = model._columns[left]._symbol_set[0] |
|||
column = '%s.%s' % (table_alias, expression._quote(left)) |
|||
query = '(%s %s %s)' % (column, sql_operator, format) |
|||
elif left in expression.MAGIC_COLUMNS: |
|||
query = "(%s.\"%s\" %s %%s)" % ( |
|||
table_alias, left, sql_operator) |
|||
params = right |
|||
else: # Must not happen |
|||
raise ValueError( |
|||
"Invalid field %r in domain term %r" % (left, leaf)) |
|||
|
|||
if left in model._columns: |
|||
params = model._columns[left]._symbol_set[1](right) |
|||
|
|||
if isinstance(params, basestring): |
|||
params = [params] |
|||
return query, params |
|||
elif operator == 'inselect': |
|||
right = (right[0].replace(' % ', ' %% '), right[1]) |
|||
eleaf.leaf = (left, operator, right) |
|||
return method(self, eleaf) |
|||
return decorate_leaf_to_sql |
|||
expression.expression._expression__leaf_to_sql = patch_leaf_trgm( |
|||
expression.expression._expression__leaf_to_sql) |
|||
expression.TERM_OPERATORS += ('%',) |
@ -0,0 +1,46 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<openerp> |
|||
<data> |
|||
|
|||
<record model="ir.ui.view" id="trgm_index_view_form"> |
|||
<field name="name">trgm.index.view.form</field> |
|||
<field name="model">trgm.index</field> |
|||
<field name="arch" type="xml"> |
|||
<form string="Trigam Index"> |
|||
<sheet> |
|||
<group col="4"> |
|||
<field name="field_id" domain="[('ttype', 'in', ['char', 'text'])]"/> |
|||
<field name="index_name"/> |
|||
<field name="index_type"/> |
|||
</group> |
|||
</sheet> |
|||
</form> |
|||
</field> |
|||
</record> |
|||
|
|||
<record model="ir.ui.view" id="trgm_index_view_tree"> |
|||
<field name="name">trgm.index.view.tree</field> |
|||
<field name="model">trgm.index</field> |
|||
<field name="arch" type="xml"> |
|||
<tree string="Trigam Index"> |
|||
<field name="field_id"/> |
|||
<field name="index_name"/> |
|||
<field name="index_type"/> |
|||
</tree> |
|||
</field> |
|||
</record> |
|||
|
|||
<record model="ir.actions.act_window" id="trgm_index_action"> |
|||
<field name="name">Trigam Index</field> |
|||
<field name="res_model">trgm.index</field> |
|||
<field name="view_type">form</field> |
|||
<field name="view_mode">tree,form</field> |
|||
<field name="type">ir.actions.act_window</field> |
|||
</record> |
|||
|
|||
<menuitem id="trgm_index_menu" |
|||
parent="base.next_id_9" |
|||
action="trgm_index_action"/> |
|||
|
|||
</data> |
|||
</openerp> |
Write
Preview
Loading…
Cancel
Save
Reference in new issue