Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

JOIN queries return SQL error code -104 [DNET432] #429

Closed
firebird-automations opened this issue May 16, 2012 · 9 comments
Closed

JOIN queries return SQL error code -104 [DNET432] #429

firebird-automations opened this issue May 16, 2012 · 9 comments

Comments

@firebird-automations
Copy link

Submitted by: José María Sánchez (jmsanchez)

Relate to DNET208
Is duplicated by DNET404

Hi all,
I am working on a project to create a front-end application for a Firebird database with Microsoft Lightswitch. I am using Firebird Server 2.5, Firebird NETProvider 2.7.5 and Firebird DDEX Provider 2.0.5. Some days ago I found a problem that has puzzled me for several days. I think that the issue is due to a problem in the NETProvider, but it also could be related to the information that the DDEX provider sends to NETProvider for generating the SQL queries.
I report it because I think it may be a bug and maybe could help others to solve a similar problem.

Simple SQL queries for screens displaying data from a single table seem to generate correctly, but I found a problem in complex JOIN queries for screens that display information from different related tables using Foreign Keys.

In my Firebird database I have several related tables. I include some of them as an example.

/* Original table name is "memos" */
CREATE TABLE "memos" (
"id_memo" INTEGER CONSTRAINT "IDPK_id_memo" NOT NULL PRIMARY KEY /* "id_memo" */ ,
"id_text_grup" INTEGER NOT NULL /* "id_text_grup" */
);

/* Original table name is "memos_data" */
CREATE TABLE "memos_data" (
"id_memo_data" INTEGER CONSTRAINT "IDPK_id_memo_data" NOT NULL PRIMARY KEY /* "id_memo_data" */ ,
"id_memo" INTEGER NOT NULL /* "id_memo" */ ,
"id_idioma" INTEGER NOT NULL /* "id_idioma" */ ,
"text" BLOB SUB_TYPE 1 /* "text" */
);

/* Original table name is "texts_grups" */
CREATE TABLE "texts_grups" (
"id_text_grup" INTEGER CONSTRAINT "IDPK_id_text_grup" NOT NULL PRIMARY KEY /* "id_text_grup" */ ,
"nom_grup" VARCHAR(255) CHARACTER SET UTF8 COLLATE UTF8 /* "nom_grup" */
);

/* Original table name is "idiomes" */
CREATE TABLE "idiomes" (
"id_idioma" INTEGER CONSTRAINT "IDPK_id_idioma" NOT NULL PRIMARY KEY /* "id_idioma" */ ,
"tag" CHAR(3) CHARACTER SET UTF8 COLLATE UTF8 /* "tag" */ ,
"descrip" VARCHAR(255) CHARACTER SET UTF8 COLLATE UTF8 /* "descrip" */
);

ALTER TABLE "memos" ADD CONSTRAINT "FK_memos_id_text_grup" FOREIGN KEY ("id_text_grup") REFERENCES "texts_grups"("id_text_grup") ON UPDATE CASCADE;
ALTER TABLE "memos_data" ADD CONSTRAINT "FK_memos_data_id_idioma" FOREIGN KEY ("id_idioma") REFERENCES "idiomes"("id_idioma") ON UPDATE CASCADE;
ALTER TABLE "memos_data" ADD CONSTRAINT "FK_memos_data_id_memo" FOREIGN KEY ("id_memo") REFERENCES "memos"("id_memo") ON UPDATE CASCADE;

The NETProvider returns the following SQL error code to my Lightswitch application.

