mirror of https://github.com/AMT-Cheif/drift.git
Export table structure from CREATE TABLE statements
This commit is contained in:
parent
3a2646e837
commit
a550a49705
|
@ -5,6 +5,7 @@ import 'package:sqlparser/sqlparser.dart';
|
||||||
import 'package:sqlparser/src/reader/tokenizer/token.dart';
|
import 'package:sqlparser/src/reader/tokenizer/token.dart';
|
||||||
|
|
||||||
part 'schema/column.dart';
|
part 'schema/column.dart';
|
||||||
|
part 'schema/from_create_table.dart';
|
||||||
part 'schema/references.dart';
|
part 'schema/references.dart';
|
||||||
part 'schema/table.dart';
|
part 'schema/table.dart';
|
||||||
|
|
||||||
|
|
|
@ -16,10 +16,16 @@ class TableColumn extends Column {
|
||||||
/// The type of this column, which is immediately available.
|
/// The type of this column, which is immediately available.
|
||||||
final ResolvedType type;
|
final ResolvedType type;
|
||||||
|
|
||||||
|
/// The column constraints set on this column.
|
||||||
|
///
|
||||||
|
/// See also:
|
||||||
|
/// - https://www.sqlite.org/syntax/column-constraint.html
|
||||||
|
final List<ColumnConstraint> constraints;
|
||||||
|
|
||||||
/// The table this column belongs to.
|
/// The table this column belongs to.
|
||||||
Table table;
|
Table table;
|
||||||
|
|
||||||
TableColumn(this.name, this.type);
|
TableColumn(this.name, this.type, {this.constraints = const []});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A column that is created by an expression. For instance, in the select
|
/// A column that is created by an expression. For instance, in the select
|
||||||
|
|
|
@ -0,0 +1,52 @@
|
||||||
|
part of '../analysis.dart';
|
||||||
|
|
||||||
|
/// Reads the [Table] definition from a [CreateTableStatement].
|
||||||
|
class SchemaFromCreateTable {
|
||||||
|
Table read(CreateTableStatement stmt) {
|
||||||
|
return Table(
|
||||||
|
name: stmt.tableName,
|
||||||
|
resolvedColumns: [for (var def in stmt.columns) _readColumn(def)],
|
||||||
|
withoutRowId: stmt.withoutRowId,
|
||||||
|
tableConstraints: stmt.tableConstraints,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
TableColumn _readColumn(ColumnDefinition definition) {
|
||||||
|
final affinity = columnAffinity(definition.typeName);
|
||||||
|
final nullable = !definition.constraints.any((c) => c is NotNull);
|
||||||
|
|
||||||
|
final resolvedType = ResolvedType(type: affinity, nullable: nullable);
|
||||||
|
|
||||||
|
return TableColumn(
|
||||||
|
definition.columnName,
|
||||||
|
resolvedType,
|
||||||
|
constraints: definition.constraints,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Looks up the correct column affinity for a declared type name with the
|
||||||
|
/// rules described here:
|
||||||
|
/// https://www.sqlite.org/datatype3.html#determination_of_column_affinity
|
||||||
|
@visibleForTesting
|
||||||
|
BasicType columnAffinity(String typeName) {
|
||||||
|
if (typeName == null) {
|
||||||
|
return BasicType.blob;
|
||||||
|
}
|
||||||
|
|
||||||
|
final upper = typeName.toUpperCase();
|
||||||
|
if (upper.contains('INT')) {
|
||||||
|
return BasicType.int;
|
||||||
|
}
|
||||||
|
if (upper.contains('CHAR') ||
|
||||||
|
upper.contains('CLOB') ||
|
||||||
|
upper.contains('TEXT')) {
|
||||||
|
return BasicType.text;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (upper.contains('BLOB')) {
|
||||||
|
return BasicType.blob;
|
||||||
|
}
|
||||||
|
|
||||||
|
return BasicType.real;
|
||||||
|
}
|
||||||
|
}
|
|
@ -30,8 +30,18 @@ class Table with ResultSet, VisibleToChildren {
|
||||||
@override
|
@override
|
||||||
final List<TableColumn> resolvedColumns;
|
final List<TableColumn> resolvedColumns;
|
||||||
|
|
||||||
|
/// Whether this table was created with an "WITHOUT ROWID" modifier
|
||||||
|
final bool withoutRowId;
|
||||||
|
|
||||||
|
/// Additional constraints set on this table.
|
||||||
|
final List<TableConstraint> tableConstraints;
|
||||||
|
|
||||||
/// Constructs a table from the known [name] and [resolvedColumns].
|
/// Constructs a table from the known [name] and [resolvedColumns].
|
||||||
Table({@required this.name, this.resolvedColumns}) {
|
Table(
|
||||||
|
{@required this.name,
|
||||||
|
this.resolvedColumns,
|
||||||
|
this.withoutRowId = false,
|
||||||
|
this.tableConstraints = const []}) {
|
||||||
for (var column in resolvedColumns) {
|
for (var column in resolvedColumns) {
|
||||||
column.table = this;
|
column.table = this;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,12 @@
|
||||||
part of 'parser.dart';
|
part of 'parser.dart';
|
||||||
|
|
||||||
|
const _tokensInTypename = [
|
||||||
|
TokenType.identifier,
|
||||||
|
TokenType.leftParen,
|
||||||
|
TokenType.rightParen,
|
||||||
|
TokenType.numberLiteral,
|
||||||
|
];
|
||||||
|
|
||||||
mixin SchemaParser on ParserBase {
|
mixin SchemaParser on ParserBase {
|
||||||
CreateTableStatement _createTable() {
|
CreateTableStatement _createTable() {
|
||||||
if (!_matchOne(TokenType.create)) return null;
|
if (!_matchOne(TokenType.create)) return null;
|
||||||
|
@ -62,12 +69,15 @@ mixin SchemaParser on ParserBase {
|
||||||
ColumnDefinition _columnDefinition() {
|
ColumnDefinition _columnDefinition() {
|
||||||
final name = _consume(TokenType.identifier, 'Expected a column name')
|
final name = _consume(TokenType.identifier, 'Expected a column name')
|
||||||
as IdentifierToken;
|
as IdentifierToken;
|
||||||
IdentifierToken typeName;
|
|
||||||
|
|
||||||
if (_matchOne(TokenType.identifier)) {
|
final typeNameBuilder = StringBuffer();
|
||||||
typeName = _previous as IdentifierToken;
|
while (_match(_tokensInTypename)) {
|
||||||
|
typeNameBuilder.write(_previous.lexeme);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final typeName =
|
||||||
|
typeNameBuilder.isEmpty ? null : typeNameBuilder.toString();
|
||||||
|
|
||||||
final constraints = <ColumnConstraint>[];
|
final constraints = <ColumnConstraint>[];
|
||||||
ColumnConstraint constraint;
|
ColumnConstraint constraint;
|
||||||
while ((constraint = _columnConstraint(orNull: true)) != null) {
|
while ((constraint = _columnConstraint(orNull: true)) != null) {
|
||||||
|
@ -76,7 +86,7 @@ mixin SchemaParser on ParserBase {
|
||||||
|
|
||||||
return ColumnDefinition(
|
return ColumnDefinition(
|
||||||
columnName: name.identifier,
|
columnName: name.identifier,
|
||||||
typeName: typeName?.identifier,
|
typeName: typeName,
|
||||||
constraints: constraints,
|
constraints: constraints,
|
||||||
)..setSpan(name, _previous);
|
)..setSpan(name, _previous);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,64 @@
|
||||||
|
import 'package:sqlparser/sqlparser.dart';
|
||||||
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
|
import '../../common_data.dart';
|
||||||
|
|
||||||
|
const _affinityTests = {
|
||||||
|
'INT': BasicType.int,
|
||||||
|
'INTEGER': BasicType.int,
|
||||||
|
'TINYINT': BasicType.int,
|
||||||
|
'SMALLINT': BasicType.int,
|
||||||
|
'MEDIUMINT': BasicType.int,
|
||||||
|
'BIGINT': BasicType.int,
|
||||||
|
'UNISGNED BIG INT': BasicType.int,
|
||||||
|
'INT2': BasicType.int,
|
||||||
|
'INT8': BasicType.int,
|
||||||
|
'CHARACTER(20)': BasicType.text,
|
||||||
|
'CHARACTER(255)': BasicType.text,
|
||||||
|
'VARYING CHARACTER(255)': BasicType.text,
|
||||||
|
'NCHAR(55)': BasicType.text,
|
||||||
|
'NATIVE CHARACTER(70)': BasicType.text,
|
||||||
|
'NVARCHAR(100)': BasicType.text,
|
||||||
|
'TEXT': BasicType.text,
|
||||||
|
'CLOB': BasicType.text,
|
||||||
|
'BLOB': BasicType.blob,
|
||||||
|
null: BasicType.blob,
|
||||||
|
'REAL': BasicType.real,
|
||||||
|
'DOUBLE': BasicType.real,
|
||||||
|
'DOUBLE PRECISION': BasicType.real,
|
||||||
|
'FLOAT': BasicType.real,
|
||||||
|
'NUMERIC': BasicType.real,
|
||||||
|
'DECIMAL(10,5)': BasicType.real,
|
||||||
|
'BOOLEAN': BasicType.real,
|
||||||
|
'DATE': BasicType.real,
|
||||||
|
'DATETIME': BasicType.real,
|
||||||
|
};
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
test('affinity from typename', () {
|
||||||
|
final resolver = SchemaFromCreateTable();
|
||||||
|
|
||||||
|
_affinityTests.forEach((key, value) {
|
||||||
|
expect(resolver.columnAffinity(key), equals(value),
|
||||||
|
reason: '$key should have $value affinity');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('export table structure', () {
|
||||||
|
final engine = SqlEngine();
|
||||||
|
final stmt = engine.parse(createTableStmt).rootNode;
|
||||||
|
|
||||||
|
final table = SchemaFromCreateTable().read(stmt as CreateTableStatement);
|
||||||
|
|
||||||
|
expect(table.resolvedColumns.map((c) => c.name),
|
||||||
|
['id', 'email', 'score', 'display_name']);
|
||||||
|
expect(table.resolvedColumns.map((c) => c.type), const [
|
||||||
|
ResolvedType(type: BasicType.int),
|
||||||
|
ResolvedType(type: BasicType.text),
|
||||||
|
ResolvedType(type: BasicType.int),
|
||||||
|
ResolvedType(type: BasicType.text, nullable: true),
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(table.tableConstraints, hasLength(2));
|
||||||
|
});
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
const createTableStmt = '''
|
||||||
|
CREATE TABLE IF NOT EXISTS users (
|
||||||
|
id INT NOT NULL PRIMARY KEY DESC ON CONFLICT ROLLBACK AUTOINCREMENT,
|
||||||
|
email VARCHAR NOT NULL UNIQUE ON CONFLICT ABORT,
|
||||||
|
score INT CONSTRAINT "score set" NOT NULL DEFAULT 420 CHECK (score > 0),
|
||||||
|
display_name VARCHAR COLLATE BINARY
|
||||||
|
REFERENCES some(thing) ON UPDATE CASCADE ON DELETE SET NULL,
|
||||||
|
|
||||||
|
UNIQUE (score, display_name) ON CONFLICT ABORT,
|
||||||
|
FOREIGN KEY (id, email) REFERENCES another (a, b)
|
||||||
|
ON DELETE NO ACTION ON UPDATE RESTRICT
|
||||||
|
)
|
||||||
|
''';
|
|
@ -1,24 +1,11 @@
|
||||||
import 'package:sqlparser/src/ast/ast.dart';
|
import 'package:sqlparser/src/ast/ast.dart';
|
||||||
|
|
||||||
|
import '../common_data.dart';
|
||||||
import 'utils.dart';
|
import 'utils.dart';
|
||||||
|
|
||||||
final statement = '''
|
|
||||||
CREATE TABLE IF NOT EXISTS users (
|
|
||||||
id INT NOT NULL PRIMARY KEY DESC ON CONFLICT ROLLBACK AUTOINCREMENT,
|
|
||||||
email VARCHAR NOT NULL UNIQUE ON CONFLICT ABORT,
|
|
||||||
score INT CONSTRAINT "score set" NOT NULL DEFAULT 420 CHECK (score > 0),
|
|
||||||
display_name VARCHAR COLLATE BINARY
|
|
||||||
REFERENCES some(thing) ON UPDATE CASCADE ON DELETE SET NULL,
|
|
||||||
|
|
||||||
UNIQUE (score, display_name) ON CONFLICT ABORT,
|
|
||||||
FOREIGN KEY (id, email) REFERENCES another (a, b)
|
|
||||||
ON DELETE NO ACTION ON UPDATE RESTRICT
|
|
||||||
)
|
|
||||||
''';
|
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
testStatement(
|
testStatement(
|
||||||
statement,
|
createTableStmt,
|
||||||
CreateTableStatement(
|
CreateTableStatement(
|
||||||
tableName: 'users',
|
tableName: 'users',
|
||||||
ifNotExists: true,
|
ifNotExists: true,
|
||||||
|
|
Loading…
Reference in New Issue