Skip to content

Commit 97f4e84

Browse files
kyleconroyclaude
andcommitted
Add support for MoneyLiteral, RealLiteral and IdentifierLiteral in procedures
- Add TokenMoney lexer support with currency symbol detection ($, £, ¥, etc.) - Add RealLiteral for scientific notation (2e, 1.5e3, etc.) - Add MoneyLiteral AST type and marshaling - Add RealLiteral AST type and marshaling - Handle scientific notation in lexer's readNumber() - Convert single-identifier defaults to IdentifierLiteral in proc params Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent ea548ca commit 97f4e84

8 files changed

Lines changed: 150 additions & 4 deletions

File tree

ast/money_literal.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package ast
2+
3+
// MoneyLiteral represents a money/currency literal.
4+
type MoneyLiteral struct {
5+
LiteralType string `json:"LiteralType,omitempty"`
6+
Value string `json:"Value,omitempty"`
7+
}
8+
9+
func (*MoneyLiteral) node() {}
10+
func (*MoneyLiteral) scalarExpression() {}

ast/real_literal.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package ast
2+
3+
// RealLiteral represents a real (scientific notation) literal.
4+
type RealLiteral struct {
5+
LiteralType string `json:"LiteralType,omitempty"`
6+
Value string `json:"Value,omitempty"`
7+
}
8+
9+
func (*RealLiteral) node() {}
10+
func (*RealLiteral) scalarExpression() {}

parser/lexer.go