FirebirdSql.Data.FirebirdClient Information: 0 : Command:
SELECT
"A"."Extent1"."id_memo" AS "id_memo",
"A"."Extent1"."id_text_grup" AS "id_text_grup",
"A"."Extent2"."id_text_grup" AS "id_text_grup1",
"A"."Extent2"."nom_grup" AS "nom_grup"
FROM ( SELECT FIRST (45) SKIP (0) "C"."id_memo" AS "id_memo", "C"."id_text_grup" AS "id_text_grup1", "D"."id_text_grup" AS "id_text_grup2", "D"."nom_grup" AS "nom_grup"
FROM "memos" AS "C"
INNER JOIN "texts_grups" AS "D" ON "C"."id_text_grup" = "D"."id_text_grup"
ORDER BY "C"."id_memo" ASC
) AS "A"
FirebirdSql.Data.FirebirdClient Information: 0 : Parameters:

[Microsoft.LightSwitch.DataService][Application:Error][LightSwitchApplication.SodecaCatalogDatasource:memos_All] An exception has occurred: Microsoft.LightSwitch.DataServiceOperationException: Se produjo un error al ejecutar la definición del comando. Vea la excepción interna para obtener detalles. ---> System.Data.EntityCommandExecutionException: Se produjo un error al ejecutar la definición del comando. Vea la excepción interna para obtener detalles. ---> FirebirdSql.Data.FirebirdClient.FbException: Dynamic SQL Error
SQL error code = -104
Token unknown - line 2, column 14
. ---> FirebirdSql.Data.Common.IscException: Dynamic SQL Error
SQL error code = -104
Token unknown - line 2, column 14

That query is not correct. It should be something like:
SELECT
"A"."id_memo" AS "id_memo",
"A"."id_text_grup1" AS "id_text_grup",
"A"."id_text_grup2" AS "id_text_grup1",
"A"."nom_grup" AS "nom_grup"
FROM ( SELECT FIRST (45) SKIP (0) "C"."id_memo" AS "id_memo", "C"."id_text_grup" AS "id_text_grup1", "D"."id_text_grup" AS "id_text_grup2", "D"."nom_grup" AS "nom_grup"
FROM "memos" AS "C"
INNER JOIN "texts_grups" AS "D" ON "C"."id_text_grup" = "D"."id_text_grup"
ORDER BY "C"."id_memo" ASC
) AS "A"

So to get a better idea of what was going on, I downloaded the trunk source code, studied it for some days and found a possible solution to this problem.
I am not really sure if this is the best way to solve the issue but now all is working as expected.

The generation process for this query is something like:
1) the inner query is generated JOINING tables "C" and "D"
2) next the provider generates an intermediate query for the result of the previous JOIN as "B"
3) finally the global query is generated. As it requires no more information and to simplify the final query, "B" is reused and renamed to "A"

Debugging the process I saw that the information for the JoinSymbol required to generate "A" was somehow lost in the process. That produces this issue because the provider does not have all the internal join information ("C" and "D") needed to create the external query for "A".

I changed the following and seem to have gotten it to work:

NETProvider\source\FirebirdSql\Data\Entity\SqlGenerator.cs

