@fadhilp/stateql 0.5.3 → 0.5.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/src/sql.js +158 -6
  2. package/package.json +1 -1
package/dist/src/sql.js CHANGED
@@ -23,7 +23,10 @@ export function analyzeSql(sql, driver) {
23
23
  : driver === "mysql"
24
24
  ? "MySQL"
25
25
  : "Sqlite";
26
- const parsed = parser.astify(trimmed, { database });
26
+ const parserSql = driver === "postgres"
27
+ ? postgresParserSql(trimmed)
28
+ : trimmed;
29
+ const parsed = parser.astify(parserSql, { database });
27
30
  if (Array.isArray(parsed)) {
28
31
  if (parsed.length !== 1) {
29
32
  throw new StateQLError("INVALID_SQL", "Exactly one SQL statement is required.");
@@ -40,11 +43,15 @@ export function analyzeSql(sql, driver) {
40
43
  if (statementType === "select" && selectContainsWrite(ast)) {
41
44
  throw new StateQLError("INVALID_SQL", "Read statements cannot contain writes or SELECT INTO.");
42
45
  }
43
- const normalized = parser
44
- .sqlify(ast, { database })
45
- .replace(/;\s*$/, "")
46
- .replace(/\s+/g, " ")
47
- .trim();
46
+ const normalized = parserSql === trimmed
47
+ ? parser
48
+ .sqlify(ast, { database })
49
+ .replace(/;\s*$/, "")
50
+ .replace(/\s+/g, " ")
51
+ .trim()
52
+ // Keep the exact ordering modifiers in cache and idempotency fingerprints.
53
+ // The parser copy is analysis-only; adapters execute the original SQL.
54
+ : trimmed.replace(/;\s*$/, "");
48
55
  const details = ast;
49
56
  const read = statementType === "select";
50
57
  const mutation = statementType === "update" ||
@@ -74,6 +81,151 @@ export function analyzeSql(sql, driver) {
74
81
  throw new StateQLError("INVALID_SQL", message);
75
82
  }
76
83
  }
84
+ function postgresParserSql(sql) {
85
+ const output = sql.split("");
86
+ const orderDepths = new Set();
87
+ let previousWord;
88
+ let changed = false;
89
+ let depth = 0;
90
+ let index = 0;
91
+ while (index < sql.length) {
92
+ if (sql.startsWith("--", index)) {
93
+ index = lineCommentEnd(sql, index + 2);
94
+ continue;
95
+ }
96
+ if (sql.startsWith("/*", index)) {
97
+ index = blockCommentEnd(sql, index + 2);
98
+ continue;
99
+ }
100
+ const character = sql[index];
101
+ if (character === "'" || character === '"') {
102
+ previousWord = undefined;
103
+ index = quotedEnd(sql, index + 1, character);
104
+ continue;
105
+ }
106
+ if (character === "$") {
107
+ const delimiter = sql.slice(index).match(/^\$(?:[A-Za-z_][A-Za-z0-9_]*)?\$/)?.[0];
108
+ if (delimiter) {
109
+ previousWord = undefined;
110
+ const end = sql.indexOf(delimiter, index + delimiter.length);
111
+ index = end < 0 ? sql.length : end + delimiter.length;
112
+ continue;
113
+ }
114
+ }
115
+ if (character === "(") {
116
+ previousWord = undefined;
117
+ depth += 1;
118
+ index += 1;
119
+ continue;
120
+ }
121
+ if (character === ")") {
122
+ previousWord = undefined;
123
+ orderDepths.delete(depth);
124
+ depth = Math.max(0, depth - 1);
125
+ index += 1;
126
+ continue;
127
+ }
128
+ if (!identifierStart(character)) {
129
+ if (!/\s/u.test(character))
130
+ previousWord = undefined;
131
+ if (character === ";")
132
+ orderDepths.clear();
133
+ index += 1;
134
+ continue;
135
+ }
136
+ let end = index + 1;
137
+ while (identifierPart(sql[end]))
138
+ end += 1;
139
+ const word = sql.slice(index, end).toUpperCase();
140
+ if (word === "NULLS" && orderDepths.has(depth)) {
141
+ const ordering = triviaEnd(sql, end);
142
+ const modifier = sql.slice(ordering, ordering + 5).toUpperCase();
143
+ const length = modifier === "FIRST" ? 5 : modifier.startsWith("LAST") ? 4 : 0;
144
+ if (length && !identifierPart(sql[ordering + length])) {
145
+ for (let cursor = index; cursor < end; cursor += 1)
146
+ output[cursor] = " ";
147
+ for (let cursor = ordering; cursor < ordering + length; cursor += 1)
148
+ output[cursor] = " ";
149
+ changed = true;
150
+ previousWord = undefined;
151
+ index = ordering + length;
152
+ continue;
153
+ }
154
+ }
155
+ if (word === "BY" && previousWord === "ORDER")
156
+ orderDepths.add(depth);
157
+ if (["LIMIT", "OFFSET", "FETCH", "FOR", "UNION", "INTERSECT", "EXCEPT"].includes(word)) {
158
+ orderDepths.delete(depth);
159
+ }
160
+ previousWord = word === "ORDER" ? word : undefined;
161
+ index = end;
162
+ }
163
+ return changed ? output.join("") : sql;
164
+ }
165
+ function triviaEnd(sql, start) {
166
+ let index = start;
167
+ while (index < sql.length) {
168
+ if (/\s/u.test(sql[index])) {
169
+ index += 1;
170
+ }
171
+ else if (sql.startsWith("--", index)) {
172
+ index = lineCommentEnd(sql, index + 2);
173
+ }
174
+ else if (sql.startsWith("/*", index)) {
175
+ index = blockCommentEnd(sql, index + 2);
176
+ }
177
+ else {
178
+ break;
179
+ }
180
+ }
181
+ return index;
182
+ }
183
+ function lineCommentEnd(sql, start) {
184
+ const end = sql.indexOf("\n", start);
185
+ return end < 0 ? sql.length : end + 1;
186
+ }
187
+ function blockCommentEnd(sql, start) {
188
+ let depth = 1;
189
+ let index = start;
190
+ while (index < sql.length && depth > 0) {
191
+ if (sql.startsWith("/*", index)) {
192
+ depth += 1;
193
+ index += 2;
194
+ }
195
+ else if (sql.startsWith("*/", index)) {
196
+ depth -= 1;
197
+ index += 2;
198
+ }
199
+ else {
200
+ index += 1;
201
+ }
202
+ }
203
+ return index;
204
+ }
205
+ function quotedEnd(sql, start, quote) {
206
+ let index = start;
207
+ while (index < sql.length) {
208
+ if (sql[index] === "\\" && quote === "'") {
209
+ index += 2;
210
+ }
211
+ else if (sql[index] !== quote) {
212
+ index += 1;
213
+ }
214
+ else if (sql[index + 1] === quote) {
215
+ index += 2;
216
+ }
217
+ else {
218
+ return index + 1;
219
+ }
220
+ }
221
+ return index;
222
+ }
223
+ function identifierStart(value) {
224
+ return value !== undefined && /[A-Za-z_\u0080-\uFFFF]/u.test(value);
225
+ }
226
+ function identifierPart(value) {
227
+ return value !== undefined && /[A-Za-z0-9_$\u0080-\uFFFF]/u.test(value);
228
+ }
77
229
  function selectContainsWrite(ast) {
78
230
  const details = ast;
79
231
  const into = details.into;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fadhilp/stateql",
3
- "version": "0.5.3",
3
+ "version": "0.5.4",
4
4
  "description": "Stateful, agent-oriented database CLI for safe result reuse",
5
5
  "repository": {
6
6
  "type": "git",