This Bugzilla instance is a read-only archive of historic NetBeans bug reports. To report a bug in NetBeans please follow the project's instructions for reporting issues.

Bug 115075

Summary: Add DB Explorer utility API for working with SQL Identifiers
Product: db Reporter: David Vancouvering <davidvc>
Component: CodeAssignee: David Vancouvering <davidvc>
Status: RESOLVED FIXED    
Severity: blocker Keywords: API, API_REVIEW_FAST
Priority: P3    
Version: 6.x   
Hardware: All   
OS: All   
Issue Type: TASK Exception Reporter:
Bug Depends on:    
Bug Blocks: 114416    
Attachments: Diff for change, including test files
Updated patch file based on feedback
New revision based on latest input from Andrei

Description David Vancouvering 2007-09-08 00:10:03 UTC
Both visualweb and the DB Explorer generate SQL.  We need an API to take care of quoting identifiers (or not, as needed).
Comment 1 David Vancouvering 2007-09-08 00:19:26 UTC
Currently, we quote *every* identifier, which is a usability issue, as SQL text is very ugly and confusing when all the
identifiers are quoted.  With this API in place, we can quote only those identifiers that are necessary.

Proposed stability: Friend

Impact: No impact, as this is new.  This API will be used by visualweb sql generation code, the sql editor and the
visual query editor to control quoting of identifiers.

=== Specification ====

/**
 * This class provides some utilities for working with SQL identifiers
 */
public final class SQLIdentifier {
    /**
     * Construct an instance of SQLIdentifier.  
     * 
     * @param dbmd The DatabaseMetaData to use when working with identifiers.
     *   The metadata object is used to determine when an identifier needs
     *   to be quoted and what the quote string should be.
     */
    public SQLIdentifier(DatabaseMetaData dbmd)
    
    /**
     * Quote an identifier to be used in a SQL command, if needed.
     * This is determined based on whether the characters in the
     * identifier string are within the set of characters that do
     * not need to be quoted.  Anyone generating SQL that will be
     * visible and/or editable by the user should use this method.
     * This helps to avoid unecessary quoting, which affects the
     * readability and clarity of the resulting SQL
     * 
     * @param identifier  The identifier to be quoted, if needed
     * 
     * @return The identifier, quoted if needed
     */
    public String quoteIfNeeded(String identifier)
}
Comment 2 Andrei Badea 2007-09-10 13:52:15 UTC
AB01 I need to see the whole diff of the affected module.

AB02 Not sure which module this will go into. It can't be a friend API if it goes to the db module, since that already
exports a public API.

AB03 I don't see the reason for this class to be instantiable. Perhaps it would be visible from the diff (e.g., you need
to cache stuff from DMD, etc.). Better to expose a factory method instead of a public constructor. Best to expose no
constructor or factory method and have just a static quoteIfNeeded().

AB04 (nitpick) "@return The identifier" should be "@return the identifier".
Comment 3 David Vancouvering 2007-09-10 18:44:45 UTC
> AB01 I need to see the whole diff of the affected module.

OK, I'll attach the diff.  The instructions on the NetBeans site said make the Javadoc available, not the whole diff.

> AB02 Not sure which module this will go into. It can't be a friend API if it goes to the db module, since that already
> exports a public API.

It seems to me it belongs in the db module.  It doesn't make sense to me to *not* put it there just because it's a new
method and a new class.  Your advice on how to best use the NB API process to do this would be most appreciated.

> AB03 I don't see the reason for this class to be instantiable. Perhaps it would be visible from the diff (e.g., you 
> need to cache stuff from DMD, etc.). Better to expose a factory method instead of a public constructor. 
> Best to expose no constructor or factory method and have just a static quoteIfNeeded().

I tried it as just a static method, but as you mentioned, I need to cache DMD information, otherwise I'll be
unnecessarily hitting the database every time quoteIfNeeded() is called, which can happen a lot.  I'll change it to a
factory method prior to posting the diff.

> AB04 (nitpick) "@return The identifier" should be "@return the identifier".

OK
Comment 4 David Vancouvering 2007-09-10 20:07:27 UTC
Created attachment 48518 [details]
Diff for change, including test files
Comment 5 David Vancouvering 2007-09-10 20:09:16 UTC
Forgot to fix AB04 in the attached diff, but I have changed it in my sandbox...
Comment 6 Jaroslav Tulach 2007-09-11 10:21:32 UTC
Y01 Test file is missing license header. Btw. I am glad the test is there.

Y02 From the name SQLIdentifier I would expect the class to represent an instance of some SQL identifier. This is not 
true - it contains an utility method to convert string to quoted string. There are two ways to fix this:
a) move the quiteIfNeeded method directly to DatabaseMetaData
b) rename the class. For example to DatabaseUtilities, etc.

