Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
136 changes: 108 additions & 28 deletions docs/postgresql.md

Large diffs are not rendered by default.

13 changes: 13 additions & 0 deletions docs/queries.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,14 @@ configuration entry.
### Sources

- The `FROM` item must be a table of that schema, optionally with an alias.
- sqlcj models one namespace, so a source resolves by its unqualified name:
`users`, `public.users`, and `"public"."users"` are one table, matched without
SQL identifier delimiters and case-insensitively. A source qualified with any
other schema names a table sqlcj leaves unmodeled and is rejected as
`Table not found in schema: <schema>.<t>`, in a `FROM`, a `JOIN`, and the
target of an `INSERT`, an `UPDATE`, and a `DELETE` alike. `search_path` is
not followed; see
[Schema Snapshot Input](postgresql.md#schema-snapshot-input).
- A table may be joined with `JOIN`, `INNER JOIN`, `LEFT JOIN`, or
`LEFT OUTER JOIN`, and inner and left joins may be chained in any order. A
comma-separated source list and every other join modifier — `RIGHT`, `FULL`, a
Expand Down Expand Up @@ -900,6 +908,11 @@ Reads:
`QueryAnalyzerTest.shouldResolveQueryParametersInsideNestedAndOrExpressions`,
and `QueryAnalyzerTest.shouldResolveQueryParametersInsideInExpression` cover
the accepted read shapes.
- `QueryAnalyzerTest.shouldResolveAPublicQualifiedSource` covers the
unqualified, `public`-qualified, quoted, and upper-case qualifier forms of one
source, and `QueryAnalyzerTest.shouldRejectASourceOfAnotherSchema` covers the
rejected message of a source of another schema in a `FROM`, an `INNER` and a
`LEFT JOIN`, and an `INSERT`, an `UPDATE`, and a `DELETE` target.
- `SqlcjCompilerIntegrationTest.shouldExecuteGeneratedAliasedQualifiedQuery`,
`SqlcjCompilerIntegrationTest.shouldExecuteGeneratedJoinQueryWithDuplicateColumnNames`,
`SqlcjCompilerIntegrationTest.shouldExecuteGeneratedJoinQueryWithProjectionAliases`,
Expand Down
19 changes: 18 additions & 1 deletion sqlcj-cli/src/main/java/dev/sqlcj/analysis/QueryAnalyzer.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import dev.sqlcj.schema.ColumnType;
import dev.sqlcj.schema.Schema;
import dev.sqlcj.schema.parser.ColumnTypeMapping;
import dev.sqlcj.schema.parser.SchemaNamespace;
import dev.sqlcj.sql.ParsedSql;
import dev.sqlcj.type.DefaultTypeResolver;
import dev.sqlcj.type.TypeResolver;
Expand Down Expand Up @@ -1017,11 +1018,27 @@ private Source toSource(Table table, Schema schema, boolean leftJoined) {
table.getAlias() == null
? table.getUnquotedName()
: table.getAlias().getUnquotedName(),
findTable(schema, table.getUnquotedName()),
findSourceTable(schema, table),
leftJoined
);
}

/**
* The modeled table one query source names. sqlcj models one namespace, so
* an unqualified and a {@code public}-qualified name are the same table,
* and a name qualified with any other schema names a table sqlcj leaves
* unmodeled and fails as a missing table.
*/
private dev.sqlcj.schema.Table findSourceTable(Schema schema, Table table) {
if (!SchemaNamespace.isModeled(table)) {
throw new IllegalArgumentException(
"Table not found in schema: " + SchemaNamespace.declaredName(table)
);
}

return findTable(schema, table.getUnquotedName());
}

/**
* Accepts only a bare {@code JOIN} or explicit {@code INNER JOIN}, or a
* {@code LEFT JOIN} in its bare or {@code LEFT OUTER JOIN} spelling, of one
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@

import dev.sqlcj.schema.ColumnType;
import dev.sqlcj.schema.EnumType;
import net.sf.jsqlparser.schema.MultiPartName;
import net.sf.jsqlparser.statement.create.table.ColDataType;

import java.util.List;
import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
Expand All @@ -22,6 +22,9 @@ public final class ColumnTypeMapping {
/** One parenthesized type argument group, such as {@code (10, 2)}. */
private static final Pattern TYPE_ARGUMENTS = Pattern.compile("\\([^)]*\\)");

