001/* 002 * (C) Copyright 2006-2018 Nuxeo (http://nuxeo.com/) and others. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 * 016 * Contributors: 017 * Florent Guillaume 018 * Benoit Delbosc 019 */ 020package org.nuxeo.ecm.core.storage.sql.jdbc.dialect; 021 022import java.io.Serializable; 023import java.sql.Array; 024import java.sql.Connection; 025import java.sql.DatabaseMetaData; 026import java.sql.PreparedStatement; 027import java.sql.ResultSet; 028import java.sql.SQLException; 029import java.sql.Statement; 030import java.sql.Timestamp; 031import java.sql.Types; 032import java.util.ArrayList; 033import java.util.Arrays; 034import java.util.Collections; 035import java.util.HashMap; 036import java.util.HashSet; 037import java.util.LinkedList; 038import java.util.List; 039import java.util.Map; 040import java.util.Map.Entry; 041import java.util.Set; 042import java.util.UUID; 043import java.util.regex.Matcher; 044import java.util.regex.Pattern; 045import java.util.stream.Collectors; 046 047import org.apache.commons.lang3.StringUtils; 048import org.apache.commons.logging.Log; 049import org.apache.commons.logging.LogFactory; 050import org.nuxeo.ecm.core.NXCore; 051import org.nuxeo.ecm.core.api.NuxeoException; 052import org.nuxeo.ecm.core.api.security.SecurityConstants; 053import org.nuxeo.ecm.core.query.QueryParseException; 054import org.nuxeo.ecm.core.security.SecurityService; 055import org.nuxeo.ecm.core.storage.FulltextConfiguration; 056import org.nuxeo.ecm.core.storage.FulltextQueryAnalyzer; 057import org.nuxeo.ecm.core.storage.FulltextQueryAnalyzer.FulltextQuery; 058import org.nuxeo.ecm.core.storage.FulltextQueryAnalyzer.Op; 059import org.nuxeo.ecm.core.storage.sql.ColumnType; 060import org.nuxeo.ecm.core.storage.sql.Model; 061import org.nuxeo.ecm.core.storage.sql.RepositoryDescriptor; 062import org.nuxeo.ecm.core.storage.sql.jdbc.JDBCLogger; 063import org.nuxeo.ecm.core.storage.sql.jdbc.db.Column; 064import org.nuxeo.ecm.core.storage.sql.jdbc.db.Database; 065import org.nuxeo.ecm.core.storage.sql.jdbc.db.Join; 066import org.nuxeo.ecm.core.storage.sql.jdbc.db.Table; 067import org.nuxeo.ecm.core.storage.sql.jdbc.db.TableAlias; 068 069/** 070 * PostgreSQL-specific dialect. 071 * 072 * @author Florent Guillaume 073 */ 074public class DialectPostgreSQL extends Dialect { 075 076 private static final Log log = LogFactory.getLog(DialectPostgreSQL.class); 077 078 private static final String DEFAULT_FULLTEXT_ANALYZER = "english"; 079 080 private static final String DEFAULT_USERS_SEPARATOR = ","; 081 082 private static final String PREFIX_SEARCH = ":*"; 083 084 // prefix search syntax foo* or foo% or foo:*-> foo:* 085 private static final Pattern PREFIX_PATTERN = Pattern.compile("(\\*|%|:\\*)( |\"|$)"); 086 087 private static final String PREFIX_REPL = PREFIX_SEARCH + "$2"; 088 089 private static final String[] RESERVED_COLUMN_NAMES = { "xmin", "xmax", "cmin", "cmax", "ctid", "oid", "tableoid" }; 090 091 private static final String UNLOGGED_KEYWORD = "UNLOGGED"; 092 093 protected final String fulltextAnalyzer; 094 095 protected final boolean supportsWith; 096 097 protected boolean hierarchyCreated; 098 099 protected boolean pathOptimizationsEnabled; 100 101 protected final boolean arrayColumnsEnabled; 102 103 protected final boolean childNameUniqueConstraintEnabled; 104 105 protected final boolean collectionUniqueConstraintEnabled; 106 107 protected String usersSeparator; 108 109 protected final DialectIdType idType; 110 111 protected boolean compatibilityFulltextTable; 112 113 protected final String unloggedKeyword; 114 115 protected String idSequenceName; 116 117 public DialectPostgreSQL(DatabaseMetaData metadata, RepositoryDescriptor repositoryDescriptor) { 118 super(metadata, repositoryDescriptor); 119 fulltextAnalyzer = repositoryDescriptor == null ? null 120 : repositoryDescriptor.getFulltextAnalyzer() == null ? DEFAULT_FULLTEXT_ANALYZER 121 : repositoryDescriptor.getFulltextAnalyzer(); 122 pathOptimizationsEnabled = repositoryDescriptor != null && repositoryDescriptor.getPathOptimizationsEnabled(); 123 if (repositoryDescriptor != null) { 124 log.info("Path optimizations " + (pathOptimizationsEnabled ? "enabled" : "disabled")); 125 } 126 arrayColumnsEnabled = repositoryDescriptor != null && repositoryDescriptor.getArrayColumns(); 127 childNameUniqueConstraintEnabled = repositoryDescriptor != null 128 && repositoryDescriptor.getChildNameUniqueConstraintEnabled(); 129 collectionUniqueConstraintEnabled = repositoryDescriptor != null 130 && repositoryDescriptor.getCollectionUniqueConstraintEnabled(); 131 int major, minor; 132 try { 133 major = metadata.getDatabaseMajorVersion(); 134 minor = metadata.getDatabaseMinorVersion(); 135 } catch (SQLException e) { 136 throw new NuxeoException(e); 137 } 138 supportsWith = major > 8 || (major == 8 && minor >= 4); 139 if ((major == 9 && minor >= 1) || (major > 9)) { 140 unloggedKeyword = UNLOGGED_KEYWORD; 141 } else { 142 unloggedKeyword = ""; 143 } 144 usersSeparator = repositoryDescriptor == null ? null 145 : repositoryDescriptor.usersSeparatorKey == null ? DEFAULT_USERS_SEPARATOR 146 : repositoryDescriptor.usersSeparatorKey; 147 String idt = repositoryDescriptor == null ? null : repositoryDescriptor.idType; 148 if (idt == null || "".equals(idt) || "varchar".equalsIgnoreCase(idt)) { 149 idType = DialectIdType.VARCHAR; 150 } else if ("uuid".equalsIgnoreCase(idt)) { 151 idType = DialectIdType.UUID; 152 } else if (idt.toLowerCase().startsWith("sequence")) { 153 idType = DialectIdType.SEQUENCE; 154 if (idt.toLowerCase().startsWith("sequence:")) { 155 String[] split = idt.split(":"); 156 idSequenceName = split[1]; 157 } else { 158 idSequenceName = "hierarchy_seq"; 159 } 160 } else { 161 throw new NuxeoException("Unknown id type: '" + idt + "'"); 162 } 163 try { 164 compatibilityFulltextTable = getCompatibilityFulltextTable(metadata); 165 } catch (SQLException e) { 166 throw new NuxeoException(e); 167 } 168 } 169 170 protected boolean getCompatibilityFulltextTable(DatabaseMetaData metadata) throws SQLException { 171 ResultSet rs = metadata.getColumns(null, null, Model.FULLTEXT_TABLE_NAME, "%"); 172 while (rs.next()) { 173 // COLUMN_NAME=fulltext DATA_TYPE=1111 TYPE_NAME=tsvector 174 String columnName = rs.getString("COLUMN_NAME"); 175 if (Model.FULLTEXT_FULLTEXT_KEY.equals(columnName)) { 176 String typeName = rs.getString("TYPE_NAME"); 177 return "tsvector".equals(typeName); 178 } 179 } 180 return false; 181 } 182 183 @Override 184 public String toBooleanValueString(boolean bool) { 185 return bool ? "true" : "false"; 186 } 187 188 @Override 189 public String getNoColumnsInsertString(Column idColumn) { 190 return "DEFAULT VALUES"; 191 } 192 193 @Override 194 public String getCascadeDropConstraintsString() { 195 return "CASCADE"; 196 } 197 198 @Override 199 public JDBCInfo getJDBCTypeAndString(ColumnType type) { 200 switch (type.spec) { 201 case STRING: 202 if (type.isUnconstrained()) { 203 return jdbcInfo("varchar", Types.VARCHAR); 204 } else if (type.isClob()) { 205 return jdbcInfo("text", Types.CLOB); 206 } else { 207 return jdbcInfo("varchar(%d)", type.length, Types.VARCHAR); 208 } 209 case ARRAY_STRING: 210 if (type.isUnconstrained()) { 211 return jdbcInfo("varchar[]", Types.ARRAY, "varchar", Types.VARCHAR); 212 } else if (type.isClob()) { 213 return jdbcInfo("text[]", Types.ARRAY, "text", Types.CLOB); 214 } else { 215 return jdbcInfo("varchar(%d)[]", type.length, Types.ARRAY, "varchar", Types.VARCHAR); 216 } 217 case BOOLEAN: 218 return jdbcInfo("bool", Types.BIT); 219 case ARRAY_BOOLEAN: 220 return jdbcInfo("bool[]", Types.ARRAY, "bool", Types.BOOLEAN); 221 case LONG: 222 return jdbcInfo("int8", Types.BIGINT); 223 case ARRAY_LONG: 224 return jdbcInfo("int8[]", Types.ARRAY, "int8", Types.BIGINT); 225 case DOUBLE: 226 return jdbcInfo("float8", Types.DOUBLE); 227 case ARRAY_DOUBLE: 228 return jdbcInfo("float8[]", Types.ARRAY, "float8", Types.DOUBLE); 229 case TIMESTAMP: 230 return jdbcInfo("timestamp", Types.TIMESTAMP); 231 case ARRAY_TIMESTAMP: 232 return jdbcInfo("timestamp[]", Types.ARRAY, "timestamp", Types.TIMESTAMP); 233 case BLOBID: 234 return jdbcInfo("varchar(250)", Types.VARCHAR); 235 case ARRAY_BLOBID: 236 return jdbcInfo("varchar(250)[]", Types.ARRAY, "varchar", Types.VARCHAR); 237 // ----- 238 case NODEID: 239 case NODEIDFK: 240 case NODEIDFKNP: 241 case NODEIDFKMUL: 242 case NODEIDFKNULL: 243 case NODEIDPK: 244 case NODEVAL: 245 switch (idType) { 246 case VARCHAR: 247 return jdbcInfo("varchar(36)", Types.VARCHAR); 248 case UUID: 249 return jdbcInfo("uuid", Types.OTHER); 250 case SEQUENCE: 251 return jdbcInfo("int8", Types.BIGINT); 252 } 253 throw new AssertionError("Unknown id type: " + idType); 254 case NODEARRAY: 255 switch (idType) { 256 case VARCHAR: 257 return jdbcInfo("varchar(36)[]", Types.ARRAY, "varchar", Types.VARCHAR); 258 case UUID: 259 return jdbcInfo("uuid[]", Types.ARRAY, "uuid", Types.OTHER); 260 case SEQUENCE: 261 return jdbcInfo("int8[]", Types.ARRAY, "int8", Types.BIGINT); 262 } 263 throw new AssertionError("Unknown id type: " + idType); 264 case SYSNAME: 265 return jdbcInfo("varchar(250)", Types.VARCHAR); 266 case SYSNAMEARRAY: 267 return jdbcInfo("varchar(250)[]", Types.ARRAY, "varchar", Types.VARCHAR); 268 case TINYINT: 269 return jdbcInfo("int2", Types.SMALLINT); 270 case INTEGER: 271 return jdbcInfo("int4", Types.INTEGER); 272 case ARRAY_INTEGER: 273 return jdbcInfo("int4[]", Types.ARRAY, "int4", Types.INTEGER); 274 case AUTOINC: 275 return jdbcInfo("serial", Types.INTEGER); 276 case FTINDEXED: 277 if (compatibilityFulltextTable) { 278 return jdbcInfo("tsvector", Types.OTHER); 279 } else { 280 return jdbcInfo("text", Types.CLOB); 281 } 282 case FTSTORED: 283 if (compatibilityFulltextTable) { 284 return jdbcInfo("tsvector", Types.OTHER); 285 } else { 286 return jdbcInfo("text", Types.CLOB); 287 } 288 case CLUSTERNODE: 289 return jdbcInfo("int4", Types.INTEGER); 290 case CLUSTERFRAGS: 291 return jdbcInfo("varchar[]", Types.ARRAY, "varchar", Types.VARCHAR); 292 } 293 throw new AssertionError(type); 294 } 295 296 @Override 297 public boolean isAllowedConversion(int expected, int actual, String actualName, int actualSize) { 298 // CLOB vs VARCHAR compatibility 299 if (expected == Types.VARCHAR && actual == Types.CLOB) { 300 return true; 301 } 302 if (expected == Types.CLOB && actual == Types.VARCHAR) { 303 return true; 304 } 305 // INTEGER vs BIGINT compatibility 306 if (expected == Types.BIGINT && actual == Types.INTEGER) { 307 return true; 308 } 309 if (expected == Types.INTEGER && actual == Types.BIGINT) { 310 return true; 311 } 312 // TSVECTOR vs CLOB compatibility during upgrade tests 313 // where column detection is done before upgrade test setup 314 if (expected == Types.CLOB && (actual == Types.OTHER && actualName.equals("tsvector"))) { 315 return true; 316 } 317 return false; 318 } 319 320 @Override 321 public Serializable getGeneratedId(Connection connection) throws SQLException { 322 if (idType != DialectIdType.SEQUENCE) { 323 return super.getGeneratedId(connection); 324 } 325 String sql = String.format("SELECT NEXTVAL('%s')", idSequenceName); 326 try (Statement s = connection.createStatement()) { 327 try (ResultSet rs = s.executeQuery(sql)) { 328 rs.next(); 329 return Long.valueOf(rs.getLong(1)); 330 } 331 } 332 } 333 334 @Override 335 public void setId(PreparedStatement ps, int index, Serializable value) throws SQLException { 336 switch (idType) { 337 case VARCHAR: 338 ps.setObject(index, value); 339 break; 340 case UUID: 341 ps.setObject(index, value, Types.OTHER); 342 break; 343 case SEQUENCE: 344 setIdLong(ps, index, value); 345 break; 346 default: 347 throw new AssertionError(); 348 } 349 } 350 351 @SuppressWarnings("boxing") 352 public Serializable getId(ResultSet rs, int index) throws SQLException { 353 switch (idType) { 354 case VARCHAR: 355 case UUID: 356 return rs.getString(index); 357 case SEQUENCE: 358 return rs.getLong(index); 359 default: 360 throw new AssertionError(); 361 } 362 } 363 364 @Override 365 public void setToPreparedStatement(PreparedStatement ps, int index, Serializable value, Column column) 366 throws SQLException { 367 switch (column.getJdbcType()) { 368 case Types.VARCHAR: 369 case Types.CLOB: 370 setToPreparedStatementString(ps, index, value, column); 371 return; 372 case Types.BIT: 373 ps.setBoolean(index, ((Boolean) value).booleanValue()); 374 return; 375 case Types.SMALLINT: 376 ps.setInt(index, ((Long) value).intValue()); 377 return; 378 case Types.INTEGER: 379 case Types.BIGINT: 380 ps.setLong(index, ((Number) value).longValue()); 381 return; 382 case Types.DOUBLE: 383 ps.setDouble(index, ((Double) value).doubleValue()); 384 return; 385 case Types.TIMESTAMP: 386 setToPreparedStatementTimestamp(ps, index, value, column); 387 return; 388 case Types.ARRAY: 389 int jdbcBaseType = column.getJdbcBaseType(); 390 String jdbcBaseTypeName = column.getSqlBaseTypeString(); 391 if (jdbcBaseType == Types.TIMESTAMP) { 392 value = getTimestampFromCalendar((Serializable[]) value); 393 } 394 Array array = ps.getConnection().createArrayOf(jdbcBaseTypeName, (Object[]) value); 395 ps.setArray(index, array); 396 return; 397 case Types.OTHER: 398 ColumnType type = column.getType(); 399 if (type.isId()) { 400 setId(ps, index, value); 401 return; 402 } else if (type == ColumnType.FTSTORED) { 403 ps.setString(index, (String) value); 404 return; 405 } 406 throw new SQLException("Unhandled type: " + column.getType()); 407 default: 408 throw new SQLException("Unhandled JDBC type: " + column.getJdbcType()); 409 } 410 } 411 412 @Override 413 @SuppressWarnings("boxing") 414 public Serializable getFromResultSet(ResultSet rs, int index, Column column) throws SQLException { 415 int jdbcType = rs.getMetaData().getColumnType(index); 416 if (column.getJdbcType() == Types.ARRAY && jdbcType != Types.ARRAY) { 417 jdbcType = column.getJdbcBaseType(); 418 } else { 419 jdbcType = column.getJdbcType(); 420 } 421 switch (jdbcType) { 422 case Types.VARCHAR: 423 case Types.CLOB: 424 return getFromResultSetString(rs, index, column); 425 case Types.BIT: 426 return rs.getBoolean(index); 427 case Types.SMALLINT: 428 case Types.INTEGER: 429 case Types.BIGINT: 430 return rs.getLong(index); 431 case Types.DOUBLE: 432 return rs.getDouble(index); 433 case Types.TIMESTAMP: 434 return getFromResultSetTimestamp(rs, index, column); 435 case Types.ARRAY: 436 Array array = rs.getArray(index); 437 if (array == null) { 438 return null; 439 } 440 if (array.getBaseType() == Types.TIMESTAMP) { 441 return getCalendarFromTimestamp((Timestamp[]) array.getArray()); 442 } else { 443 return (Serializable) array.getArray(); 444 } 445 case Types.OTHER: 446 ColumnType type = column.getType(); 447 if (type.isId()) { 448 return getId(rs, index); 449 } 450 throw new SQLException("Unhandled type: " + column.getType()); 451 } 452 throw new SQLException( 453 "Unhandled JDBC type: " + column.getJdbcType() + " for type " + column.getType().toString()); 454 } 455 456 @Override 457 protected int getMaxNameSize() { 458 return 63; 459 } 460 461 @Override 462 public String getColumnName(String name) { 463 // ignore suffixed "_" when checking for reservedness 464 String n = name.replaceAll("_+$", ""); 465 for (String reserved : RESERVED_COLUMN_NAMES) { 466 if (n.equals(reserved)) { 467 // reserved, add one more suffix "_" 468 name += "_"; 469 break; 470 } 471 } 472 return super.getColumnName(name); 473 } 474 475 @Override 476 public List<String> getCustomPostCreateSqls(Table table, Model model) { 477 List<String> sqls = new ArrayList<>(); 478 String key = table.getKey(); 479 if (childNameUniqueConstraintEnabled && key.equals(Model.HIER_TABLE_NAME)) { 480 // CREATE UNIQUE INDEX hierarchy_unique_child ON hierarchy (parentid, name) 481 // WHERE isproperty = false 482 sqls.add(String.format( 483 "CREATE UNIQUE INDEX \"hierarchy_unique_child\" ON \"%s\" (\"%s\", \"%s\") WHERE \"%s\" = false", 484 Model.HIER_TABLE_NAME, Model.HIER_PARENT_KEY, Model.HIER_CHILD_NAME_KEY, 485 Model.HIER_CHILD_ISPROPERTY_KEY)); 486 // CREATE UNIQUE INDEX hierarchy_unique_child_complex ON hierarchy (parentid, name) 487 // WHERE isproperty = true AND pos IS NULL 488 sqls.add(String.format( 489 "CREATE UNIQUE INDEX \"hierarchy_unique_child_complex\" ON \"%s\" (\"%s\", \"%s\") WHERE \"%s\" = true AND \"%s\" IS NULL", 490 Model.HIER_TABLE_NAME, Model.HIER_PARENT_KEY, Model.HIER_CHILD_NAME_KEY, 491 Model.HIER_CHILD_ISPROPERTY_KEY, Model.HIER_CHILD_POS_KEY)); 492 // CREATE UNIQUE INDEX hierarchy_unique_child_complex_list ON hierarchy (parentid, name, pos) 493 // WHERE isproperty = true AND pos IS NOT NULL 494 sqls.add(String.format( 495 "CREATE UNIQUE INDEX \"hierarchy_unique_child_complex_list\" ON \"%s\" (\"%s\", \"%s\", \"%s\") WHERE \"%s\" = true AND \"%s\" IS NOT NULL", 496 Model.HIER_TABLE_NAME, Model.HIER_PARENT_KEY, Model.HIER_CHILD_NAME_KEY, Model.HIER_CHILD_POS_KEY, 497 Model.HIER_CHILD_ISPROPERTY_KEY, Model.HIER_CHILD_POS_KEY)); 498 } else if (collectionUniqueConstraintEnabled && model.isCollectionFragment(key)) { 499 // CREATE UNIQUE INDEX dc_contributors_unique_pos ON dc_contributors (id, pos) 500 String name = table.getPhysicalName(); 501 sqls.add(String.format("CREATE UNIQUE INDEX \"%s_unique_pos\" ON \"%s\" (\"%s\", \"%s\")", name, name, 502 Model.MAIN_KEY, Model.COLL_TABLE_POS_KEY)); 503 } 504 return sqls; 505 } 506 507 @Override 508 public String getCreateFulltextIndexSql(String indexName, String quotedIndexName, Table table, List<Column> columns, 509 Model model) { 510 String sql; 511 if (compatibilityFulltextTable) { 512 sql = "CREATE INDEX %s ON %s USING GIN(%s)"; 513 } else { 514 sql = "CREATE INDEX %s ON %s USING GIN(NX_TO_TSVECTOR(%s))"; 515 } 516 return String.format(sql, quotedIndexName.toLowerCase(), table.getQuotedName(), columns.get(0).getQuotedName()); 517 } 518 519 // must not be interpreted as a regexp, we split on it 520 protected static final String FT_LIKE_SEP = " @#AND#@ "; 521 522 protected static final String FT_LIKE_COL = "??"; 523 524 /** 525 * {@inheritDoc} 526 * <p> 527 * The result of this is passed to {@link #getFulltextScoredMatchInfo}. 528 */ 529 @Override 530 public String getDialectFulltextQuery(String query) { 531 query = query.replace(" & ", " "); // PostgreSQL compatibility BBB 532 query = PREFIX_PATTERN.matcher(query).replaceAll(PREFIX_REPL); 533 FulltextQuery ft = FulltextQueryAnalyzer.analyzeFulltextQuery(query); 534 if (ft == null) { 535 return ""; // won't match anything 536 } 537 if (!FulltextQueryAnalyzer.hasPhrase(ft)) { 538 return FulltextQueryAnalyzer.translateFulltext(ft, "|", "&", "& !", ""); 539 } 540 if (compatibilityFulltextTable) { 541 throw new QueryParseException("Cannot use phrase search in fulltext compatibilty mode. " 542 + "Please upgrade the fulltext table: " + query); 543 } 544 /* 545 * Phrase search. We have to do the phrase query using a LIKE, but for performance we pre-filter using as many 546 * fulltext matches as possible by breaking some of the phrases into words. We do an AND of the two. 1. 547 * pre-filter using fulltext on a query that is a superset of the original query, 548 */ 549 FulltextQuery broken = breakPhrases(ft); 550 String ftsql = FulltextQueryAnalyzer.translateFulltext(broken, "|", "&", "& !", ""); 551 /* 552 * 2. AND with a LIKE-based search for all terms, except those that are already exactly matched by the first 553 * part, i.e., toplevel ANDed non-phrases. 554 */ 555 FulltextQuery noand = removeToplevelAndedWords(ft); 556 if (noand != null) { 557 StringBuilder buf = new StringBuilder(); 558 generateLikeSql(noand, buf); 559 ftsql += FT_LIKE_SEP + buf.toString(); 560 561 } 562 return ftsql; 563 } 564 565 /** 566 * Returns a fulltext query that is a superset of the original one and does not have phrase searches. 567 * <p> 568 * Negative phrases (which are at AND level) are removed, positive phrases are split into ANDed words. 569 */ 570 protected static FulltextQuery breakPhrases(FulltextQuery ft) { 571 FulltextQuery newFt = new FulltextQuery(); 572 if (ft.op == Op.AND || ft.op == Op.OR) { 573 List<FulltextQuery> newTerms = new LinkedList<>(); 574 for (FulltextQuery term : ft.terms) { 575 FulltextQuery broken = breakPhrases(term); 576 if (broken == null) { 577 // remove negative phrase 578 } else if (ft.op == Op.AND && broken.op == Op.AND) { 579 // associativity (sub-AND hoisting) 580 newTerms.addAll(broken.terms); 581 } else { 582 newTerms.add(broken); 583 } 584 } 585 if (newTerms.size() == 1) { 586 // single-term parenthesis elimination 587 newFt = newTerms.get(0); 588 } else { 589 newFt.op = ft.op; 590 newFt.terms = newTerms; 591 } 592 } else { 593 boolean isPhrase = ft.isPhrase(); 594 if (!isPhrase) { 595 newFt = ft; 596 } else if (ft.op == Op.WORD) { 597 // positive phrase 598 // split it 599 List<FulltextQuery> newTerms = new LinkedList<>(); 600 for (String subword : ft.word.split(" ")) { 601 FulltextQuery sft = new FulltextQuery(); 602 sft.op = Op.WORD; 603 sft.word = subword; 604 newTerms.add(sft); 605 } 606 newFt.op = Op.AND; 607 newFt.terms = newTerms; 608 } else { 609 // negative phrase 610 // removed 611 newFt = null; 612 } 613 } 614 return newFt; 615 } 616 617 /** 618 * Removes toplevel ANDed simple words from the query. 619 */ 620 protected static FulltextQuery removeToplevelAndedWords(FulltextQuery ft) { 621 if (ft.op == Op.OR || ft.op == Op.NOTWORD) { 622 return ft; 623 } 624 if (ft.op == Op.WORD) { 625 if (ft.isPhrase()) { 626 return ft; 627 } 628 return null; 629 } 630 List<FulltextQuery> newTerms = new LinkedList<>(); 631 for (FulltextQuery term : ft.terms) { 632 if (term.op == Op.NOTWORD) { 633 newTerms.add(term); 634 } else { // Op.WORD 635 if (term.isPhrase()) { 636 newTerms.add(term); 637 } 638 } 639 } 640 if (newTerms.isEmpty()) { 641 return null; 642 } else if (newTerms.size() == 1) { 643 // single-term parenthesis elimination 644 return newTerms.get(0); 645 } else { 646 FulltextQuery newFt = new FulltextQuery(); 647 newFt.op = Op.AND; 648 newFt.terms = newTerms; 649 return newFt; 650 } 651 } 652 653 // turn non-toplevel ANDed single words into SQL 654 // abc "foo bar" -"gee man" 655 // -> ?? LIKE '% foo bar %' AND ?? NOT LIKE '% gee man %' 656 // ?? is a pseudo-parameter for the col 657 protected static void generateLikeSql(FulltextQuery ft, StringBuilder buf) { 658 if (ft.op == Op.AND || ft.op == Op.OR) { 659 buf.append('('); 660 boolean first = true; 661 for (FulltextQuery term : ft.terms) { 662 if (!first) { 663 if (ft.op == Op.AND) { 664 buf.append(" AND "); 665 } else { // Op.OR 666 buf.append(" OR "); 667 } 668 } 669 first = false; 670 generateLikeSql(term, buf); 671 } 672 buf.append(')'); 673 } else { 674 buf.append(FT_LIKE_COL); 675 if (ft.op == Op.NOTWORD) { 676 buf.append(" NOT"); 677 } 678 buf.append(" LIKE '% "); 679 String word = ft.word.toLowerCase(); 680 // SQL escaping 681 word = word.replace("'", "''"); 682 word = word.replace("\\", ""); // don't take chances 683 word = word.replace(PREFIX_SEARCH, "%"); 684 buf.append(word); 685 if (!word.endsWith("%")) { 686 buf.append(" %"); 687 } 688 buf.append("'"); 689 } 690 } 691 692 // OLD having problems in pre-9.2 (NXP-9228) 693 // SELECT ..., 694 // TS_RANK_CD(NX_TO_TSVECTOR(fulltext), nxquery, 32) as nxscore 695 // FROM ... 696 // LEFT JOIN fulltext ON fulltext.id = hierarchy.id, 697 // TO_TSQUERY('french', ?) nxquery 698 // WHERE ... 699 // AND nxquery @@ NX_TO_TSVECTOR(fulltext) 700 // AND fulltext LIKE '% foo bar %' -- when phrase search 701 // ORDER BY nxscore DESC 702 703 // NEW 704 // SELECT ..., 705 // TS_RANK_CD(NX_TO_TSVECTOR(fulltext), TO_TSQUERY('french', ?), 32) as 706 // nxscore 707 // FROM ... 708 // LEFT JOIN fulltext ON fulltext.id = hierarchy.id 709 // WHERE ... 710 // AND TO_TSQUERY('french', ?) @@ NX_TO_TSVECTOR(fulltext) 711 // AND fulltext LIKE '% foo bar %' -- when phrase search 712 // ORDER BY nxscore DESC 713 @Override 714 public FulltextMatchInfo getFulltextScoredMatchInfo(String fulltextQuery, String indexName, int nthMatch, 715 Column mainColumn, Model model, Database database) { 716 String indexSuffix = model.getFulltextIndexSuffix(indexName); 717 Table ft = database.getTable(Model.FULLTEXT_TABLE_NAME); 718 Column ftMain = ft.getColumn(Model.MAIN_KEY); 719 Column ftColumn = ft.getColumn(Model.FULLTEXT_FULLTEXT_KEY + indexSuffix); 720 String ftColumnName = ftColumn.getFullQuotedName(); 721 String nthSuffix = nthMatch == 1 ? "" : String.valueOf(nthMatch); 722 FulltextMatchInfo info = new FulltextMatchInfo(); 723 info.joins = new ArrayList<>(); 724 if (nthMatch == 1) { 725 // Need only one JOIN involving the fulltext table 726 info.joins.add(new Join(Join.INNER, ft.getQuotedName(), null, null, ftMain.getFullQuotedName(), 727 mainColumn.getFullQuotedName())); 728 } 729 /* 730 * for phrase search, fulltextQuery may contain a LIKE part 731 */ 732 String like; 733 if (fulltextQuery.contains(FT_LIKE_SEP)) { 734 String[] tmp = fulltextQuery.split(FT_LIKE_SEP, 2); 735 fulltextQuery = tmp[0]; 736 like = tmp[1].replace(FT_LIKE_COL, ftColumnName); 737 } else { 738 like = null; 739 } 740 String tsquery = String.format("TO_TSQUERY('%s', ?)", fulltextAnalyzer); 741 String tsvector; 742 if (compatibilityFulltextTable) { 743 tsvector = ftColumnName; 744 } else { 745 tsvector = String.format("NX_TO_TSVECTOR(%s)", ftColumnName); 746 } 747 String where = String.format("(%s @@ %s)", tsquery, tsvector); 748 if (like != null) { 749 where += " AND (" + like + ")"; 750 } 751 info.whereExpr = where; 752 info.whereExprParam = fulltextQuery; 753 info.scoreExpr = String.format("TS_RANK_CD(%s, %s, 32)", tsvector, tsquery); 754 info.scoreExprParam = fulltextQuery; 755 info.scoreAlias = "_nxscore" + nthSuffix; 756 info.scoreCol = new Column(mainColumn.getTable(), null, ColumnType.DOUBLE, null); 757 return info; 758 } 759 760 @Override 761 public boolean getMaterializeFulltextSyntheticColumn() { 762 return true; 763 } 764 765 @Override 766 public int getFulltextIndexedColumns() { 767 return 1; 768 } 769 770 @Override 771 public String getFreeVariableSetterForType(ColumnType type) { 772 if (type == ColumnType.FTSTORED && compatibilityFulltextTable) { 773 return "NX_TO_TSVECTOR(?)"; 774 } 775 return "?"; 776 } 777 778 @Override 779 public boolean supportsUpdateFrom() { 780 return true; 781 } 782 783 @Override 784 public boolean doesUpdateFromRepeatSelf() { 785 return false; 786 } 787 788 @Override 789 public boolean needsAliasForDerivedTable() { 790 return true; 791 } 792 793 @Override 794 public boolean supportsIlike() { 795 return true; 796 } 797 798 @Override 799 public boolean supportsReadAcl() { 800 return aclOptimizationsEnabled; 801 } 802 803 @Override 804 public String getPrepareUserReadAclsSql() { 805 return "SELECT nx_prepare_user_read_acls(?)"; 806 } 807 808 @Override 809 public String getReadAclsCheckSql(String userIdCol) { 810 return String.format("%s = md5(array_to_string(?, '%s'))", userIdCol, getUsersSeparator()); 811 } 812 813 @Override 814 public String getUpdateReadAclsSql() { 815 return "SELECT nx_update_read_acls();"; 816 } 817 818 @Override 819 public String getRebuildReadAclsSql() { 820 return "SELECT nx_rebuild_read_acls();"; 821 } 822 823 @Override 824 public String getSecurityCheckSql(String idColumnName) { 825 return String.format("NX_ACCESS_ALLOWED(%s, ?, ?)", idColumnName); 826 } 827 828 @Override 829 public boolean supportsAncestorsTable() { 830 return true; 831 } 832 833 @Override 834 public boolean supportsFastDescendants() { 835 return pathOptimizationsEnabled; 836 } 837 838 @Override 839 public String getInTreeSql(String idColumnName, String id) { 840 String cast; 841 try { 842 cast = getCastForId(id); 843 } catch (IllegalArgumentException e) { 844 // discard query with invalid id 845 return null; 846 } 847 if (pathOptimizationsEnabled) { 848 return String.format("EXISTS(SELECT 1 FROM ancestors WHERE id = %s AND ARRAY[?]%s <@ ancestors)", 849 idColumnName, getCastForArray(cast)); 850 } 851 return String.format("%s IN (SELECT * FROM nx_children(?%s))", idColumnName, cast); 852 } 853 854 protected String getCastForArray(String cast) { 855 if (cast.isEmpty()) { 856 return cast; 857 } 858 return cast + "[]"; 859 } 860 861 protected String getCastForId(String id) { 862 String ret; 863 switch (idType) { 864 case VARCHAR: 865 return ""; 866 case UUID: 867 // check that it's really a uuid 868 if (id != null) { 869 UUID.fromString(id); 870 } 871 ret = "::uuid"; 872 break; 873 case SEQUENCE: 874 // check that it's really an integer 875 if (id != null && !StringUtils.isNumeric(id)) { 876 throw new IllegalArgumentException("Invalid sequence id: " + id); 877 } 878 ret = "::bigint"; 879 break; 880 default: 881 throw new AssertionError("Unknown id type: " + idType); 882 } 883 return ret; 884 } 885 886 @Override 887 public String getMatchMixinType(Column mixinsColumn, String mixin, boolean positive, String[] returnParam) { 888 returnParam[0] = mixin; 889 String sql = "ARRAY[?]::varchar[] <@ " + mixinsColumn.getFullQuotedName(); 890 return positive ? sql : "NOT(" + sql + ")"; 891 } 892 893 @Override 894 public boolean supportsSysNameArray() { 895 return true; 896 } 897 898 @Override 899 public boolean supportsArrays() { 900 return true; 901 } 902 903 @Override 904 public boolean supportsArrayColumns() { 905 return true; 906 } 907 908 public static class ArraySubQueryPostgreSQL extends ArraySubQuery { 909 910 protected Dialect dialect = null; 911 912 protected Table fakeSubqueryTableAlias = null; 913 914 public ArraySubQueryPostgreSQL(Column arrayColumn, String alias) { 915 super(arrayColumn, alias); 916 dialect = arrayColumn.getTable().getDialect(); 917 fakeSubqueryTableAlias = new TableAlias(arrayColumn.getTable(), alias); 918 } 919 920 @Override 921 public Column getSubQueryIdColumn() { 922 Column column = fakeSubqueryTableAlias.getColumn(Model.MAIN_KEY); 923 return new ArraySubQueryPostgreSQLColumn(column.getPhysicalName(), column.getType()); 924 } 925 926 @Override 927 public Column getSubQueryValueColumn() { 928 return new ArraySubQueryPostgreSQLColumn(Model.COLL_TABLE_VALUE_KEY, arrayColumn.getBaseType()); 929 } 930 931 public class ArraySubQueryPostgreSQLColumn extends Column { 932 private static final long serialVersionUID = 1L; 933 934 ArraySubQueryPostgreSQLColumn(String columnName, ColumnType columnType) { 935 super(fakeSubqueryTableAlias, columnName, columnType, columnName); 936 } 937 938 @Override 939 public String getFullQuotedName() { 940 return dialect.openQuote() + subQueryAlias + dialect.closeQuote() + '.' + getQuotedName(); 941 } 942 } 943 944 @Override 945 public String toSql() { 946 Table table = arrayColumn.getTable(); 947 return String.format("(SELECT %s, UNNEST(%s) AS %s, generate_subscripts(%s, 1) AS %s FROM %s) ", 948 table.getColumn(Model.MAIN_KEY).getQuotedName(), arrayColumn.getQuotedName(), 949 Model.COLL_TABLE_VALUE_KEY, arrayColumn.getQuotedName(), Model.COLL_TABLE_POS_KEY, 950 table.getRealTable().getQuotedName()); 951 } 952 } 953 954 @Override 955 public ArraySubQuery getArraySubQuery(Column arrayColumn, String subQueryAlias) { 956 return new ArraySubQueryPostgreSQL(arrayColumn, subQueryAlias); 957 } 958 959 @Override 960 public String getArrayElementString(String arrayColumnName, int arrayElementIndex) { 961 // PostgreSQL arrays index start at 1 962 return arrayColumnName + "[" + (arrayElementIndex + 1) + "]"; 963 } 964 965 @Override 966 public String getArrayInSql(Column arrayColumn, String cast, boolean positive, List<Serializable> params) { 967 StringBuilder sql = new StringBuilder(); 968 if (!positive) { 969 sql.append("(NOT("); 970 } 971 if (params.size() == 1) { 972 // ? = ANY(arrayColumn) 973 sql.append("? = ANY("); 974 sql.append(arrayColumn.getFullQuotedName()); 975 if (cast != null) { 976 // DATE cast 977 sql.append("::"); 978 sql.append(cast); 979 sql.append("[]"); 980 } 981 sql.append(")"); 982 } else { 983 // arrayColumn && ARRAY[?, ?, ?] 984 sql.append(arrayColumn.getFullQuotedName()); 985 sql.append(" && "); 986 sql.append("ARRAY["); 987 for (int i = 0; i < params.size(); i++) { 988 if (i != 0) { 989 sql.append(", "); 990 } 991 sql.append('?'); 992 } 993 sql.append("]::"); 994 sql.append(arrayColumn.getSqlTypeString()); 995 } 996 if (!positive) { 997 sql.append(") OR "); 998 sql.append(arrayColumn.getFullQuotedName()); 999 sql.append(" IS NULL)"); 1000 } 1001 return sql.toString(); 1002 } 1003 1004 @Override 1005 public String getArrayLikeSql(Column arrayColumn, String refName, boolean positive, Table dataHierTable) { 1006 return getArrayOpSql(arrayColumn, refName, positive, dataHierTable, "LIKE"); 1007 } 1008 1009 @Override 1010 public String getArrayIlikeSql(Column arrayColumn, String refName, boolean positive, Table dataHierTable) { 1011 return getArrayOpSql(arrayColumn, refName, positive, dataHierTable, "ILIKE"); 1012 } 1013 1014 protected String getArrayOpSql(Column arrayColumn, String refName, boolean positive, Table dataHierTable, 1015 String op) { 1016 Table table = arrayColumn.getTable(); 1017 String tableAliasName = openQuote() + getTableName(refName) + closeQuote(); 1018 String sql = String.format("EXISTS (SELECT 1 FROM %s AS %s WHERE %s = %s AND %s %s ?)", 1019 getArraySubQuery(arrayColumn, tableAliasName).toSql(), tableAliasName, 1020 dataHierTable.getColumn(Model.MAIN_KEY).getFullQuotedName(), 1021 tableAliasName + '.' + table.getColumn(Model.MAIN_KEY).getQuotedName(), 1022 tableAliasName + '.' + Model.COLL_TABLE_VALUE_KEY, op); 1023 if (!positive) { 1024 sql = "NOT(" + sql + ")"; 1025 } 1026 return sql; 1027 } 1028 1029 @Override 1030 public Array createArrayOf(int type, Object[] elements, Connection connection) throws SQLException { 1031 if (elements == null || elements.length == 0) { 1032 return null; 1033 } 1034 String typeName; 1035 switch (type) { 1036 case Types.VARCHAR: 1037 typeName = "varchar"; 1038 break; 1039 case Types.CLOB: 1040 typeName = "text"; 1041 break; 1042 case Types.BIT: 1043 typeName = "bool"; 1044 break; 1045 case Types.BIGINT: 1046 typeName = "int8"; 1047 break; 1048 case Types.DOUBLE: 1049 typeName = "float8"; 1050 break; 1051 case Types.TIMESTAMP: 1052 typeName = "timestamp"; 1053 break; 1054 case Types.SMALLINT: 1055 typeName = "int2"; 1056 break; 1057 case Types.INTEGER: 1058 typeName = "int4"; 1059 break; 1060 case Types.OTHER: // id 1061 switch (idType) { 1062 case VARCHAR: 1063 typeName = "varchar"; 1064 break; 1065 case UUID: 1066 typeName = "uuid"; 1067 break; 1068 case SEQUENCE: 1069 typeName = "int8"; 1070 break; 1071 default: 1072 throw new AssertionError("Unknown id type: " + idType); 1073 } 1074 break; 1075 default: 1076 throw new AssertionError("Unknown type: " + type); 1077 } 1078 return connection.createArrayOf(typeName, elements); 1079 } 1080 1081 @Override 1082 public String getSQLStatementsFilename() { 1083 return "nuxeovcs/postgresql.sql.txt"; 1084 } 1085 1086 @Override 1087 public String getTestSQLStatementsFilename() { 1088 return "nuxeovcs/postgresql.test.sql.txt"; 1089 } 1090 1091 @Override 1092 public Map<String, Serializable> getSQLStatementsProperties(Model model, Database database) { 1093 Map<String, Serializable> properties = new HashMap<>(); 1094 switch (idType) { 1095 case VARCHAR: 1096 properties.put("idType", "varchar(36)"); 1097 properties.put("idTypeParam", "varchar"); 1098 properties.put("idNotPresent", "'-'"); 1099 properties.put("sequenceEnabled", Boolean.FALSE); 1100 break; 1101 case UUID: 1102 properties.put("idType", "uuid"); 1103 properties.put("idTypeParam", "uuid"); 1104 properties.put("idNotPresent", "'00000000-FFFF-FFFF-FFFF-FFFF00000000'"); 1105 properties.put("sequenceEnabled", Boolean.FALSE); 1106 break; 1107 case SEQUENCE: 1108 properties.put("idType", "int8"); 1109 properties.put("idTypeParam", "int8"); 1110 properties.put("idNotPresent", "-1"); 1111 properties.put("sequenceEnabled", Boolean.TRUE); 1112 properties.put("idSequenceName", idSequenceName); 1113 } 1114 properties.put("aclOptimizationsEnabled", Boolean.valueOf(aclOptimizationsEnabled)); 1115 properties.put("pathOptimizationsEnabled", Boolean.valueOf(pathOptimizationsEnabled)); 1116 properties.put("fulltextAnalyzer", fulltextAnalyzer); 1117 properties.put("fulltextEnabled", Boolean.valueOf(!fulltextDisabled)); 1118 properties.put("fulltextSearchEnabled", Boolean.valueOf(!fulltextSearchDisabled)); 1119 properties.put("clusteringEnabled", Boolean.valueOf(clusteringEnabled)); 1120 properties.put("proxiesEnabled", Boolean.valueOf(proxiesEnabled)); 1121 properties.put("softDeleteEnabled", Boolean.valueOf(softDeleteEnabled)); 1122 properties.put("arrayColumnsEnabled", Boolean.valueOf(arrayColumnsEnabled)); 1123 if (!fulltextSearchDisabled) { 1124 Table ft = database.getTable(Model.FULLTEXT_TABLE_NAME); 1125 FulltextConfiguration fti = model.getFulltextConfiguration(); 1126 List<String> lines = new ArrayList<>(fti.indexNames.size()); 1127 for (String indexName : fti.indexNames) { 1128 String suffix = model.getFulltextIndexSuffix(indexName); 1129 Column ftft = ft.getColumn(Model.FULLTEXT_FULLTEXT_KEY + suffix); 1130 Column ftst = ft.getColumn(Model.FULLTEXT_SIMPLETEXT_KEY + suffix); 1131 Column ftbt = ft.getColumn(Model.FULLTEXT_BINARYTEXT_KEY + suffix); 1132 String concat; 1133 if (compatibilityFulltextTable) { 1134 // tsvector 1135 concat = " NEW.%s := COALESCE(NEW.%s, ''::TSVECTOR) || COALESCE(NEW.%s, ''::TSVECTOR);"; 1136 } else { 1137 // text with space at beginning and end 1138 concat = " NEW.%s := ' ' || COALESCE(NEW.%s, '') || ' ' || COALESCE(NEW.%s, '') || ' ';"; 1139 } 1140 String line = String.format(concat, ftft.getQuotedName(), ftst.getQuotedName(), ftbt.getQuotedName()); 1141 lines.add(line); 1142 } 1143 properties.put("fulltextTriggerStatements", String.join("\n", lines)); 1144 } 1145 String[] permissions = NXCore.getSecurityService().getPermissionsToCheck(SecurityConstants.BROWSE); 1146 List<String> permsList = new LinkedList<>(); 1147 for (String perm : permissions) { 1148 permsList.add("('" + perm + "')"); 1149 } 1150 properties.put("readPermissions", String.join(", ", permsList)); 1151 properties.put("usersSeparator", getUsersSeparator()); 1152 properties.put("everyone", SecurityConstants.EVERYONE); 1153 properties.put("readAclMaxSize", Integer.toString(readAclMaxSize)); 1154 properties.put("unlogged", unloggedKeyword); 1155 return properties; 1156 } 1157 1158 @Override 1159 public List<String> getStartupSqls(Model model, Database database) { 1160 if (aclOptimizationsEnabled) { 1161 log.info("Vacuuming tables used by optimized acls"); 1162 return Collections.singletonList("SELECT nx_vacuum_read_acls()"); 1163 } 1164 return Collections.emptyList(); 1165 } 1166 1167 @Override 1168 public boolean isClusteringSupported() { 1169 return true; 1170 } 1171 1172 @Override 1173 public String getClusterInsertInvalidations() { 1174 return "SELECT NX_CLUSTER_INVAL(?, ?, ?, ?)"; 1175 } 1176 1177 @Override 1178 public String getClusterGetInvalidations() { 1179 return "DELETE FROM cluster_invals WHERE nodeid = ? RETURNING id, fragments, kind"; 1180 } 1181 1182 @Override 1183 public boolean isConcurrentUpdateException(Throwable t) { 1184 while (t.getCause() != null) { 1185 t = t.getCause(); 1186 } 1187 if (t instanceof SQLException) { 1188 String sqlState = ((SQLException) t).getSQLState(); 1189 if ("23503".equals(sqlState)) { 1190 // insert or update on table ... violates foreign key constraint 1191 return true; 1192 } 1193 if ("23505".equals(sqlState)) { 1194 // duplicate key value violates unique constraint 1195 return true; 1196 } 1197 if ("40P01".equals(sqlState)) { 1198 // deadlock detected 1199 return true; 1200 } 1201 } 1202 return false; 1203 } 1204 1205 @Override 1206 public boolean supportsPaging() { 1207 return true; 1208 } 1209 1210 @Override 1211 public String addPagingClause(String sql, long limit, long offset) { 1212 return sql + String.format(" LIMIT %d OFFSET %d", limit, offset); 1213 } 1214 1215 @Override 1216 public boolean supportsWith() { 1217 return false; // don't activate until proven useful 1218 // return supportsWith; 1219 } 1220 1221 @Override 1222 public void performAdditionalStatements(Connection connection) throws SQLException { 1223 // Check if there is a pre-existing aclr_permission table 1224 boolean createAclrPermission; 1225 try (Statement s = connection.createStatement()) { 1226 String sql = "SELECT 1 FROM pg_tables WHERE tablename = 'aclr_permission'"; 1227 try (ResultSet rs = s.executeQuery(sql)) { 1228 createAclrPermission = !rs.next(); 1229 } 1230 } 1231 // If no table, it will be created and filled at DDL execution time 1232 if (createAclrPermission) { 1233 return; 1234 } 1235 // Warn user if BROWSE permissions has changed 1236 Set<String> dbPermissions = new HashSet<>(); 1237 try (Statement s = connection.createStatement()) { 1238 String sql = "SELECT * FROM aclr_permission"; 1239 try (ResultSet rs = s.executeQuery(sql)) { 1240 while (rs.next()) { 1241 dbPermissions.add(rs.getString(1)); 1242 } 1243 } 1244 } 1245 SecurityService securityService = NXCore.getSecurityService(); 1246 Set<String> confPermissions = new HashSet<>( 1247 Arrays.asList(securityService.getPermissionsToCheck(SecurityConstants.BROWSE))); 1248 if (!dbPermissions.equals(confPermissions)) { 1249 log.error("Security permission for BROWSE has changed, you need to rebuild the optimized read acls:" 1250 + "DROP TABLE aclr_permission; DROP TABLE aclr; then restart."); 1251 } 1252 } 1253 1254 public String getUsersSeparator() { 1255 if (usersSeparator == null) { 1256 return DEFAULT_USERS_SEPARATOR; 1257 } 1258 return usersSeparator; 1259 } 1260 1261 @Override 1262 public String getValidationQuery() { 1263 return ""; 1264 } 1265 1266 @Override 1267 public String getAncestorsIdsSql() { 1268 return "SELECT NX_ANCESTORS(?)"; 1269 } 1270 1271 @Override 1272 public boolean needsNullsLastOnDescSort() { 1273 return true; 1274 } 1275 1276 @Override 1277 public String getDateCast() { 1278 // this is more amenable to being indexed than a CAST 1279 return "DATE(%s)"; 1280 } 1281 1282 @Override 1283 public String castIdToVarchar(String expr) { 1284 switch (idType) { 1285 case VARCHAR: 1286 return expr; 1287 case UUID: 1288 return expr + "::varchar"; 1289 case SEQUENCE: 1290 return expr + "::varchar"; 1291 default: 1292 throw new AssertionError("Unknown id type: " + idType); 1293 } 1294 } 1295 1296 @Override 1297 public DialectIdType getIdType() { 1298 return idType; 1299 } 1300 1301 @Override 1302 public String getSoftDeleteSql() { 1303 return "SELECT NX_DELETE(?, ?)"; 1304 } 1305 1306 @Override 1307 public String getSoftDeleteCleanupSql() { 1308 return "SELECT NX_DELETE_PURGE(?, ?)"; 1309 } 1310 1311 @Override 1312 public String getBinaryFulltextSql(List<String> columns) { 1313 if (compatibilityFulltextTable) { 1314 // extract tokens from tsvector 1315 String columnsAs = columns.stream() 1316 .map(col -> "regexp_replace(" + col + "::text, $$'|'\\:[^']*'?$$, ' ', 'g')") 1317 .collect(Collectors.joining(", ")); 1318 return "SELECT " + columnsAs + " FROM fulltext WHERE id=?"; 1319 } 1320 return super.getBinaryFulltextSql(columns); 1321 } 1322 1323 // parenthesizes parameter part, with optional nested parentheses 1324 private static final Pattern SIG_MATCH = Pattern.compile("[^(]*\\((([^()]*|\\([^()]*\\))*)\\).*", Pattern.DOTALL); 1325 1326 @Override 1327 public List<String> checkStoredProcedure(String procName, String procCreate, String ddlMode, Connection connection, 1328 JDBCLogger logger, Map<String, Serializable> properties) throws SQLException { 1329 boolean compatCheck = ddlMode.contains(RepositoryDescriptor.DDL_MODE_COMPAT); 1330 if (compatCheck) { 1331 procCreate = "CREATE OR REPLACE " + procCreate.substring("create ".length()); 1332 return Collections.singletonList(procCreate); 1333 } 1334 // extract signature from create statement 1335 Matcher m = SIG_MATCH.matcher(procCreate); 1336 if (!m.matches()) { 1337 throw new NuxeoException("Cannot parse arguments: " + procCreate); 1338 } 1339 String procArgs = normalizeArgs(m.group(1)); 1340 try (Statement st = connection.createStatement()) { 1341 // check if the stored procedure exists and its content 1342 String getBody = "SELECT prosrc, pg_get_function_identity_arguments(oid) FROM pg_proc WHERE proname = '" 1343 + procName + "'"; 1344 logger.log(getBody); 1345 try (ResultSet rs = st.executeQuery(getBody)) { 1346 while (rs.next()) { 1347 String body = rs.getString(1); 1348 String args = rs.getString(2); 1349 if (!args.equals(procArgs)) { 1350 // different signature 1351 continue; 1352 } 1353 // stored proc already exists 1354 if (normalizeString(procCreate).contains(normalizeString(body))) { 1355 logger.log(" -> exists, unchanged"); 1356 return Collections.emptyList(); 1357 } else { 1358 logger.log(" -> exists, old"); 1359 // we can't drop then recreate as for instance a function used by a trigger 1360 // would say "cannot drop function ... because other objects depend on it" 1361 // so we hack and do an do a replace 1362 if (!procCreate.toLowerCase().startsWith("create ")) { 1363 throw new NuxeoException("Should start with CREATE: " + procCreate); 1364 } 1365 procCreate = "CREATE OR REPLACE " + procCreate.substring("create ".length()); 1366 return Collections.singletonList(procCreate); 1367 } 1368 } 1369 } 1370 logger.log(" -> missing"); 1371 return Collections.singletonList(procCreate); 1372 } 1373 } 1374 1375 protected static String normalizeString(String string) { 1376 return string.replaceAll("[ \n\r\t]+", " ").trim(); 1377 } 1378 1379 /** The type aliases that we use for our stored procedure argument definitions. */ 1380 private static final Map<String, String> TYPE_ALIASES = new HashMap<>(); 1381 1382 static { 1383 TYPE_ALIASES.put("bool", "boolean"); 1384 TYPE_ALIASES.put("varchar", "character varying"); 1385 TYPE_ALIASES.put("int", "integer"); 1386 TYPE_ALIASES.put("int4", "integer"); 1387 TYPE_ALIASES.put("int8", "bigint"); 1388 TYPE_ALIASES.put("timestamp", "timestamp without time zone"); 1389 } 1390 1391 /** Normalize PostgreSQL type aliases. */ 1392 protected static String normalizeArgs(String args) { 1393 if (args.isEmpty()) { 1394 return args; 1395 } 1396 args = args.toLowerCase(); 1397 List<String> argList = Arrays.asList(args.split(",[ ]*")); 1398 List<String> newArgList = new ArrayList<>(argList.size()); 1399 for (String arg : argList) { 1400 // array or size spec 1401 int i = arg.indexOf('('); 1402 if (i == -1) { 1403 i = arg.indexOf('['); 1404 } 1405 String suffix = ""; 1406 if (i > 0) { 1407 suffix = arg.substring(i); 1408 arg = arg.substring(0, i); 1409 } 1410 for (Entry<String, String> es : TYPE_ALIASES.entrySet()) { 1411 String type = es.getKey(); 1412 if (arg.equals(type) || arg.endsWith(" " + type)) { 1413 arg = arg.substring(0, arg.length() - type.length()) + es.getValue(); 1414 break; 1415 } 1416 } 1417 newArgList.add(arg + suffix); 1418 } 1419 return String.join(", ", newArgList); 1420 } 1421 1422}