SqlSelectStatement CreateNewSelectStatement(SqlSelectStatement oldStatement,
string inputVarName, TypeUsage inputVarType, bool finalizeOldStatement, out Symbol fromSymbol)
{
fromSymbol = null;

// Finalize the old statement
if \(finalizeOldStatement && oldStatement\.Select\.IsEmpty\)
\{
	List<Symbol\> columns = AddDefaultColumns\(oldStatement\);

	// Thid could not have been called from a join node\.
	Debug\.Assert\(oldStatement\.FromExtents\.Count == 1\);

	// if the oldStatement has a join as its input, \.\.\.
	// clone the join symbol, so that we "reuse" the
	// join symbol\.  Normally, we create a new symbol \- see the next block
	// of code\.
	JoinSymbol oldJoinSymbol = oldStatement\.FromExtents\[0\] as JoinSymbol;
	if \(oldJoinSymbol \!= null\)
	\{
		// Note: oldStatement\.FromExtents will not do, since it might
		// just be an alias of joinSymbol, and we want an actual JoinSymbol\.
		JoinSymbol newJoinSymbol = new JoinSymbol\(inputVarName, inputVarType, oldJoinSymbol\.ExtentList\);
		// This indicates that the oldStatement is a blocking scope
		// i\.e\. it hides/renames extent columns
		newJoinSymbol\.IsNestedJoin = true;
		newJoinSymbol\.ColumnList = columns;
		newJoinSymbol\.FlattenedExtentList = oldJoinSymbol\.FlattenedExtentList;

		fromSymbol = newJoinSymbol;
	\}
\}

//JMS\-START: THESE ARE MY LINES
if \(fromSymbol == null\)
\{
    // if the oldStatement has a join as its input, \.\.\.
    // clone the join symbol, so that we "reuse" the
    // join symbol\.  Normally, we create a new symbol \- see the next block
    // of code\.
    JoinSymbol oldJoinSymbol = oldStatement\.FromExtents\[0\] as JoinSymbol;
    if \(oldJoinSymbol \!= null\)
    \{
        // Note: oldStatement\.FromExtents will not do, since it might
        // just be an alias of joinSymbol, and we want an actual JoinSymbol\.
        JoinSymbol newJoinSymbol = new JoinSymbol\(inputVarName, inputVarType, oldJoinSymbol\.ExtentList\);
        // This indicates that the oldStatement is a blocking scope
        // i\.e\. it hides/renames extent columns
        newJoinSymbol\.IsNestedJoin = true;
        newJoinSymbol\.ColumnList = null;
        newJoinSymbol\.FlattenedExtentList = oldJoinSymbol\.FlattenedExtentList;

        fromSymbol = newJoinSymbol;
    \}
\}
//JMS\-END: THESE ARE MY LINES

if \(fromSymbol == null\)
\{
    // This is just a simple extent/SqlSelectStatement,
    // and we can get the column list from the type\.
    fromSymbol = new Symbol\(inputVarName, inputVarType\);
\}

// Observe that the following looks like the body of Visit\(ExtentExpression\)\.
SqlSelectStatement selectStatement = new SqlSelectStatement\(\);
selectStatement\.From\.Append\("\( "\);
selectStatement\.From\.Append\(oldStatement\);
selectStatement\.From\.AppendLine\(\);
selectStatement\.From\.Append\("\) "\);


return selectStatement;

}

Maybe this is not the best solution to solve the problem, but I include it as a possible solution.

Thank you very much for this wonderful project Jiri.

@firebird-automations
Copy link
Author

Modified by: @cincuranet

status: Open [ 1 ] => In Progress [ 3 ]

@firebird-automations
Copy link
Author

Modified by: @cincuranet

priority: Critical [ 2 ] => Major [ 3 ]

@firebird-automations
Copy link
Author

Modified by: @cincuranet

Component: Entity Framework support [ 10110 ]

Component: http://ADO.NET Provider [ 10041 ] =>

@firebird-automations
Copy link
Author

Commented by: @cincuranet

How does the LINQ query looks like?

@firebird-automations
Copy link
Author

Commented by: José María Sánchez (jmsanchez)

Hi Jiri,
thank you very much for your quick response.

There's no LINQ query.
I created a new Lightswitch project and used Entity Framework support to automatically generate a DataSource linked to a firebird database. My project Datasource have many different tables that have relations between them. I included the SQL commands to recreate some of them in the issue text.
So with these entities in the DataSource: idiomes (languages), texts_grups (text groups), memos and memos_data, I then create a screen for the memos entity and add memos_data related information to it. Lightswitch creates a Master-Detail screen with a list of memos at the top and a list of related memos_data at the bottom. So as you see, I have no code at all, Visual Studio Lightswitch or the Entity Framework auto-generated it transparently.
When I debug my application and open that screen, Lightswitch gives error code -104 as a result.

