Project

General

Profile

Bug #10171

Word Index/CONTAINS support for MS SQL Server.

Added by Stefanel Pezamosca about 1 year ago. Updated about 2 months ago.

Status:
Test
Priority:
Normal
Target version:
-
Start date:
Due date:
% Done:

100%

billable:
No
vendor_id:
GCD
case_num:
version_reported:
version_resolved:
production:
No
env_name:
topics:
Word Indexes

schema_word_tables_fwd_sqlserver2012.sql (5.97 KB) Stefanel Pezamosca, 07/02/2025 10:44 AM


Related issues

Related to Database - Feature #9456: implement SQL Server dialect and helpers WIP

History

#2 Updated by Stefanel Pezamosca about 1 year ago

  • Related to Feature #9456: implement SQL Server dialect and helpers added

#3 Updated by Stefanel Pezamosca about 1 year ago

This task is for implementing support for word indexing and CONTAINS statements in Microsoft SQL Server.
As part of the solution, I created a words user-defined function (UDF) that splits input text into individual words. These words will be stored in the word index table.

I thought of creating a separate table for storing the word delimiters:

Then, this table will be used to fast check if a character is a delimiter in this dbo.words udf function:

Above SQL statements can be ran at UDF setup step. Initial tests shows that this works pretty good and fast.
I will run some tests on a larger table and also experiment with the insert trigger to automatically update the word index table.

I'd like some feedback on the idea described above.

#4 Updated by Stefanel Pezamosca about 1 year ago

Another version without using the dbo.Delimiters table:

#5 Updated by Stefanel Pezamosca about 1 year ago

  • Status changed from New to WIP
  • % Done changed from 0 to 40

What I have so far.

  • An improved dbo.words function:
    IF OBJECT_ID('dbo.words', 'IF') IS NOT NULL
        DROP FUNCTION dbo.words;
    GO
    
    CREATE FUNCTION dbo.words(@input NVARCHAR(MAX), @caseSensitive BIT = 0)
    RETURNS TABLE
    AS
    RETURN
    (
        SELECT DISTINCT
            CASE
                WHEN @caseSensitive = 0 THEN UPPER(s.value)
                ELSE s.value
            END AS word
        FROM
            -- Replace all delimiters with a space character.
            (VALUES (
                TRANSLATE(
                    @input,
                    N' !"&()*+,-./:;<=>?[\]^`{|}~'
                    + NCHAR(1) + NCHAR(2) + NCHAR(3) + NCHAR(4) + NCHAR(5) + NCHAR(6) + NCHAR(7) + NCHAR(8)
                    + NCHAR(9) + NCHAR(10) + NCHAR(11) + NCHAR(12) + NCHAR(13) + NCHAR(14) + NCHAR(15) + NCHAR(16)
                    + NCHAR(17) + NCHAR(18) + NCHAR(19) + NCHAR(20) + NCHAR(21) + NCHAR(22) + NCHAR(23) + NCHAR(24)
                    + NCHAR(25) + NCHAR(26) + NCHAR(27) + NCHAR(28) + NCHAR(29) + NCHAR(30) + NCHAR(31)
                    + NCHAR(127) + NCHAR(128) + NCHAR(129) + NCHAR(130) + NCHAR(131) + NCHAR(132) + NCHAR(133) + NCHAR(134) + NCHAR(135) + NCHAR(136) + NCHAR(137)
                    + NCHAR(159) + NCHAR(161) + NCHAR(162) + NCHAR(163) + NCHAR(165) + NCHAR(169) + NCHAR(171) + NCHAR(184) + NCHAR(187)
                    + NCHAR(188) + NCHAR(189) + NCHAR(191) + NCHAR(197) + NCHAR(198) + NCHAR(215) + NCHAR(216) + NCHAR(229) + NCHAR(230) + NCHAR(248),
                    REPLICATE(' ', 88)
                )
            )) AS T(TranslatedString)
        CROSS APPLY STRING_SPLIT(T.TranslatedString, ' ') AS s
        -- Filter out empty results
        WHERE
            s.value <> ''
    );
    GO
    
  • DDLs for creating the primary key and other indexes:
  • DDLs for creating the necessary triggers:

Next: Review, test and work on the java code to generate all of these. (P2JSQLServer2012Dialect ?)

#6 Updated by Ovidiu Maxiniuc about 1 year ago

Yes, there are a few changes in the UDFs for word tables. To check their existence the right type is IF not FN and the you need to GRANT SELECT, not EXEC since they are viewed more like tables, not scalar functions.

Please see update the specific methods from P2JSQLServer2012Dialect. You might also need to touch some methods in FqlToSqlConverter, as well.

#7 Updated by Stefanel Pezamosca about 1 year ago

  • % Done changed from 40 to 50
  • Assignee set to Stefanel Pezamosca