Lines changed: 85 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ const (
1818
TokenString
1919
TokenNationalString
2020
TokenBinary
21+
TokenMoney
2122
TokenStar
2223
TokenComma
2324
TokenDot
@@ -491,8 +492,11 @@ func (l *Lexer) NextToken() Token {
491492
case '"':
492493
tok = l.readDoubleQuotedIdentifier()
493494
default:
494-
// Handle $ only if followed by a letter (for pseudo-columns like $ROWGUID)
495-
if l.ch == '$' && isLetter(l.peekChar()) {
495+
// Handle currency symbols for money literals
496+
if l.isCurrencySymbol() {
497+
tok = l.readMoneyLiteral()
498+
} else if l.ch == '$' && isLetter(l.peekChar()) {
499+
// Handle $ only if followed by a letter (for pseudo-columns like $ROWGUID)
496500
tok = l.readIdentifier()
497501
} else if isLetter(l.ch) || l.ch == '_' || l.ch == '@' || l.ch == '#' {
498502
tok = l.readIdentifier()
@@ -817,6 +821,19 @@ func (l *Lexer) readNumber() Token {
817821
}
818822
}
819823
}
824+
// Handle scientific notation (e.g., 2e, 2e+5, 2E-10, 1.5e3)
825+
// T-SQL allows 'e' without exponent digits (e.g., "2e" is a valid real literal)
826+
if l.ch == 'e' || l.ch == 'E' {
827+
l.readChar() // consume e/E
828+
// Optional sign
829+
if l.ch == '+' || l.ch == '-' {
830+
l.readChar()
831+
}
832+
// Optional exponent digits (T-SQL allows just "2e" with no exponent)
833+
for isDigit(l.ch) {
834+
l.readChar()
835+
}
836+
}
820837
return Token{
821838
Type: TokenNumber,
822839
Literal: l.input[startPos:l.pos],
@@ -837,6 +854,72 @@ func isDigit(ch byte) bool {
837854
return ch >= '0' && ch <= '9'
838855
}
839856

857+
// isCurrencySymbol checks if current position is a currency symbol for money literals
858+
func (l *Lexer) isCurrencySymbol() bool {
859+
if l.ch == '$' {
860+
// Check if followed by digit, space+digit, or +/- then digit
861+
next := l.peekChar()
862+
if isDigit(next) || next == ' ' || next == '+' || next == '-' {
863+
return true
864+
}
865+
return false
866+
}
867+
// Check for Unicode currency symbols
868+
if l.ch >= 0x80 {
869+
r, _ := l.peekRune()
870+
// Common currency symbols: £ (U+00A3), ¤ (U+00A4), ¥ (U+00A5)
871+
// and various others in the Currency Symbols block
872+
if r == '£' || r == '¤' || r == '¥' || r == '৲' || r == '৳' ||
873+
r == '฿' || r == '₡' || r == '₢' || r == '₣' || r == '₤' ||
874+
r == '₦' || r == '₧' || r == '₨' || r == '₩' || r == '₪' || r == '₫' {
875+
return true
876+
}
877+
}
878+
return false
879+
}
880+
881+
// readMoneyLiteral reads a money literal starting with a currency symbol
882+
func (l *Lexer) readMoneyLiteral() Token {
883+
startPos := l.pos
884+
885+
// Read currency symbol (may be multi-byte)
886+
if l.ch >= 0x80 {
887+
_, size := l.peekRune()
888+
for i := 0; i < size; i++ {
889+
l.readChar()
890+
}
891+
} else {
892+
l.readChar() // consume $
893+
}
894+
895+
// Skip optional +/- after currency symbol
896+
if l.ch == '+' || l.ch == '-' {
897+
l.readChar()
898+
}
899+
900+
// Skip optional whitespace after currency symbol
901+
for l.ch == ' ' || l.ch == '\t' {
902+
l.readChar()
903+
}
904+
905+
// Read digits and decimal point
906+
for isDigit(l.ch) {
907+
l.readChar()
908+
}
909+
if l.ch == '.' {
910+
l.readChar()
911+
for isDigit(l.ch) {
912+
l.readChar()
913+
}
914+
}
915+
916+
return Token{
917+
Type: TokenMoney,
918+
Literal: l.input[startPos:l.pos],
919+
Pos: startPos,
920+
}
921+
}
922+
840923
var keywords = map[string]TokenType{
841924
"SELECT": TokenSelect,
842925
"FROM": TokenFrom,

parser/marshal.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2034,6 +2034,28 @@ func scalarExpressionToJSON(expr ast.ScalarExpression) jsonNode {
20342034
node["Value"] = e.Value
20352035
}
20362036
return node
2037+
case *ast.RealLiteral:
2038+
node := jsonNode{
2039+
"$type": "RealLiteral",
2040+
}
2041+
if e.LiteralType != "" {
2042+
node["LiteralType"] = e.LiteralType
2043+
}
2044+
if e.Value != "" {
2045+
node["Value"] = e.Value
2046+
}
2047+
return node
2048+
case *ast.MoneyLiteral:
2049+
node := jsonNode{
2050+
"$type": "MoneyLiteral",
2051+
}
2052+
if e.LiteralType != "" {
2053+
node["LiteralType"] = e.LiteralType
2054+
}
2055+
if e.Value != "" {
2056+
node["Value"] = e.Value
2057+
}
2058+
return node
20372059
case *ast.StringLiteral:
20382060
node := jsonNode{
20392061
"$type": "StringLiteral",

parser/parse_select.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1128,11 +1128,19 @@ func (p *Parser) parsePrimaryExpression() (ast.ScalarExpression, error) {
11281128
case TokenNumber:
11291129
val := p.curTok.Literal
11301130
p.nextToken()
1131+
// Check if it's scientific notation (real literal)
1132+
if strings.ContainsAny(val, "eE") {
1133+
return &ast.RealLiteral{LiteralType: "Real", Value: val}, nil
1134+
}
11311135
// Check if it's a decimal number
11321136
if strings.Contains(val, ".") {
11331137
return &ast.NumericLiteral{LiteralType: "Numeric", Value: val}, nil
11341138
}
11351139
return &ast.IntegerLiteral{LiteralType: "Integer", Value: val}, nil
1140+
case TokenMoney:
1141+
val := p.curTok.Literal
1142+
p.nextToken()
1143+
return &ast.MoneyLiteral{LiteralType: "Money", Value: val}, nil
11361144
case TokenBinary:
11371145
val := p.curTok.Literal
11381146
p.nextToken()

parser/parse_statements.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4686,6 +4686,19 @@ func (p *Parser) parseProcedureParameters() ([]*ast.ProcedureParameter, error) {
46864686
if err != nil {
46874687
return nil, err
46884688
}
4689+
// Convert single-identifier ColumnReferenceExpression to IdentifierLiteral
4690+
// (e.g., for default values like false, true, null)
4691+
if colRef, ok := val.(*ast.ColumnReferenceExpression); ok {
4692+
if colRef.MultiPartIdentifier != nil && colRef.MultiPartIdentifier.Count == 1 &&
4693+
len(colRef.MultiPartIdentifier.Identifiers) == 1 {
4694+
ident := colRef.MultiPartIdentifier.Identifiers[0]
4695+
val = &ast.IdentifierLiteral{
4696+
LiteralType: "Identifier",
4697+
QuoteType: ident.QuoteType,
4698+
Value: ident.Value,
4699+
}
4700+
}
4701+
}
46894702
param.Value = val
46904703
}
46914704

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{"todo": true}
1+
{}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{"todo": true}
1+
{}

0 commit comments

Comments
 (0)