Y03 Discoverability: There is no link from DatabaseMetaData to the utilities class, afaik. As such it is going to be 
pretty hard for people holding instance of DatabaseMetaData to find out that there is such utility class. If Y02b is 
implemented and not Y02a, would not it be better to have DatabaseMetaData.getDatabaseUtilities() method?
Comment 7 _ wadechandler 2007-09-11 12:54:49 UTC
> Y02: ... SQLIdentifier (little confusing)...a) move the quiteIfNeeded method directly to DatabaseMetaData

My thoughts exactly. I mean, you already require DatabaseMetaData, and it is a method to work against the meta data, so
it logically should go there. I don't see why you need to have a factory method for this type method. It makes much more
sense to use the data you already have as it is logically connected. It is a simple action against the concepts and data
already in the class, and it is not a new concept you are adding to the API, so to me it doesn't warrant a new class.
Comment 8 Andrei Badea 2007-09-11 15:39:00 UTC
AB05 Re Y02: DatabaseMetaData is a JDK class, so we can't modify it. But I don't like the SQLIdentifier name either. In
the future I would like to add a method like isSQL99Keyword() method to it (returning true if a given identifier is a
keyword according to the SQL-99 standard). But this method does not need a DMD instance to compute its result, so it can
be static. But then you have both static and instance methods in the class, which is ugly. I also don't like
DatabaseUtilities much, it is too generic. So I suggest

class SQLIdentifiers { // note the plural

    static Quoter createQuoter(DMD dmd);

    static boolean isSQL99Keyword(String identifier); // in a later API version

    static class Quoter {

        String quoteIfNeeded(String identifier);
    }
}

AB06 The method seems to quote an already quoted identifier (e.g., when passed "foo" it will return ""foo""). So it
seems to expect unquoted identifiers (not surrounded by quotes, to be precise). This needs to be mentioned in the Javadoc.

AB07 the current SQLIdentifier class is not thread-safe. This needs to be mentioned in the Javadoc. Or probably better
to make it thread-safe.

AB08 Please make the dbmd field final, regardless of AB07.

AB09 Perhaps should log warnings about failed DMD method calls with a WARNING level rather than INFO.

AB10 I would discourage allowing null as a valid parameter to quoteIfNeeded(). I would rather throw a
NullPointerException in that case. You can use org.openide.util.Parameters.notNull() for that. You should also make this
check as the very first thing in the method body (before computing nonQuoteChars).

AB11 The comments in the containsLowerCase() and containsUpperCase() methods do not make sense.

AB12 Method containsLowerCase() can be simplified to test if (ch >= 'a' || ch <= 'z') and you get can rid of the
UC_CHARS and LC_CHARS constants. Or you perhaps can refactor both methods to a single containsRange(char start, char
end, String str). 

AB13 The code does not seem to deal with getIdentifierQuoteString() returning a space, which its Javadoc says it can.

AB14 The value of the NONQUOTE_CHARS field is missing "7".

AB15 Is it really necessary to have all those escaped characters in testNonAscii()? A single character should suffice
for the test.
Comment 9 Andrei Badea 2007-09-11 21:46:57 UTC
Whoops, the test in AB12 should use &&, not ||.

AB16 The note about not quoting new identifiers in the Javadoc of quoteIfNeeded() is irrelevant for this method.
Comment 10 David Vancouvering 2007-09-11 21:58:44 UTC
Andrei, excellent, excellent review, as always.   You are making me a better Java programmer, I really appreciate it.

AB05 Re Y02: DatabaseMetaData is a JDK class, so we can't modify it. But I don't like the SQLIdentifier name either. In
the future I would like to add a method like isSQL99Keyword() method to it (returning true if a given identifier is a
keyword according to the SQL-99 standard). But this method does not need a DMD instance to compute its result, so it can
be static. But then you have both static and instance methods in the class, which is ugly. I also don't like
DatabaseUtilities much, it is too generic. So I suggest

class SQLIdentifiers { // note the plural

    static Quoter createQuoter(DMD dmd);

    static boolean isSQL99Keyword(String identifier); // in a later API version

    static class Quoter {

        String quoteIfNeeded(String identifier);
    }
}

