001: /**
002: * Sequoia: Database clustering technology.
003: * Copyright (C) 2002-2004 French National Institute For Research In Computer
004: * Science And Control (INRIA).
005: * Copyright (C) 2005-2006 Continuent, Inc.
006: * Contact: sequoia@continuent.org
007: *
008: * Licensed under the Apache License, Version 2.0 (the "License");
009: * you may not use this file except in compliance with the License.
010: * You may obtain a copy of the License at
011: *
012: * http://www.apache.org/licenses/LICENSE-2.0
013: *
014: * Unless required by applicable law or agreed to in writing, software
015: * distributed under the License is distributed on an "AS IS" BASIS,
016: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
017: * See the License for the specific language governing permissions and
018: * limitations under the License.
019: *
020: * Initial developer(s): Emmanuel Cecchet.
021: * Contributor(s): Mathieu Peltier.
022: */package org.continuent.sequoia.controller.requests;
023:
024: import java.io.Serializable;
025: import java.sql.SQLException;
026: import java.util.ArrayList;
027: import java.util.HashMap;
028: import java.util.StringTokenizer;
029: import java.util.TreeSet;
030:
031: import org.continuent.sequoia.common.i18n.Translate;
032: import org.continuent.sequoia.common.sql.schema.DatabaseColumn;
033: import org.continuent.sequoia.common.sql.schema.DatabaseSchema;
034: import org.continuent.sequoia.common.sql.schema.DatabaseTable;
035: import org.continuent.sequoia.common.sql.schema.TableColumn;
036:
037: /**
038: * An <code>UpdateRequest</code> is an SQL request with the following syntax:
039: *
040: * <pre>
041: * UPDATE table-name SET (column-name=expression[,column-name=expression]*) WHERE search-condition
042: * </pre>
043: *
044: * @author <a href="mailto:Emmanuel.Cecchet@inria.fr">Emmanuel Cecchet </a>
045: * @author <a href="mailto:Mathieu.Peltier@inrialpes.fr">Mathieu Peltier </a>
046: * @version 1.0
047: */
048: public class UpdateRequest extends AbstractWriteRequest implements
049: Serializable {
050: private static final long serialVersionUID = 1943340529813559587L;
051:
052: /** <code>true</code> if this request updates a <code>UNIQUE</code> row. */
053: protected transient boolean isUnique;
054:
055: protected transient HashMap updatedValues = null;
056:
057: /**
058: * Creates a new <code>UpdateRequest</code> instance. The caller must give
059: * an SQL request, without any leading or trailing spaces and beginning with
060: * 'update ' (it will not be checked).
061: * <p>
062: * The request is not parsed but it can be done later by a call to
063: * {@link #parse(DatabaseSchema, int, boolean)}.
064: *
065: * @param sqlQuery the SQL query
066: * @param escapeProcessing should the driver to escape processing before
067: * sending to the database ?
068: * @param timeout an <code>int</code> value
069: * @param lineSeparator the line separator used in the query
070: * @see #parse
071: */
072: public UpdateRequest(String sqlQuery, boolean escapeProcessing,
073: int timeout, String lineSeparator) {
074: super (sqlQuery, escapeProcessing, timeout, lineSeparator,
075: RequestType.UPDATE);
076: }
077:
078: /**
079: * @see org.continuent.sequoia.controller.requests.AbstractRequest#altersAggregateList()
080: */
081: public boolean altersAggregateList() {
082: return false;
083: }
084:
085: /**
086: * @see org.continuent.sequoia.controller.requests.AbstractRequest#altersDatabaseCatalog()
087: */
088: public boolean altersDatabaseCatalog() {
089: return false;
090: }
091:
092: /**
093: * @see org.continuent.sequoia.controller.requests.AbstractRequest#altersDatabaseSchema()
094: */
095: public boolean altersDatabaseSchema() {
096: return false;
097: }
098:
099: /**
100: * @see org.continuent.sequoia.controller.requests.AbstractRequest#altersMetadataCache()
101: */
102: public boolean altersMetadataCache() {
103: return false;
104: }
105:
106: /**
107: * @see org.continuent.sequoia.controller.requests.AbstractRequest#altersQueryResultCache()
108: */
109: public boolean altersQueryResultCache() {
110: return true;
111: }
112:
113: /**
114: * @see org.continuent.sequoia.controller.requests.AbstractRequest#altersSomething()
115: */
116: public boolean altersSomething() {
117: return true;
118: }
119:
120: /**
121: * @see org.continuent.sequoia.controller.requests.AbstractRequest#altersStoredProcedureList()
122: */
123: public boolean altersStoredProcedureList() {
124: return false;
125: }
126:
127: /**
128: * @see org.continuent.sequoia.controller.requests.AbstractRequest#altersUserDefinedTypes()
129: */
130: public boolean altersUserDefinedTypes() {
131: return false;
132: }
133:
134: /**
135: * @see org.continuent.sequoia.controller.requests.AbstractRequest#altersUsers()
136: */
137: public boolean altersUsers() {
138: return false;
139: }
140:
141: /**
142: * @see AbstractRequest#cloneParsing(AbstractRequest)
143: */
144: public void cloneParsing(AbstractRequest request) {
145: if (!request.isParsed())
146: return;
147: cloneTableNameAndColumns((AbstractWriteRequest) request);
148: updatedValues = ((UpdateRequest) request).getUpdatedValues();
149: isParsed = true;
150: }
151:
152: /**
153: * What are the updated values in this request
154: *
155: * @return a hashtable of (colname,value) or null if parsing granularity has
156: * stop computation
157: */
158: public HashMap getUpdatedValues() {
159: return updatedValues;
160: }
161:
162: /**
163: * Returns <code>true</code> as this request updates a <code>UNIQUE</code>
164: * row.
165: *
166: * @return <code>false</code>
167: */
168: public boolean isUnique() {
169: return isUnique;
170: }
171:
172: /**
173: * @see org.continuent.sequoia.controller.requests.AbstractRequest#needsMacroProcessing()
174: */
175: public boolean needsMacroProcessing() {
176: return true;
177: }
178:
179: /**
180: * Parses the SQL request and extract the selected columns and tables given
181: * the <code>DatabaseSchema</code> of the database targeted by this request.
182: * Determines also if this query only deletes a single row, and the equivalent
183: * <code>INSERT</code> statement.
184: * <p>
185: * An exception is thrown when the parsing fails. Warning, this method does
186: * not check the validity of the request. In particular, invalid request could
187: * be parsed without throwing an exception. However, valid SQL request should
188: * never throw an exception.
189: *
190: * @param schema a <code>DatabaseSchema</code> value
191: * @param granularity parsing granularity as defined in
192: * <code>ParsingGranularities</code>
193: * @param isCaseSensitive true if table name parsing is case sensitive
194: * @exception SQLException if the parsing fails
195: */
196: public void parse(DatabaseSchema schema, int granularity,
197: boolean isCaseSensitive) throws SQLException {
198: if (granularity == ParsingGranularities.NO_PARSING) {
199: isParsed = true;
200: return;
201: }
202:
203: String whereClause = null;
204: isUnique = true;
205:
206: String originalSQL = this .trimCarriageReturnAndTabs();
207: String sql = originalSQL.toLowerCase();
208:
209: // Strip 'update '
210: sql = sql.substring(7).trim();
211:
212: // Look for the SET or WHERE clause
213: int setIdx = sql.indexOf(" set ");
214: if (setIdx == -1)
215: throw new SQLException(
216: "Unable to find the SET keyword in this UPDATE statement: '"
217: + sqlQueryOrTemplate + "'");
218:
219: int whereIdx = sql.indexOf(" where ");
220: if (whereIdx == -1)
221: whereIdx = sql.indexOf(")where ");
222:
223: if (isCaseSensitive)
224: sql = originalSQL.substring(7).trim();
225:
226: if (whereIdx == -1) {
227: whereIdx = sql.length();
228: isUnique = false;
229: } else {
230: whereIdx++;
231: // whereIdx now points to the 'w' and not the preceding
232: // character ' ' or ')'
233: whereClause = sql.substring(whereIdx + 5);
234: // 5 = "where".length(), do not trim or remove anything after
235: // else the following code will no more work
236: sql = sql.substring(0, whereIdx + 1).trim();
237: }
238:
239: tableName = sql.substring(0, setIdx).trim();
240:
241: if (schema == null) {
242: writeLockedTables = new TreeSet();
243: writeLockedTables.add(tableName);
244: isParsed = true;
245: return;
246: }
247:
248: // Get the table on which UPDATE occurs
249: DatabaseTable t = schema.getTable(tableName, isCaseSensitive);
250: if (t == null)
251: throw new SQLException("Unknown table '" + tableName
252: + "' in this UPDATE statement: '"
253: + sqlQueryOrTemplate + "'");
254: else
255: // Get the real name here (resolves case sentivity problems)
256: tableName = t.getName();
257:
258: // Lock this table in write
259: writeLockedTables = new TreeSet();
260: writeLockedTables.add(tableName);
261: addDependingTables(schema, writeLockedTables);
262:
263: if (granularity > ParsingGranularities.TABLE) {
264: // We have to get the affected columns
265: // Column names are separated by comas and are before a '=' symbol
266: StringTokenizer columnTokens = new StringTokenizer(sql
267: .substring(setIdx + 5, whereIdx), ",");
268: // 5 = length(" SET ")
269: columns = new ArrayList();
270: DatabaseColumn col = null;
271: while (columnTokens.hasMoreTokens()) {
272: String token = columnTokens.nextToken();
273: int eq = token.indexOf("=");
274: if (eq == -1)
275: continue;
276: token = token.substring(0, eq).trim();
277: col = t.getColumn(token, isCaseSensitive);
278: if (col == null) {
279: tableName = null;
280: columns = null;
281: throw new SQLException("Unknown column name '"
282: + token + "' in this UPDATE statement: '"
283: + sqlQueryOrTemplate + "'");
284: } else
285: columns.add(new TableColumn(tableName, col
286: .getName()));
287: }
288: }
289:
290: isParsed = true;
291: if (!isUnique)
292: return;
293: else
294: isUnique = false;
295:
296: if (granularity < ParsingGranularities.COLUMN_UNIQUE)
297: return;
298:
299: // Prepare hashtable for updated values
300: updatedValues = new HashMap(columns.size());
301:
302: // Check whether this update affects a single row or not
303: // Instead of parsing the clause, we use a brutal force technique
304: // and we try to directly identify every column name of the table.
305: DatabaseColumn col = null;
306: ArrayList cols = t.getColumns();
307: int size = cols.size();
308: for (int j = 0; j < size; j++) {
309: col = (DatabaseColumn) cols.get(j);
310: String colName = col.getName();
311: // if pattern found and column not already in result, it's a dependency !
312: int matchIdx = whereClause.indexOf(colName);
313: while (matchIdx > 0) {
314: // Try to check that we got the full pattern and not a sub-pattern
315: char beforePattern = whereClause.charAt(matchIdx - 1);
316: if (((beforePattern >= 'a') && (beforePattern <= 'z'))
317: || ((beforePattern >= 'A') && (beforePattern <= 'Z'))
318: || (beforePattern == '_'))
319: matchIdx = whereClause.indexOf(colName,
320: matchIdx + 1);
321: else { // Ok it's a good one, check if it is UNIQUE
322: isUnique = col.isUnique();
323: if (!isUnique)
324: return;
325: // Check if this UNIQUE columns stands in the left part of an
326: // equality
327: int eq = whereClause.indexOf("=", matchIdx);
328: if ((eq == -1)
329: || (whereClause.substring(
330: matchIdx + colName.length(), eq)
331: .trim().length() > 0)) {
332: isUnique = false;
333: return;
334: }
335: do {
336: eq++; // Skip spaces
337: } while (whereClause.charAt(eq) == ' ');
338:
339: // Check if we have "..." or '...'
340: char startChar = whereClause.charAt(eq);
341: int end;
342: if ((startChar == '\'') || (startChar == '"')) {
343: eq++;
344: do { // Look for the end of the quote and take care of \' or \"
345: end = whereClause.indexOf(startChar, eq);
346: } while (whereClause.charAt(end - 1) == '\\');
347: } else {
348: // It's a regular value just find the next comma
349: end = whereClause.indexOf(",", eq);
350: if (end == -1)
351: end = whereClause.length();
352: }
353: pkValue = whereClause.substring(eq, end);
354:
355: matchIdx = whereClause.indexOf(colName,
356: matchIdx + 1);
357: }
358: }
359: }
360:
361: cacheable = RequestType.UNIQUE_CACHEABLE;
362:
363: // Now get the values for each updated field
364: sql = originalSQL.substring(7).substring(0, whereIdx).trim();
365: if (!isCaseSensitive)
366: sql = sql.toLowerCase();
367: int set = sql.toLowerCase().indexOf("set");
368: sql = sql.substring(set + 3).trim();
369:
370: for (int j = 0; j < cols.size(); j++) {
371: col = (DatabaseColumn) cols.get(j);
372: // if pattern found and column not already in result, it's a dependency !
373: String colName = (isCaseSensitive) ? col.getName() : col
374: .getName().toLowerCase();
375: int matchIdx = sql.indexOf(colName);
376:
377: while (matchIdx >= 0) {
378: char afterPattern = sql.charAt(matchIdx
379: + colName.length());
380: if ((afterPattern != '=') && (afterPattern != ' ')) {
381: matchIdx = sql.indexOf(colName, matchIdx
382: + colName.length());
383: continue;
384: }
385:
386: // Try to check that we got the full pattern and not a sub-pattern
387: char beforePattern = Character.CONTROL;
388: try {
389: beforePattern = sql.charAt(matchIdx - 1);
390: } catch (RuntimeException e) {
391: // nothing
392: }
393: if (((beforePattern >= 'a') && (beforePattern <= 'z')) // Everything
394: // should be
395: // lowercase here
396: || (beforePattern == '_'))
397: matchIdx = sql.indexOf(colName, matchIdx + 1);
398: else { // Ok, it's good, get the value on the right part of the equality
399: int eq = sql.indexOf("=", matchIdx);
400: do {
401: eq++; // Skip spaces
402: } while (sql.charAt(eq) == ' ');
403:
404: // Check if we have "..." or '...'
405: char startChar = sql.charAt(eq);
406: int end;
407: if ((startChar == '\'') || (startChar == '"')) {
408: eq++;
409: do { // Look for the end of the quote and take care of \' or \"
410: end = sql.indexOf(startChar, eq);
411: } while (sql.charAt(end - 1) == '\\');
412: } else {
413: // It's a regular value just find the next comma
414: end = sql.indexOf(",", eq);
415: if (end == -1)
416: end = sql.length();
417: }
418: updatedValues.put(col.getName(), sql.substring(eq,
419: end).trim());
420: break;
421: }
422: }
423: }
424: }
425:
426: /**
427: * Does this request returns a ResultSet?
428: *
429: * @return false
430: */
431: public boolean returnsResultSet() {
432: return false;
433: }
434:
435: /**
436: * @see org.continuent.sequoia.controller.requests.AbstractRequest#getParsingResultsAsString()
437: */
438: public String getParsingResultsAsString() {
439: StringBuffer sb = new StringBuffer(super
440: .getParsingResultsAsString());
441: sb.append(Translate.get("request.update.unique", isUnique));
442: if (updatedValues != null && updatedValues.size() > 0) {
443: sb.append(Translate.get("request.update.values"));
444: for (int i = 0; i < updatedValues.size(); i++) {
445: sb.append(Translate.get("request.update.value",
446: new String[] {
447: updatedValues.keySet().toArray()[i]
448: .toString(),
449: updatedValues.get(
450: updatedValues.keySet()
451: .toArray()[i])
452: .toString() }));
453: }
454: }
455: sb.append(Translate.get("request.alters", new String[] {
456: String.valueOf(altersAggregateList()),
457: String.valueOf(altersDatabaseCatalog()),
458: String.valueOf(altersDatabaseSchema()),
459: String.valueOf(altersMetadataCache()),
460: String.valueOf(altersQueryResultCache()),
461: String.valueOf(altersSomething()),
462: String.valueOf(altersStoredProcedureList()),
463: String.valueOf(altersUserDefinedTypes()),
464: String.valueOf(altersUsers()) }));
465: return sb.toString();
466: }
467:
468: /**
469: * Displays some debugging information about this request.
470: */
471: public void debug() {
472: super .debug();
473: if (tableName != null)
474: System.out.println("Updated table: " + tableName);
475: else
476: System.out.println("No information about updated table");
477:
478: if (columns != null) {
479: System.out.println("Updated columns:");
480: for (int i = 0; i < columns.size(); i++)
481: System.out.println(" "
482: + ((TableColumn) columns.get(i))
483: .getColumnName());
484: } else
485: System.out.println("No information about updated columns");
486:
487: System.out.println("Unique update: " + isUnique);
488:
489: System.out.println("");
490: }
491: }
|