I have an untested prototype for P2JSQLServer2012Dialect.generateWordTablesDDLImpl method. I guess I should work over 9456a changes.

#8 Updated by Ovidiu Maxiniuc about 1 year ago

Yes, please. Use 9456a.

#9 Updated by Stefanel Pezamosca about 1 year ago

I have the function to generate the necessary triggers for word index table updates. I attached a generated DDL to see how it looks at the moment.
So, I have dbo.words_sb UDF + the necessary triggers. The SQL generated at the moment by FqlToSqlConverter for CONTAINS statement is already compatible with SQL Server so we are good here.

There is still the part for creating/dropping indexes that needs to be fixed now.

#10 Updated by Stefanel Pezamosca about 1 year ago

  • % Done changed from 60 to 80

Word tables should theoretically work now. All I need to do is to test them on a real testcase. Before that, I want a review on the current changes.

Ovidiu/Eduard, Do you have any pending changes to 9456a or can I commit my changes to 9456a?

#11 Updated by Eduard Soltan about 1 year ago

Stefanel Pezamosca wrote:

Word tables should theoretically work now. All I need to do is to test them on a real testcase. Before that, I want a review on the current changes.

Ovidiu/Eduard, Do you have any pending changes to 9456a or can I commit my changes to 9456a?

I am planning to commit some changes by the EOD.

#12 Updated by Stefanel Pezamosca about 1 year ago

  • % Done changed from 80 to 90
  • Status changed from WIP to Review
  • reviewer Ovidiu Maxiniuc added

I have committed my changes for word tables support in MS SQL Server in 9456a revision 16041.
At the moment only the generated DDLs and the SQL generated by FqlToSqlConverter for CONTAINS statement were manually tested in SQL Server Management Studio.
I also added basic initial versions of udf/sqlserver/error_handler.sql, udf/sqlserver/getFWDVersion.sql, udf/sqlserver/guarded.sql to make ./gradlew all work.

Ovidiu, please review!

#13 Updated by Ovidiu Maxiniuc about 1 year ago

I did not do a thorough review, only a few observations:
  • please stick to JDK8 for the moment. List<E> of(E... elements) is JDK9+;
  • when a trigger is generated, the 2nd parameter of dbo.words_sb() is a bit, not a boolean. At this moment, the %6$b parameter will generate true, which is not be accepted by SQL Server. 1 / 0 should be generated instead;
  • when generating
    create table <table-name>__<word-column> (
       parent__id bigint not null,
       word nvarchar(max) not null
    );
    , the type of word column cannot be nvarchar(max) because it is not indexable. Please change it to nvarchar(4000) which is the maximum size for indexable character data. It should be enough since it will store a single word;
  • these seems to be a change in PSQL dialect as well. In trunk it was used:
    drop index if exists idx__<table-name>__<word-column>;
    But in 9456a I can see now:
    if exists (select * from sys.indexes where name = 'idx__<table-name>__<word-column>') drop index <table-name>__<word-column>.idx__<table-name>__<word-column>;
    From what I know that is not valid in PSQL, OTOH it looks identical to the SQL Server dialect, so I think that is a leak.

#14 Updated by Stefanel Pezamosca about 1 year ago

Ovidiu Maxiniuc wrote:

I did not do a thorough review, only a few observations:
  • please stick to JDK8 for the moment. List<E> of(E... elements) is JDK9+;

Hmm, another case of "IntelliJ knows better".

  • when a trigger is generated, the 2nd parameter of dbo.words_sb() is a bit, not a boolean. At this moment, the %6$b parameter will generate true, which is not be accepted by SQL Server. 1 / 0 should be generated instead;

Oh, I remember testing the generated sql manually in SQLServer and got an error but I forgot to fix it in code.

  • when generating [...], the type of word column cannot be nvarchar(max) because it is not indexable. Please change it to nvarchar(4000) which is the maximum size for indexable character data. It should be enough since it will store a single word;

Right, I didn't test create table statement, I had the table already created manually...
The problem is from this line in DDLGeneratorWorker: boolean isIndexed = isIndexComponent(field, table); where it does not know that word will have an index. Then it sends indexed=false to getSqlMappedType

  • these seems to be a change in PSQL dialect as well. In trunk it was used:[...]But in 9456a I can see now:
    [...]From what I know that is not valid in PSQL, OTOH it looks identical to the SQL Server dialect, so I think that is a leak.

That change should be active only for the Sql Server dialect. Indeed, there is an issue when creating a WordTable instance, as the generated DDLs are for the Sql Server dialect.
The problem lies in the wordTablesKeys map and possibly in the wordTables map, which currently use only dbname and wordTableName as keys. We should include the dialect as part of the key as well.
Working on it.

#15 Updated by Stefanel Pezamosca about 1 year ago

  • Status changed from Review to WIP
  • % Done changed from 90 to 80