David: I like this, and I made these changes (except I didn't add isSQL99Keyword).

AB06 The method seems to quote an already quoted identifier (e.g., when passed "foo" it will return ""foo""). So it
seems to expect unquoted identifiers (not surrounded by quotes, to be precise). This needs to be mentioned in the Javadoc.

David: I fixed it to ignore an identifier that is already quoted.

AB07 the current SQLIdentifier class is not thread-safe. This needs to be mentioned in the Javadoc. Or probably better
to make it thread-safe.

David: I fixed this by making it immutable (all fields final and initialized in the constructor)

AB08 Please make the dbmd field final, regardless of AB07.

David: Done

AB09 Perhaps should log warnings about failed DMD method calls with a WARNING level rather than INFO.

David: Done

AB10 I would discourage allowing null as a valid parameter to quoteIfNeeded(). I would rather throw a
NullPointerException in that case. You can use org.openide.util.Parameters.notNull() for that. You should also make this
check as the very first thing in the method body (before computing nonQuoteChars.

David: Done

AB11 The comments in the containsLowerCase() and containsUpperCase() methods do not make sense.

David: Sorry, cut-and-paste error

AB12 Method containsLowerCase() can be simplified to test if (ch >= 'a' || ch <= 'z') and you get can rid of the
UC_CHARS and LC_CHARS constants. Or you perhaps can refactor both methods to a single containsRange(char start, char
end, String str). 

David: Doh!  I *knew* there had to be a better way.  I wouldn't have passed a Google interview :).  I don't like
containsRange, it's basically as much writing as the other way, and actually obfuscates things a bit.  BTW, I think you
mean't '&&' and not '||'.

AB13 The code does not seem to deal with getIdentifierQuoteString() returning a space, which its Javadoc says it can.

David: Hm, I didn't see this.  At any rate, with the changes I made, I definitely don't see this.  Sorry if I'm missing
it, can you point this out?

Here's what I have now.  quoteString is initialized in the constructor now...: 

        public final String quoteIfNeeded(String identifier) {
            Parameters.notNull("identifier", identifier);
            
            if ( needToQuote(identifier) ) {
                return quoteString + identifier + quoteString;
            }

            return identifier;
        }


AB14 The value of the NONQUOTE_CHARS field is missing "7".

David: not relevant any more, as I have removed that field

AB15 Is it really necessary to have all those escaped characters in testNonAscii()? A single character should suffice
for the test.

David: Yes, I know.  This was just for fun, as it's the first message in the Japanese version of the Derby message file.
 If you don't mind I'd like to keep it - no real harm...
Comment 11 David Vancouvering 2007-09-11 22:13:32 UTC
Re AB16: Removed this comment, no biggie
Comment 12 David Vancouvering 2007-09-11 22:14:30 UTC
Created attachment 48607 [details]
Updated patch file based on feedback
Comment 13 David Vancouvering 2007-09-11 22:19:36 UTC
One more comment: we haven't resolved where to put this API.  Andrei, are you OK with it going under the db module?  If
not, what do you suggest I do?

Thanks.
Comment 14 Andrei Badea 2007-09-12 11:11:12 UTC
Yes, I would put this in the db module. Not sure about the package though. Initially I thought
o.n.api.db.explorer.support. But perhaps a package like o.n.api.db.sql.support would be better, since this class has
nothing do to with the DB Explorer.

Re AB13: disregard that, I missed the trim() call on the string returned by getExtraNameCharacters().

Thank you for addressing my comments. I have several more on the new code, again, in no particular order:

AB17 You need to update the class name in apichanges.xml and arch.xml.

AB18 Please add a private constructor to SQLIdentifiers to ensure that it can't be instantiated.

AB19 SQLIdentifiers.LOGGER looks unused.

AB20 I would use an assertion in needToQuote() instead of an exception. I think public API methods should not use
assertions to ensure valid parameters values, but they should throw exceptions. This ensures parameter values are
enforced even when assertions are turned off. OTOH, I think assertions serve private methods better. They are simpler to
write and they just need to assert that the validity of the parameter has been checked somewhere else, usually in the
public entry point. That is, turning them off should cause no harm. But this is just a personal opinion, feel free to
leave the code as it is if you don't agree.

AB21 I just realized some databases might not accept unquoted identifiers starting with a number, such as 1FOO. Derby
doesn't, but it accepts such identifiers if they are quoted, e.g., "1FOO" is fine. Perhaps we should be conservative and
quote such identifiers.

AB22 (nitpick) The private methods taking DMD as a parameter (getExtraNameChars(), etc.) can be made static too.

AB23 You can simplify testNullIdentifier():

try {
    quoter.quoteIfNeeded(null);
    fail();
} catch (NPE npe) {
    // expected
}
Comment 15 David Vancouvering 2007-09-12 17:47:44 UTC
Hi, Andrei.  I'm working on your most recent set of comments, but one question I have is, how to work with the
architecture and API changes files when I change the package to o.n.a.db.sql.support.  When I generate the Javadoc it
doens't include this package, and the Javadoc/Arch Description is entitled "Database Explorer".

How do I go about fixing this?  I'm a little lost here.

Thanks,

David
Comment 16 David Vancouvering 2007-09-12 18:05:41 UTC
Andrei: never mind, I think I figured it out :)
Comment 17 David Vancouvering 2007-09-12 18:31:58 UTC
Andrei: Yes, I would put this in the db module. Not sure about the package though. Initially I thought
o.n.api.db.explorer.support. But perhaps a package like o.n.api.db.sql.support would be better, since this class has
nothing do to with the DB Explorer.

David: yes, that makes sense to me, done.

AB17 You need to update the class name in apichanges.xml and arch.xml.

David: done

AB18 Please add a private constructor to SQLIdentifiers to ensure that it can't be instantiated.

David: done

AB19 SQLIdentifiers.LOGGER looks unused.

David: removed

AB20 I would use an assertion in needToQuote() instead of an exception. I think public API methods should not use
assertions to ensure valid parameters values, but they should throw exceptions. This ensures parameter values are
enforced even when assertions are turned off. OTOH, I think assertions serve private methods better. They are simpler to
write and they just need to assert that the validity of the parameter has been checked somewhere else, usually in the
public entry point. That is, turning them off should cause no harm. But this is just a personal opinion, feel free to
leave the code as it is if you don't agree.

David: yes, this makes sense, and is what I normally do.  I just missed fixing this when I moved the NPE up to the
public API.  Changed.

AB21 I just realized some databases might not accept unquoted identifiers starting with a number, such as 1FOO. Derby
doesn't, but it accepts such identifiers if they are quoted, e.g., "1FOO" is fine. Perhaps we should be conservative and
quote such identifiers.

David: Man, you *are* good!  I wish I could think through all corner cases like that, you truly inspire me (seriously)!
 I think underbar ('_') is also iffy as a first char.  I am quoting any identifiers that start with a number or underbar
now.

AB22 (nitpick) The private methods taking DMD as a parameter (getExtraNameChars(), etc.) can be made static too.

David: yes, you're right, cool. Done.

AB23 You can simplify testNullIdentifier():

try {
    quoter.quoteIfNeeded(null);
    fail();
} catch (NPE npe) {
    // expected
}

David: I should listen to that uneasy feeling that there's a better way and try to find it.  Thanks.
Comment 18 David Vancouvering 2007-09-12 18:35:58 UTC
Created attachment 48678 [details]
New revision based on latest input from Andrei
Comment 19 Andrei Badea 2007-09-13 10:17:17 UTC
Thanks. Looks good now, just a final nitpick:

AB24: please keep the packages sorted in the public-packages element in project.xml. The api.db.sql.support package
should come before the spi.db.explorer package.
Comment 20 David Vancouvering 2007-09-15 01:07:43 UTC
Fixed that, Andrei, but not bothering with a new patch.

Unless I hear any objections, I'll assume this API review is closed and will assign it to myself on Monday and check in
the changes.
Comment 21 David Vancouvering 2007-09-17 19:52:06 UTC
Reassigning to myself, API review passes
Comment 22 David Vancouvering 2007-09-17 20:17:19 UTC
cvs server: scheduling file `catalog.xml' for addition
cvs server: use 'cvs commit' to add this file permanently
Checking in test/unit/src/org/netbeans/modules/db/util/DBTestBase.java;
/cvs/db/test/unit/src/org/netbeans/modules/db/util/DBTestBase.java,v  <--  DBTestBase.java
new revision: 1.3; previous revision: 1.2
done
Checking in nbproject/project.xml;
/cvs/db/nbproject/project.xml,v  <--  project.xml
new revision: 1.23; previous revision: 1.22
done
RCS file: /cvs/db/catalog.xml,v
done
Checking in catalog.xml;
/cvs/db/catalog.xml,v  <--  catalog.xml
initial revision: 1.1
done
Checking in apichanges.xml;
/cvs/db/apichanges.xml,v  <--  apichanges.xml
new revision: 1.7; previous revision: 1.6
done
Checking in arch.xml;
/cvs/db/arch.xml,v  <--  arch.xml
new revision: 1.16; previous revision: 1.15
done
RCS file: /cvs/db/test/unit/src/org/netbeans/api/db/sql/support/QuoterTest.java,v
done
Checking in test/unit/src/org/netbeans/api/db/sql/support/QuoterTest.java;
/cvs/db/test/unit/src/org/netbeans/api/db/sql/support/QuoterTest.java,v  <--  QuoterTest.java
initial revision: 1.1
done
RCS file: /cvs/db/src/org/netbeans/api/db/sql/support/SQLIdentifiers.java,v
done
Checking in src/org/netbeans/api/db/sql/support/SQLIdentifiers.java;
/cvs/db/src/org/netbeans/api/db/sql/support/SQLIdentifiers.java,v  <--  SQLIdentifiers.java
initial revision: 1.1
done
Comment 23 Andrei Badea 2007-09-17 21:09:58 UTC
Watch out, the CVS support was too smart and it commited the catalog.xml file, which I guess you didn't intend to commit.