/** The array dimension groups a declared type text ends with. */
private static final Pattern TRAILING_DIMENSIONS = Pattern.compile("(\\[[^\\[\\]]*\\])+$");

/**
* One declared SQL type and the type sqlcj maps it to.
*
Expand Down Expand Up @@ -64,7 +67,8 @@ public boolean mapped() {
* recorded like any other unmapped type.
*/
public MappedType map(ColDataType declaredType, List<EnumType> enums) {
String typeName = typeName(declaredType);
String elementText = elementText(declaredType.getDataType());
String typeName = typeName(elementText);
int arrayDimensions = arrayDimensions(declaredType);
boolean array = arrayDimensions == 1;

Expand All @@ -83,7 +87,7 @@ public MappedType map(ColDataType declaredType, List<EnumType> enums) {
);
}
} else {
EnumType enumType = declaredEnum(declaredType, enums);
EnumType enumType = declaredEnum(elementText, enums);

if (enumType != null) {
return new MappedType(
Expand Down Expand Up @@ -137,13 +141,18 @@ public boolean isArrayElementType(ColumnType type) {
}

/**
* The enum type an unmapped declared type names, or {@code null} when the
* schema declares no such type. The declared type is matched without its
* SQL identifier delimiters and case-insensitively, as PostgreSQL resolves
* an unquoted type name.
* The enum type an unmapped declared element type names, or {@code null}
* when the schema declares no such type. The name is resolved in the one
* namespace sqlcj models, so a {@code public}-qualified name resolves as
* its unqualified name and a name of any other schema never resolves; see
* {@link SchemaNamespace}.
*/
private EnumType declaredEnum(ColDataType declaredType, List<EnumType> enums) {
String typeName = MultiPartName.unquote(declaredType.getDataType());
private EnumType declaredEnum(String elementText, List<EnumType> enums) {
String typeName = SchemaNamespace.modeledTypeName(elementText);

if (typeName == null) {
return null;
}

return enums.stream()
.filter(enumType -> enumType.name().equalsIgnoreCase(typeName))
Expand All @@ -154,28 +163,44 @@ private EnumType declaredEnum(ColDataType declaredType, List<EnumType> enums) {
/**
* Reports how many array dimensions a declaration states. The parser
* reports an array's dimensions separately from its element type, so a
* declaration with any dimension is an array of the reported type.
* declaration with any dimension is an array of the reported type, except
* that a qualified element type carries its dimensions inside its own text,
* as in {@code public.mood[]}.
*/
private int arrayDimensions(ColDataType declaredType) {
List<Integer> arrayData = declaredType.getArrayData();

return arrayData == null
int dimensions = arrayData == null
? 0
: arrayData.size();

return dimensions + textDimensions(declaredType.getDataType());
}

/** How many array dimension groups a declared type text itself ends with. */
private int textDimensions(String dataType) {
Matcher matcher = TRAILING_DIMENSIONS.matcher(dataType);

if (!matcher.find()) {
return 0;
}

return (int) matcher.group().chars().filter(character -> character == '[').count();
}

/** The declared element type text, without its array dimension groups. */
private String elementText(String dataType) {
return TRAILING_DIMENSIONS.matcher(dataType).replaceAll("");
}

/**
* Returns the canonical spelling of a declared SQL type: upper case,
* Returns the canonical spelling of a declared element type: upper case,
* without type arguments such as a length or a precision, and with single
* spaces between the remaining words.
*/
private String typeName(ColDataType declaredType) {
String dataType = declaredType
.getDataType()
.toUpperCase(Locale.ROOT);

private String typeName(String elementText) {
return TYPE_ARGUMENTS
.matcher(dataType)
.matcher(elementText.toUpperCase(Locale.ROOT))
.replaceAll(" ")
.replaceAll("\\s+", " ")
.trim();
Expand Down
Loading
Loading