Repository navigation
GEP-26: continue the GINQ SQL/jOOQ reference implementation (from #2658) - #3155
daniellansun wants to merge 8 commits into
Conversation
dda4fb3 to
e0aebbc
Compare
There was a problem hiding this comment.
🟡 Changes recommended
Several moderate active findings remain in SQL rendering and provider selection, including unsafe table-name handling, incorrect NULL membership semantics, unsupported SQLite functions, and unreliable jOOQ auto-detection.
4 open issues
Review effort: Lite
| private Expression selectAll(String tableName) { | ||
| Expression dataSourceExpr = configurationExpressions.get(GinqGroovyMethods.CONF_DATA_SOURCE) | ||
| return callX(classX(SQL_GINQ_RUNTIME_TYPE), 'execute', | ||
| args(dataSourceExpr, constX('SELECT * FROM ' + tableName), new ListExpression())) |
| // `x in []` is false and `x !in []` is true, including when x is null | ||
| return new SqlBinary('=', new SqlLiteral(1), new SqlLiteral('!in' == op ? 1 : 0)) | ||
| } | ||
| List<SqlExpr> values = elements.collect { Expression e -> translateExpr(e) } |
| return "MIN"; | ||
| default: | ||
| return ansiName; |
|
|
||
| String astWalkerClassName | ||
| if (CONF_PROVIDER_AUTO_SQL == provider) { | ||
| astWalkerClassName = PREFERRED_SQL_PROVIDERS.findResult { providers.get(it) } |
e0aebbc to
eb7e380
Compare
eb7e380 to
3edd995
Compare
0d104b0 to
3edd995
Compare
|
I've been through this with 7.0.0-alpha-1 in mind and I think it can go in as is, as an incubating optional module, with the smaller items handled in later alphas. Checked: CI: the macOS red is the known 60-minute stall. Please rebase onto master before merging: the branch is ~40 commits behind and lacks 7155266 (the cache-scrub fix), which is exactly what failed on Windows for #3167 after a successful Gradle run. For later alphas (none blocking):
While comparing providers I also hit two pre-existing collection-provider bugs ( |
|
Thanks Paul — landed the later-alpha items in this PR and fixed the two collection-provider NPEs you hit while comparing providers:
Tip is e8244ce; CI green. Happy to rebase onto master (incl. the cache-scrub) whenever you’re ready to merge. |
e8244ce to
93f5030
Compare
|
(1) from e in 'employees'
join d in 'departments' on e.deptId == d.id
select e.name.reverse(), d.name.reverse() // double use of nameThis should force mixed mode. The source records are reconstructed independently, but their column labels originate from the SQL projection. If the database returns duplicate column labels within a source projection, some source information could be lost. The design assumes that JDBC metadata provides a stable representation of the original source schema. That assumption is not universally safe. JDBC drivers and database systems can differ in their treatment of labels, duplicate names, case folding and quoting. naming the columns could help, even though it requires remapping later. Also in that map __ginq_mixed_key could collide with a database column of the same name. There is also a lot about the mapping of the operations... I think a documented list of intentional differences would help to understand not only what is mapped, but also how semantics may differ (exact semantics depend on the db of course if mapped to a db operation. Maybe something like: (2) def wanted = null
GQL(provider: 'native-sql', dataSource: db) {
from e in 'employees'
where e.deptId == wanted
select e.name
}This will most likely result in I am not saying this must work according to Groovy semantics, but it needs documentation when it does and when not. (3) def suffix = '!'
from e in 'employees'
select e.name + suffixisStringy() does not recognize the captured string variable and silently generates arithmetic addition. The implementation depends a lot on recognizes cases like this, but cannot always do so. Maybe we should consider rejecting some constructions. Having clear limits is better than unpredictable behavior (4) While reasonable, if a translation succeeds in producing a SqlFunction, a particular dialect might still not support the corresponding function. FlOOR and CEILING have been fixed, but I think the mixed-mode planner needs general mechanism for considering dialect capabilities when determining which clauses can be pushed down. Or there must be a way for the dialect renderer to fall back to non-SQL implementation. |
93f5030 to
4eff449
Compare
|
Jochen, thanks for the review. I agree with the direction: a mapping that cannot be decided should be rejected or left in memory, not emitted as a guess. The four points are handled as follows. Mixed-mode rows. A pushed join is no longer reconstructed from the labels of Nulls.
Dialects. Whether a function can be rendered is one table, shared by the translator and both renderers. SQLite does not include |
This comment has been minimized.
This comment has been minimized.
(cherry picked from commit 2780be0)
…additional functions) (cherry picked from commit 8a0d584)
(cherry picked from commit 7a1c966)
Pin SQL provider dependency checksums and H2 2.5.252 for the ginq-sql tests. Add opt-in mixed-mode execution and Phase 5 function/dialect polish, and keep SQL translation faithful to Groovy results. Map coalesce, count(distinct), and Math max/min to SQL COALESCE, COUNT(DISTINCT), and GREATEST/LEAST (SQLite scalar MAX/MIN). cast(expr, 'TYPE') renders CAST(expr AS TYPE) for a whitelist of SQL types; select expr as alias stays a column alias. Scalar method mappings, including coalesce and Math.max/min, live in SqlFunctionRegistry; indexOf renders as a 0-based POSITION, LOCATE or INSTR depending on the dialect. Import types so the lint gate accepts simple names. Mixed-mode SELECT * loads use the same plain-identifier check and qualified-name rendering as a translated FROM. Null members of an IN/NOT IN list literal become IS NULL / IS NOT NULL plus any non-null values. The sqlite dialect rejects FLOOR and CEILING at render time. provider: 'sql' prefers jooq-sql only when jOOQ loads, otherwise native-sql; explicit jooq-sql is unchanged. provider: 'sql' asks each candidate whether its classes load, and an explicit provider name fails with that provider's message when they do not. @SInCE on the GEP-26 SQL API is 7.0.0; these types were not in the 6.0.0 release.
Both types stay package-private. Use the same incubating annotation as the other groovy-ginq-sql types.
Aggregate values keep the JDBC type the database returns. H2's AVG is a Double, while the collection provider returns BigDecimal.
denseRank() counted distinct preceding keys by comparing a null sentinel row, which dereferenced orderby e.prop. A range frame also passed a null order key to toBigDecimal. Treat a missing previous row as a new key, and resolve null keys as their own peer group in a range frame.
Name mixed-mode source columns and read them back by ordinal, and keep the join ordinal off the row so it cannot collide with a real column. Map == and != with null-safe comparison, and keep null rows for !in on a list or a constant range. Push + and text methods only when the operand kind is known. A dialect that cannot render a function leaves that clause in memory instead of emitting SQL it cannot run.
4eff449 to
c5e0d98
Compare

Continues #2658 .
Summary
Continues / Relates to #2658 (Paul King's GEP-26 reference implementation: GINQ SQL / jOOQ).
This branch rebases Paul's three reference commits onto current
master, resolves drift (includingverification-metadata.xml), pins checksums for the SQL provider dependencies, adds opt-in mixed-mode, finishes Phase 5 polish, pins H2 2.5.252 for ginq-sql tests, and applies a faithfulness audit so SQL results match Groovy/collection semantics where the GEP requires it.Credit: Paul King for the GEP-26 reference implementation (#2658).
Phase 5 / mixed mode / audit highlights
mixedMode: true.concat,isEmpty,abs/floor/ceil/round, constant ranges onin.dialect:ansi(default),h2,postgres,mysql,mariadb,sqlite./, null-safecontains/LIKE, emptyin/!in,COALESCE(SUM(...),0), jOOQ schema.table split.Tests (local, last tip)
:groovy-ginq-sql:test— 137 passed:groovy-ginq:test— 665 passedOut of scope / deferred (per GEP)
table(…)helper, jOOQRecordresults / runtimeDSLContext, async/streaming.