feat(parser): extend Parser with parseColumnCollation
This commit is contained in:
1 parent
4e101e46eb
commit
c2329a388e
1 file changed
+190
-6
@@ -500,7 +500,153 @@ class Parser(private val tokens: List<Token>) {
|
||||
else -> break
|
||||
}
|
||||
}
|
||||
return Column(columnName, type, notNull, primaryKey)
|
||||
if (generated != null && default != null) {
|
||||
throw error("a column cannot declare both DEFAULT and GENERATED ALWAYS AS", peek())
|
||||
}
|
||||
if (generated != null && primaryKey) {
|
||||
throw error("a GENERATED column cannot be a PRIMARY KEY", peek())
|
||||
}
|
||||
return Column(columnName, spec.type, notNull, primaryKey, spec.precision, spec.scale, default, generated, collation)
|
||||
}
|
||||
|
||||
/** `COLLATE name` on a column definition (`CREATE TABLE`/`ALTER TABLE ADD COLUMN`) - reuses the same name validation as expression-level `expr COLLATE name` (see [parseCollate]). */
|
||||
private fun parseColumnCollation(): Collation {
|
||||
advance()
|
||||
val nameToken = expect(TokenType.Identifier, "expected a collation name after COLLATE")
|
||||
return Collation.fromName(nameToken.lexeme) ?: throw error("no such collating sequence: ${nameToken.lexeme}", nameToken)
|
||||
}
|
||||
|
||||
/**
|
||||
* `GENERATED ALWAYS AS ( expression ) STORED` - Postgres only ever supports `STORED` (unlike
|
||||
* MySQL/SQLite, which also allow `VIRTUAL`), so this engine matches Postgres exactly rather than
|
||||
* accepting a keyword it would then have to silently reinterpret. `GENERATED`/`ALWAYS`/`STORED`
|
||||
* are contextual (same mechanism as `ROLLUP`/`CUBE`/`FILTER`), not reserved keywords, so a table
|
||||
* can still name a column `generated` without quoting.
|
||||
*/
|
||||
private fun parseGeneratedClause(): Expression {
|
||||
advance()
|
||||
if (!isContextualKeyword("ALWAYS")) throw error("expected ALWAYS after GENERATED", peek())
|
||||
advance()
|
||||
expect(TokenType.As, "expected AS after GENERATED ALWAYS")
|
||||
expect(TokenType.LeftParen, "expected '(' after GENERATED ALWAYS AS")
|
||||
val expression = parseExpression()
|
||||
expect(TokenType.RightParen, "expected ')' to close GENERATED ALWAYS AS expression")
|
||||
if (!isContextualKeyword("STORED")) throw error("expected STORED after GENERATED ALWAYS AS (...)", peek())
|
||||
advance()
|
||||
return expression
|
||||
}
|
||||
|
||||
private fun parseTableUnique(): UniqueConstraint {
|
||||
expect(TokenType.Unique, "expected UNIQUE")
|
||||
expect(TokenType.LeftParen, "expected '(' after UNIQUE")
|
||||
val columns = mutableListOf<String>()
|
||||
do {
|
||||
columns.add(expectIdentifier("expected column name"))
|
||||
} while (match(TokenType.Comma))
|
||||
expect(TokenType.RightParen, "expected ')' after UNIQUE column list")
|
||||
return UniqueConstraint(null, columns)
|
||||
}
|
||||
|
||||
private fun parseTableForeignKey(): ForeignKeyConstraint {
|
||||
expect(TokenType.Foreign, "expected FOREIGN")
|
||||
expect(TokenType.Key, "expected KEY after FOREIGN")
|
||||
expect(TokenType.LeftParen, "expected '(' after FOREIGN KEY")
|
||||
val columnNames = mutableListOf<String>()
|
||||
do {
|
||||
columnNames.add(expectIdentifier("expected column name"))
|
||||
} while (match(TokenType.Comma))
|
||||
expect(TokenType.RightParen, "expected ')' after foreign key column list")
|
||||
return parseInlineForeignKey(columnNames)
|
||||
}
|
||||
|
||||
private fun parseInlineForeignKey(columnName: String): ForeignKeyConstraint = parseInlineForeignKey(listOf(columnName))
|
||||
|
||||
private fun parseInlineForeignKey(columnNames: List<String>): ForeignKeyConstraint {
|
||||
expect(TokenType.References, "expected REFERENCES")
|
||||
val refTable = expectIdentifier("expected referenced table name")
|
||||
expect(TokenType.LeftParen, "expected '(' before referenced column list")
|
||||
val refColumns = mutableListOf<String>()
|
||||
do {
|
||||
refColumns.add(expectIdentifier("expected referenced column name"))
|
||||
} while (match(TokenType.Comma))
|
||||
expect(TokenType.RightParen, "expected ')' after referenced column list")
|
||||
if (refColumns.size != columnNames.size) {
|
||||
throw error(
|
||||
"FOREIGN KEY (${columnNames.joinToString(", ")}) has ${columnNames.size} column(s) but " +
|
||||
"REFERENCES $refTable(${refColumns.joinToString(", ")}) has ${refColumns.size}",
|
||||
peek()
|
||||
)
|
||||
}
|
||||
var onDelete = ForeignKeyAction.NoAction
|
||||
var onUpdate = ForeignKeyAction.NoAction
|
||||
while (check(TokenType.On)) {
|
||||
advance()
|
||||
when {
|
||||
match(TokenType.Delete) -> onDelete = parseForeignKeyAction()
|
||||
match(TokenType.Update) -> onUpdate = parseForeignKeyAction()
|
||||
else -> throw error("expected DELETE or UPDATE after ON", peek())
|
||||
}
|
||||
}
|
||||
return ForeignKeyConstraint(columnNames, refTable, refColumns, onDelete, onUpdate)
|
||||
}
|
||||
|
||||
private fun parseForeignKeyAction(): ForeignKeyAction = when {
|
||||
match(TokenType.Cascade) -> ForeignKeyAction.Cascade
|
||||
match(TokenType.Restrict) -> ForeignKeyAction.Restrict
|
||||
match(TokenType.Set) -> {
|
||||
expect(TokenType.Null, "expected NULL after SET")
|
||||
ForeignKeyAction.SetNull
|
||||
}
|
||||
match(TokenType.No) -> {
|
||||
expect(TokenType.Action, "expected ACTION after NO")
|
||||
ForeignKeyAction.NoAction
|
||||
}
|
||||
else -> throw error("expected CASCADE, RESTRICT, SET NULL, or NO ACTION but found '${describe(peek())}'", peek())
|
||||
}
|
||||
|
||||
private fun parseTableCheck(): CheckConstraint = parseCheckConstraint()
|
||||
|
||||
private fun parseColumnCheck(): CheckConstraint = parseCheckConstraint()
|
||||
|
||||
private fun parseCheckConstraint(): CheckConstraint {
|
||||
expect(TokenType.Check, "expected CHECK")
|
||||
expect(TokenType.LeftParen, "expected '(' after CHECK")
|
||||
val expression = parseExpression()
|
||||
expect(TokenType.RightParen, "expected ')' after CHECK expression")
|
||||
return CheckConstraint(null, expression)
|
||||
}
|
||||
|
||||
private data class TypeSpec(val type: ColumnType, val precision: Int?, val scale: Int?)
|
||||
|
||||
/** Shared by column definitions (`CREATE TABLE`) and `CAST(... AS type)`. */
|
||||
private fun parseTypeSpec(): TypeSpec {
|
||||
val typeToken = advance()
|
||||
val type = when (typeToken.type) {
|
||||
TokenType.Integer -> ColumnType.Integer
|
||||
TokenType.Text -> ColumnType.Text
|
||||
TokenType.Boolean -> ColumnType.Boolean
|
||||
TokenType.Real -> ColumnType.Real
|
||||
TokenType.Decimal -> ColumnType.Decimal
|
||||
TokenType.Date -> ColumnType.Date
|
||||
else -> throw error("expected a type but found '${describe(typeToken)}'", typeToken)
|
||||
}
|
||||
var precision: Int? = null
|
||||
var scale: Int? = null
|
||||
if (type == ColumnType.Decimal && match(TokenType.LeftParen)) {
|
||||
precision = expectIntLiteral("expected precision in DECIMAL(...)")
|
||||
scale = if (match(TokenType.Comma)) expectIntLiteral("expected scale in DECIMAL(precision, scale)") else 0
|
||||
expect(TokenType.RightParen, "expected ')' after DECIMAL precision/scale")
|
||||
}
|
||||
return TypeSpec(type, precision, scale)
|
||||
}
|
||||
|
||||
private fun expectIntLiteral(message: String): Int = expect(TokenType.IntLiteral, message).intValue!!.toInt()
|
||||
|
||||
/** A sequence option's bound (MINVALUE/MAXVALUE/START WITH/RESTART WITH) needs the full 64-bit range, unlike [expectIntLiteral]'s callers - and, unlike those, may be negative for a descending sequence. */
|
||||
private fun expectLongLiteral(message: String): Long {
|
||||
val negative = match(TokenType.Minus)
|
||||
val value = expect(TokenType.IntLiteral, message).intValue!!
|
||||
return if (negative) -value else value
|
||||
}
|
||||
|
||||
private fun parseInsert(): InsertStatement {
|
||||
@@ -517,24 +663,62 @@ class Parser(private val tokens: List<Token>) {
|
||||
columns = names
|
||||
}
|
||||
expect(TokenType.Values, "expected VALUES")
|
||||
val rows = mutableListOf<List<Expression>>()
|
||||
val rows = mutableListOf<List<Expression?>>()
|
||||
do {
|
||||
expect(TokenType.LeftParen, "expected '(' before value list")
|
||||
val values = mutableListOf<Expression>()
|
||||
val values = mutableListOf<Expression?>()
|
||||
do {
|
||||
values.add(parseExpression())
|
||||
values.add(if (match(TokenType.Default)) null else parseExpression())
|
||||
} while (match(TokenType.Comma))
|
||||
expect(TokenType.RightParen, "expected ')' after value list")
|
||||
rows.add(values)
|
||||
} while (match(TokenType.Comma))
|
||||
return InsertStatement(tableName, columns, rows)
|
||||
val onConflict = if (check(TokenType.On)) parseOnConflict() else null
|
||||
val returning = parseReturning()
|
||||
return InsertStatement(tableName, columns, rows, onConflict, returning)
|
||||
}
|
||||
|
||||
private fun parseReturning(): Projection? = if (match(TokenType.Returning)) parseProjection() else null
|
||||
|
||||
private fun parseOnConflict(): OnConflictClause {
|
||||
expect(TokenType.On, "expected ON")
|
||||
expect(TokenType.Conflict, "expected CONFLICT after ON")
|
||||
var targetColumns: List<String> = emptyList()
|
||||
if (match(TokenType.LeftParen)) {
|
||||
val names = mutableListOf<String>()
|
||||
do {
|
||||
names.add(expectIdentifier("expected column name"))
|
||||
} while (match(TokenType.Comma))
|
||||
expect(TokenType.RightParen, "expected ')' after ON CONFLICT column list")
|
||||
targetColumns = names
|
||||
}
|
||||
expect(TokenType.Do, "expected DO after ON CONFLICT")
|
||||
val action: ConflictAction = when {
|
||||
match(TokenType.Nothing) -> ConflictDoNothing
|
||||
match(TokenType.Update) -> {
|
||||
expect(TokenType.Set, "expected SET after DO UPDATE")
|
||||
val assignments = parseAssignmentList()
|
||||
val where = if (match(TokenType.Where)) parseExpression() else null
|
||||
ConflictDoUpdate(assignments, where)
|
||||
}
|
||||
else -> throw error("expected NOTHING or UPDATE after DO but found '${describe(peek())}'", peek())
|
||||
}
|
||||
return OnConflictClause(targetColumns, action)
|
||||
}
|
||||
|
||||
private fun parseSelect(): SelectStatement {
|
||||
val core = parseSelectCore()
|
||||
val orderBy = parseOrderBy()
|
||||
val (limit, offset) = parseLimitOffset()
|
||||
return core.copy(orderBy = orderBy, limit = limit, offset = offset)
|
||||
}
|
||||
|
||||
private fun parseSelectCore(): SelectStatement {
|
||||
expect(TokenType.Select, "expected SELECT")
|
||||
val (distinct, distinctOn) = parseDistinctClause()
|
||||
val projection = parseProjection()
|
||||
expect(TokenType.From, "expected FROM")
|
||||
val (tableName, tableAlias) = parseTableRef()
|
||||
val from = parseFromItem()
|
||||
val joins = parseJoins()
|
||||
val where = if (match(TokenType.Where)) parseExpression() else null
|
||||
val groupBy = parseGroupBy()
|
||||
|
||||
Reference in new issue
Block a user