-
Notifications
You must be signed in to change notification settings - Fork 10
Add support for MySQL DELIMITER directive #91
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 1 commit
8df7518
95b23fa
67783ff
f50a6c3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -72,11 +72,12 @@ const KEYWORDS = [ | |
| 'TRIGGERS', | ||
| 'VARIABLES', | ||
| 'WARNINGS', | ||
| 'DELIMITER', | ||
| ]; | ||
|
|
||
| const INDIVIDUALS: Record<string, Token['type']> = { | ||
| ';': 'semicolon', | ||
| }; | ||
| // The semicolon token is now emitted by the delimiter-match path in | ||
| // scanToken, so it can handle arbitrary terminators like '$$' or '//'. | ||
| const INDIVIDUALS: Record<string, Token['type']> = {}; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not sure worth keeping this around given it does nothing, and not sure what other characters might be added to this. Easy enough to re-introduce if we do come up with a test case for it.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Removed in f50a6c3. The empty Generated by Claude Code |
||
|
|
||
| const ENDTOKENS: Record<string, Char> = { | ||
| '"': '"', | ||
|
|
@@ -89,6 +90,7 @@ export function scanToken( | |
| state: State, | ||
| dialect: Dialect = 'generic', | ||
| paramTypes: ParamTypes = { positional: true }, | ||
| delimiter = ';', | ||
| ): Token { | ||
| const ch = read(state); | ||
|
|
||
|
|
@@ -112,14 +114,25 @@ export function scanToken( | |
| return scanParameter(state, dialect, paramTypes); | ||
| } | ||
|
|
||
| if (isDollarQuotedString(state)) { | ||
| // MySQL/MariaDB does not support dollar-quoted strings, and treating `$$` | ||
| // as one would conflict with its use as a custom DELIMITER terminator. | ||
| if (dialect !== 'mysql' && isDollarQuotedString(state)) { | ||
| return scanDollarQuotedString(state); | ||
| } | ||
|
|
||
| if (isQuotedIdentifier(ch, dialect) && ch !== null) { | ||
| return scanQuotedIdentifier(state, ENDTOKENS[ch]); | ||
| } | ||
|
|
||
| // Match the current statement terminator. Handles ';', '$', '$$', '//', etc. | ||
| // Runs before scanIndividualCharacter so it's the single source of | ||
| // terminator tokens. Word-like delimiters would be consumed by scanWord | ||
| // above, so only symbol delimiters are fully supported. | ||
| const delimiterToken = scanDelimiter(state, delimiter); | ||
| if (delimiterToken) { | ||
| return delimiterToken; | ||
| } | ||
|
|
||
| if (isLetter(ch)) { | ||
| return scanWord(state); | ||
| } | ||
|
|
@@ -132,6 +145,24 @@ export function scanToken( | |
| return skipChar(state); | ||
| } | ||
|
|
||
| function scanDelimiter(state: State, delimiter: string): Token | null { | ||
| if (!delimiter) { | ||
| return null; | ||
| } | ||
| if (state.input.slice(state.start, state.start + delimiter.length) !== delimiter) { | ||
| return null; | ||
| } | ||
| for (let i = 0; i < delimiter.length - 1; i++) { | ||
| read(state); | ||
| } | ||
| return { | ||
| type: 'semicolon', | ||
| value: delimiter, | ||
| start: state.start, | ||
| end: state.start + delimiter.length - 1, | ||
| }; | ||
| } | ||
|
|
||
| function read(state: State, skip = 0): Char { | ||
| if (state.position + skip === state.input.length - 1) { | ||
| return null; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,6 +3,76 @@ import { expect } from 'chai'; | |
| import { identify } from '../../src'; | ||
|
|
||
| describe('identifier', () => { | ||
| describe('MySQL DELIMITER directive', () => { | ||
| it('should identify the canonical example from issue #66', () => { | ||
| const actual = identify('\nSELECT 1;\n\nDELIMITER $\n\nSELECT 2$\n\nSELECT 3$\n', { | ||
| dialect: 'mysql', | ||
| }); | ||
| expect(actual).to.eql([ | ||
| { | ||
| start: 1, | ||
| end: 9, | ||
| text: 'SELECT 1;', | ||
| type: 'SELECT', | ||
| executionType: 'LISTING', | ||
| parameters: [], | ||
| tables: [], | ||
| columns: [], | ||
| }, | ||
| { | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Probably the
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done. Generated by Claude Code |
||
| start: 12, | ||
| end: 22, | ||
| text: 'DELIMITER $', | ||
| type: 'DELIMITER', | ||
| executionType: 'MODIFICATION', | ||
| parameters: [], | ||
| tables: [], | ||
| columns: [], | ||
| }, | ||
| { | ||
| start: 25, | ||
| end: 33, | ||
| text: 'SELECT 2$', | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How is a user of the library meant to know that Maybe we should be providing statements either without the terminator (;, $), or include an optional extra field specifying the terminating character. Probably the latter and we can introduce it as a new field, rather than changing the format of the text field, unless we feel like the text change is better?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added an Consumers can now do Documented in the README API section and with a worked example in the new "Working with MySQL DELIMITER" section. Generated by Claude Code |
||
| type: 'SELECT', | ||
| executionType: 'LISTING', | ||
| parameters: [], | ||
| tables: [], | ||
| columns: [], | ||
| }, | ||
| { | ||
| start: 36, | ||
| end: 44, | ||
| text: 'SELECT 3$', | ||
| type: 'SELECT', | ||
| executionType: 'LISTING', | ||
| parameters: [], | ||
| tables: [], | ||
| columns: [], | ||
| }, | ||
| ]); | ||
| }); | ||
|
|
||
| it('should split a CREATE PROCEDURE body with $$ delimiter', () => { | ||
| const input = | ||
| 'DELIMITER $$\nCREATE PROCEDURE foo()\nBEGIN\n SELECT 1;\n SELECT 2;\nEND$$\nDELIMITER ;'; | ||
| const actual = identify(input, { dialect: 'mysql' }); | ||
|
|
||
| expect(actual).to.have.lengthOf(3); | ||
| expect(actual[0]).to.include({ type: 'DELIMITER', text: 'DELIMITER $$' }); | ||
| expect(actual[1]).to.include({ | ||
| type: 'CREATE_PROCEDURE', | ||
| text: 'CREATE PROCEDURE foo()\nBEGIN\n SELECT 1;\n SELECT 2;\nEND$$', | ||
| }); | ||
| expect(actual[2]).to.include({ type: 'DELIMITER', text: 'DELIMITER ;' }); | ||
| }); | ||
|
|
||
| it('should strip matching surrounding quotes from the delimiter value', () => { | ||
| const actual = identify('DELIMITER "//"\nSELECT 1//', { dialect: 'mysql' }); | ||
| expect(actual.map((stmt) => stmt.type)).to.eql(['DELIMITER', 'SELECT']); | ||
| expect(actual[1].text).to.eql('SELECT 1//'); | ||
| }); | ||
| }); | ||
|
|
||
| describe('given queries with multiple statements', () => { | ||
| it('should identify a query with different statements in a single line', () => { | ||
| const actual = identify( | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -333,7 +333,7 @@ describe('getExecutionType', () => { | |
| expect(getExecutionType('SELECT')).to.equal('LISTING'); | ||
| }); | ||
|
|
||
| ['UPDATE', 'DELETE', 'INSERT', 'TRUNCATE'].forEach((type) => { | ||
| ['UPDATE', 'DELETE', 'INSERT', 'TRUNCATE', 'DELIMITER'].forEach((type) => { | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. DELIMITER isn't really a Suggest a better category. Add a new one if we need one. Why?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good point. Introduced a new
Generated by Claude Code |
||
| it(`should return MODIFICATION for ${type}`, () => { | ||
| expect(getExecutionType(type)).to.equal('MODIFICATION'); | ||
| }); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add an example to the readme of identifying a set of queries after a DELIMITER change and how the user should use the information provided by the library to interpret each query.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added a "Working with MySQL DELIMITER" section to the README. It shows:
CREATE PROCEDUREand a reset to;.identify()output withnewDelimiter/endStatementpopulated on the relevant statements.type === 'DELIMITER'(client-side only,NO_OP) and stripendStatementfrom each remaining statement'stextbefore sending it to the server.Also added an "Each returned statement has…" bullet list in the API section documenting every field, including the new
endStatementandnewDelimiter.Generated by Claude Code