#16 Updated by Stefanel Pezamosca about 1 year ago

  • % Done changed from 80 to 90
  • Status changed from WIP to Review

I fixed the above issues in revision 9456a revision 16053.

#17 Updated by Ovidiu Maxiniuc about 1 year ago

  • Status changed from Review to WIP

Stefanel Pezamosca wrote:

Hmm, another case of "IntelliJ knows better".

It tries to help based on your configuration. Press Ctrl+Alt+Shift+S to go to Project structure. In Project tab you will find the current SDK. It's OK to use JDK17 here, but in next box (Language level) select 8 - Lambdas, type annotations, etc.. This will cause the IDE to use Java17 for compile/run, but will limit it only to features compatible with Java8.

  • when generating [...], the type of word column cannot be nvarchar(max) because it is not indexable. Please change it to nvarchar(4000) which is the maximum size for indexable character data. It should be enough since it will store a single word;

The problem is from this line in DDLGeneratorWorker: boolean isIndexed = isIndexComponent(field, table); where it does not know that word will have an index. Then it sends indexed=false to getSqlMappedType

OK, I will look at this. It seems that the word indexes are not correctly processed... I'll be back with more info on this matter.

Review of 9456a, r16053
  • P2JSQLServer2012Dialect.java: lines 116, 138, 158: These lists do not require wrapping in a unmodifiableList() since they are private and processed locally not returned by any method. TBH, I really do not like the extra processing at runtime and I would prefer to have them processed at compile time, using + concatenation. Unfortunately, the joining element (eoln, which depend on the target OS) is not available at that time, so we are stuck here. OTOH, I am not sure it matters that much, but this is a discussion for another time.
  • DDLGeneratorWorker.java:
    • the addLastFieldImpl() method was added because the last column in a table (see the javadoc) and some things had to be processed differently. However, its semantic seems to have changed over time in that is is only called now for word tables. Therefore, a better name would be addWordIndexComponent()? I understand that this should also fix the above word column issue.

I am updating my test environment and continue to work/test with these changes. Meanwhile I am setting the status of this task to WIP for the remaining 10% of %Done.

#18 Updated by Ovidiu Maxiniuc about 1 year ago

I have just noticed a warning when the word indexes are created:

     [exec] Warning! The maximum key length for a clustered index is 900 bytes. The index 'pk__<table>__<field>' has maximum length of 8008 bytes. For some combination of large values, the insert/update operation will fail.
     [exec] Warning! The maximum key length for a nonclustered index is 1700 bytes. The index 'idx__<table>__<field>' has maximum length of 8000 bytes. For some combination of large values, the insert/update operation will fail.

Both of these indexes have as component the word field we talked about earlier. It contains a single word tokenized by the word splitter. I suggested to get the allocated size to 4K since the max caused the index creation to fail. The warnings are just informational, the chances we will encounter such words are near 0. Therefore, to get rid of the warnings, we need to scale down the size of the word column to 850 for full compatibility with nonclustered indexes and 446 to remove the warning in case of clustered index.

I do not know why these indexes have different clustering. I inspected the database schema and they are indeed - different, even if the DDL we generate does not specify any kind.

#19 Updated by Stefanel Pezamosca about 1 year ago

I fixed the above warnings by creating the word field in the generated wordTable with the parent field maxWidth.
I also got rid of Collections.unmodifiableList for word table support templates in Postgresql and SQLServer dialects.

Committed in 9456a revision 16054.

#20 Updated by Stefanel Pezamosca about 1 year ago

  • Status changed from WIP to Review
  • % Done changed from 90 to 100

Ovidiu,

There was another issue in the trigger function generation for word index tables over expanded extent fields, a dangling union all.

Additionally, I encountered a problem with SQL generation for contains:
  • I had to add missing overrides for isUdfContains and containsUdfName.
  • In convertBooleans, the logic was generating contains_ss(field, s) = 1. The =1 part caused issues because contains gets translated into a subselect. Therefore, we should skip the convertBooleans logic when dialect.isUdfContains is true and dialect.useUdf4Contains is false.

I fixed these errors in revision 9456a (r16061). I also set up a working environment to connect to SQL Server, create the database, and run an import. I tested a small testcase with contains and it works as expected.

Please review.

#21 Updated by Ovidiu Maxiniuc about 1 year ago

At a first quick look, the code seems OK. I will do a more thoroughly review soon.

#22 Updated by Stefanel Pezamosca 12 months ago

  • Status changed from Review to Internal Test

I tested 9456a with a few standalone testcases I made with CONTAINS and they were ok. 9456a was already merged to trunk, so I will switch to 9456b and search for more tests to run.

#23 Updated by Stefanel Pezamosca 10 months ago

  • Status changed from Internal Test to Test

#24 Updated by Greg Shah about 2 months ago

  • topics Word Indexes added

Also available in: Atom PDF