Skip to content
Open
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
4 changes: 4 additions & 0 deletions typed_sql/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 0.1.14
* Support `brin`, `hash`, `gin`, `gist` and `spgist` indexes through `@Index` annotations' `method` field.
* Support covering (`INCLUDE`) columns through `@Index` annotations' `covering` field.

## 0.1.13
* Support `CREATE INDEX` DDLs through `@Index` annotations.

Expand Down
2 changes: 2 additions & 0 deletions typed_sql/lib/src/codegen/build_code.dart
Original file line number Diff line number Diff line change
Expand Up @@ -433,6 +433,8 @@ Iterable<Spec> buildTable(ParsedTable table, ParsedSchema schema) sync* {
name: ${idx.name == null ? 'null' : '\'${idx.name}\''},
sqlName: ${idx.sqlName == null ? 'null' : '\'${idx.sqlName}\''},
columns: [${idx.fields.map((f) => '\'${f.sqlName}\'').join(', ')}],
method: .${idx.method.name},
covering: [${idx.covering.map((f) => '\'${f.sqlName}\'').join(', ')}],
)
''').join(', ')}
],
Expand Down
104 changes: 100 additions & 4 deletions typed_sql/lib/src/codegen/parse_library.dart
Original file line number Diff line number Diff line change
Expand Up @@ -654,18 +654,36 @@ Future<ParsedRowClass> _parseRowClass(
);
}

if (field.backingType == 'JsonValue') {
final method = ParsedIndexAccessMethod.values.firstWhere(
(m) => value.getField('_method')?.variable?.name == m.name,
orElse: () => .btree,
);

if (field.backingType == 'JsonValue' && method != .gin) {
await throwInvalidAnnotationInSource(
'JsonValue field cannot be used in an `Index` annotation',
'JsonValue field cannot be used in an `Index` annotation, '
'unless `method: .gin` is given',
annotatedElement: a,
annotation: elementAnnotation,
);
}

final covering = await _parseCoveringFields(
value: value,
fields: fields,
keyFields: [field],
method: method,
context: 'Index.field',
annotatedElement: a,
annotation: elementAnnotation,
);

indexes.add(
ParsedIndex(
name: name,
fields: [field],
method: method,
covering: covering,
),
);
}
Expand Down Expand Up @@ -706,6 +724,11 @@ Future<ParsedRowClass> _parseRowClass(
);
}

final method = ParsedIndexAccessMethod.values.firstWhere(
(m) => a.getField('_method')?.variable?.name == m.name,
orElse: () => .btree,
);

final indexFieldRefs = <ParsedField>[];
for (final fieldName in indexFields) {
final field = fields.firstWhereOrNull((f) => f.name == fieldName);
Expand All @@ -717,20 +740,33 @@ Future<ParsedRowClass> _parseRowClass(
annotation: ea,
);
}
if (field.backingType == 'JsonValue') {
if (field.backingType == 'JsonValue' && method != .gin) {
await throwInvalidAnnotationInSource(
'JsonValue field cannot be used in an `Index` annotation',
'JsonValue field cannot be used in an `Index` annotation, '
'unless `method: .gin` is given',
annotatedElement: cls,
annotation: ea,
);
}
indexFieldRefs.add(field);
}

final covering = await _parseCoveringFields(
value: a,
fields: fields,
keyFields: indexFieldRefs,
method: method,
context: 'Index',
annotatedElement: cls,
annotation: ea,
);

indexes.add(
ParsedIndex(
name: name == '-' ? null : name,
fields: indexFieldRefs,
method: method,
covering: covering,
),
);
}
Expand Down Expand Up @@ -1035,6 +1071,66 @@ Future<ParsedReferentialAction> _parseReferentialAction(
return first;
}

/// Parses the `covering` field of an `Index`/`Index.field` annotation,
/// resolving field names and validating they exist, don't overlap the key
/// fields, and are only used with [ParsedIndexAccessMethod.btree] or
/// [ParsedIndexAccessMethod.gist].
Future<List<ParsedField>> _parseCoveringFields({
required DartObject value,
required List<ParsedField> fields,
required List<ParsedField> keyFields,
required ParsedIndexAccessMethod method,
required String context,
required Element annotatedElement,
required ElementAnnotation annotation,
}) async {
final coveringNames =
value
.getField('_covering')
?.toListValue()
?.map(
(v) => v.toStringValue()!,
) ??
const <String>[];

if (coveringNames.isEmpty) {
return const [];
}

if (method != ParsedIndexAccessMethod.btree &&
method != ParsedIndexAccessMethod.gist) {
await throwInvalidAnnotationInSource(
'`$context(covering: ...)` is only supported for `method: .btree` or '
'`method: .gist` indexes',
annotatedElement: annotatedElement,
annotation: annotation,
);
}

final coveringFields = <ParsedField>[];
for (final fieldName in coveringNames) {
final field = fields.firstWhereOrNull((f) => f.name == fieldName);
if (field == null) {
await throwInvalidAnnotationInSource(
'`$context(covering: ...)` references unknown field "$fieldName", '
'no such field on row class.',
annotatedElement: annotatedElement,
annotation: annotation,
);
}
if (keyFields.any((f) => f.name == fieldName)) {
await throwInvalidAnnotationInSource(
'`$context(covering: ...)` references field "$fieldName", which is '
'already part of the index key.',
annotatedElement: annotatedElement,
annotation: annotation,
);
}
coveringFields.add(field);
}
return coveringFields;
}

String? _tryGetColumnType(DartType t) {
if (t.isDartCoreBool) {
return 'bool';
Expand Down
18 changes: 18 additions & 0 deletions typed_sql/lib/src/codegen/parsed_library.dart
Original file line number Diff line number Diff line change
Expand Up @@ -130,14 +130,32 @@ final class ParsedUniqueConstraint {
});
}

/// Parsed representation of [IndexAccessMethod].
///
/// This should always stay in sync with the [IndexAccessMethod] enum.
enum ParsedIndexAccessMethod {
brin,
btree,
gin,
gist,
hash,
spgist,
}

final class ParsedIndex {
/// The user-provided name segment for the index, or `null` to derive it from the indexed columns.
final String? name;
final List<ParsedField> fields;
final ParsedIndexAccessMethod method;

/// Non-key columns included for index-only scans.
final List<ParsedField> covering;

ParsedIndex({
required this.name,
required this.fields,
required this.method,
required this.covering,
});
}

Expand Down
9 changes: 8 additions & 1 deletion typed_sql/lib/src/dialect/mysql_dialect.dart
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,14 @@ final class _MysqlSqlDialect extends SqlDialect {
);
}),
// Indexes are emitted as separate statements after the tables.
...statements.expand((table) => createIndexStatements(table, escape)),
...statements.expand(
(table) => createIndexStatements(
table,
escape,
supportedMethods: {.hash},
typeClausePosition: .beforeOn,
),
),
];
return ScriptSqlTask(sqlStatements.toList());
}
Expand Down
9 changes: 8 additions & 1 deletion typed_sql/lib/src/dialect/postgres_dialect.dart
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,14 @@ final class _PostgresDialect extends SqlDialect {
);
}),
// Indexes are emitted as separate statements after the tables.
...statements.expand((table) => createIndexStatements(table, escape)),
...statements.expand(
(table) => createIndexStatements(
table,
escape,
supportedMethods: {.brin, .gin, .gist, .hash, .spgist},
supportsCoveringColumns: true,
),
),
];
if (resolver.context.parameters.isNotEmpty) {
throw AssertionError('Parameters are not allowed in DDL');
Expand Down
43 changes: 41 additions & 2 deletions typed_sql/lib/src/dialect/shared_dialect.dart
Original file line number Diff line number Diff line change
Expand Up @@ -48,22 +48,61 @@ String foreignKeyConstraintName(
].join('_');
}

/// Where the `USING ...` index-type clause goes in `CREATE INDEX`; differs
/// by dialect.
enum IndexTypeClausePosition {
/// `... ON table USING method (columns)` (PostgreSQL).
afterOn,

/// `... USING method ON table (columns)` (MySQL/MariaDB).
beforeOn,
}

/// Returns the `CREATE INDEX` statements for the indexes defined on [table].
///
/// [supportedMethods] are the [IndexAccessMethod]s supported beyond
/// [IndexAccessMethod.btree]; unsupported methods fall back to a plain index.
/// [supportsCoveringColumns] controls whether `INCLUDE` columns are emitted.
Iterable<String> createIndexStatements(
CreateTableStatement table,
String Function(String) escape,
) {
String Function(String) escape, {
Set<IndexAccessMethod> supportedMethods = const {},
IndexTypeClausePosition typeClausePosition = .afterOn,
bool supportsCoveringColumns = false,
}) {
return table.indexes.map((index) {
final indexName = [
table.tableName,
'idx',
if (index.sqlName == null) ...index.columns else index.sqlName,
].join('_');

final usingClause = switch (index.method) {
.brin when supportedMethods.contains(IndexAccessMethod.brin) =>
'USING BRIN',
.btree => null,
.gin when supportedMethods.contains(IndexAccessMethod.gin) => 'USING GIN',
.gist when supportedMethods.contains(IndexAccessMethod.gist) =>
'USING GIST',
.hash when supportedMethods.contains(IndexAccessMethod.hash) =>
'USING HASH',
.spgist when supportedMethods.contains(IndexAccessMethod.spgist) =>
'USING SPGIST',
_ => null,
};
final beforeOnClause = typeClausePosition == .beforeOn ? usingClause : null;
final afterOnClause = typeClausePosition == .afterOn ? usingClause : null;
final covering = supportsCoveringColumns
? index.covering
: const <String>[];

return <String>[
'CREATE INDEX ${escape(indexName)}',
?beforeOnClause,
'ON ${escape(table.tableName)}',
?afterOnClause,
'(${index.columns.map(escape).join(', ')})',
if (covering.isNotEmpty) 'INCLUDE (${covering.map(escape).join(', ')})',
].join(' ');
});
}
Loading
Loading