I am not sure if the problem is in the information or structures that the Entity Framework is sending to the ADO NET Provider in order to create that query or in the ADO NET Provider itself.
In my case, Entity Framework requests for the JOIN of "memos" and "texts_grups". The SqlGenerator class inside the ADO NET Provider crawls that structure and generates some Commands to query the database.
Before getting into the error part, some Select commands are executed without a problem. But then the SqlGenerator gets to the Extent part (JOIN information) and generates a wrong query as a result.

Wrong Query that returns sql error code -104
SELECT
"A"."Extent1"."id_memo" AS "id_memo",
"A"."Extent1"."id_text_grup" AS "id_text_grup",
"A"."Extent2"."id_text_grup" AS "id_text_grup1",
"A"."Extent2"."nom_grup" AS "nom_grup"
FROM ( SELECT FIRST (45) SKIP (0) "C"."id_memo" AS "id_memo", "C"."id_text_grup" AS "id_text_grup1", "D"."id_text_grup" AS "id_text_grup2", "D"."nom_grup" AS "nom_grup"
FROM "memos" AS "C"
INNER JOIN "texts_grups" AS "D" ON "C"."id_text_grup" = "D"."id_text_grup"
ORDER BY "C"."id_memo" ASC
) AS "A"

That query should be
SELECT
"A"."id_memo" AS "id_memo",
"A"."id_text_grup1" AS "id_text_grup",
"A"."id_text_grup2" AS "id_text_grup1",
"A"."nom_grup" AS "nom_grup"
FROM ( SELECT FIRST (45) SKIP (0) "C"."id_memo" AS "id_memo", "C"."id_text_grup" AS "id_text_grup1", "D"."id_text_grup" AS "id_text_grup2", "D"."nom_grup" AS "nom_grup"
FROM "memos" AS "C"
INNER JOIN "texts_grups" AS "D" ON "C"."id_text_grup" = "D"."id_text_grup"
ORDER BY "C"."id_memo" ASC
) AS "A"

Possible explanation.
The SqlGenerator starts to analyze the structure to create that query:
1) First it creates the most inner query, so the JOIN query from "memos as C" and "texts_grups as D" is generated.
2) Next it creates a more general select query to encapsulate the previous JOIN query ("as B").
3) Finally it creates an external query ("as A"), but for the sake of simplicity it does not create a new query surrounding "B", it renames "B" to "A". At this point, the function CreateNewSelectStatement is executed to retrieve the symbol information for "A". As the previous statement has been already closed the code inside this if statement
// Finalize the old statement
if (finalizeOldStatement && oldStatement.Select.IsEmpty)
is not executed so the "JoinSymbol" which have the needed information for "A" with the JOIN between C and D is not retrieved at this point and a new normal Symbol is created for "A" instead.

I think that the symbol retrieved for "A" should be a "JoinSymbol" and not a normal "Symbol" in order for the rest of the process to have all the necessary information and create the query correctly.

I beg your apologies. I've been struggling with this issue for several days and I have dived a bit into the ADO NET Provider code before contacting you.
Perhaps I made myself with some ideas that are not quite correct and that may make my explanation not clear enough or confusing to you.
If you need some more information or need some additional debug information, please do not hesitate to ask for it.

Best regards.
José María

@firebird-automations
Copy link
Author

Commented by: @cincuranet

The root cause is how the SKIP clause is handled. I've fixed it. It's now even a simpler query. Could you send me an email (jiri (foobar) http://cincura.net) I would send you the fixed build to try it? We could see whether the fix is OK, as I was guessing the original LINQ query.

@firebird-automations
Copy link
Author

Modified by: @cincuranet

Link: This issue relate to DNET208 [ DNET208 ]

@firebird-automations
Copy link
Author

Modified by: @cincuranet

status: In Progress [ 3 ] => Resolved [ 5 ]

resolution: Fixed [ 1 ]

Fix Version: 2.7.7 [ 10466 ]

@firebird-automations
Copy link
Author

Modified by: @cincuranet

Link: This issue is duplicated by DNET404 [ DNET404 ]

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants