@atscript/db-sqlite 0.1.65 → 0.1.66

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.
package/dist/index.cjs CHANGED
@@ -64,24 +64,82 @@ function esc(name) {
64
64
  return name.replace(/"/g, "\"\"");
65
65
  }
66
66
  /**
67
- * Basic regex-to-LIKE conversion.
68
- * - `^abc` `abc%`
69
- * - `abc$` `%abc`
70
- * - `^abc$` → `abc`
71
- * - `abc` `%abc%`
67
+ * Regex characters whose backslash form means "literal char". The walker emits
68
+ * the unescaped char (with re-escaping for SQL LIKE wildcards). Anything outside
69
+ * this set `\d`, `\w`, `\s`, `\b`, etc. — is rejected as an unsupported feature.
70
+ */
71
+ const REGEX_LITERAL_ESCAPES = /* @__PURE__ */ new Set(".^$()[]{}|/+*?-\\");
72
+ /**
73
+ * Unescaped regex metacharacters with no LIKE equivalent. Subset of
74
+ * {@link REGEX_LITERAL_ESCAPES}: the same chars are rejected unescaped here
75
+ * but accepted as literals when preceded by `\`.
76
+ */
77
+ const REGEX_UNSUPPORTED = /* @__PURE__ */ new Set("()[]{}|*+?");
78
+ /**
79
+ * Re-escape a literal char for SQL LIKE under `ESCAPE '\'`. `%`, `_`, and `\`
80
+ * are LIKE metachars and need a backslash prefix; everything else passes through.
81
+ */
82
+ function likeEscape(ch) {
83
+ return ch === "%" || ch === "_" || ch === "\\" ? `\\${ch}` : ch;
84
+ }
85
+ /**
86
+ * Translates a regex pattern into a SQLite LIKE pattern. The dialect emits the
87
+ * resulting SQL with `ESCAPE '\'`, so `\%` and `\_` in the output denote literal
88
+ * `%` / `_`, and `\\` denotes a literal backslash.
89
+ *
90
+ * Supported subset:
91
+ * - anchors `^` and `$` (must appear at the very start / end of the pattern)
92
+ * - `.` (any single char) and `.*` (any run of chars)
93
+ * - escaped literals: `\.`, `\^`, `\$`, `\(`, `\)`, `\[`, `\]`, `\{`, `\}`,
94
+ * `\|`, `\/`, `\+`, `\*`, `\?`, `\-`, `\\`
95
+ *
96
+ * Throws on character classes, alternation, groups, quantifiers other than `.*`,
97
+ * and shorthand classes (`\d`, `\w`, `\s`, `\b`, …) — these would silently match
98
+ * the wrong rows under a naive translation, and a Node-side fallback would break
99
+ * pagination, ordering, and aggregation pushdown.
100
+ *
101
+ * `^` and `$` outside the start/end of the pattern are treated as literal chars
102
+ * (multiline anchors aren't supported).
72
103
  */
73
104
  function regexToLike(pattern) {
74
105
  const hasStart = pattern.startsWith("^");
75
- const hasEnd = pattern.endsWith("$");
76
- let core = pattern;
77
- if (hasStart) core = core.slice(1);
78
- if (hasEnd) core = core.slice(0, -1);
79
- core = core.replace(/%/g, "\\%").replace(/_/g, "\\_");
80
- core = core.replace(/\.\*/g, "%").replace(/\./g, "_");
81
- if (hasStart && hasEnd) return core;
82
- if (hasStart) return `${core}%`;
83
- if (hasEnd) return `%${core}`;
84
- return `%${core}%`;
106
+ const hasEnd = endsWithUnescapedDollar(pattern);
107
+ const start = hasStart ? 1 : 0;
108
+ const end = hasEnd ? pattern.length - 1 : pattern.length;
109
+ let out = "";
110
+ for (let i = start; i < end; i++) {
111
+ const c = pattern[i];
112
+ if (c === "\\") {
113
+ const next = pattern[i + 1];
114
+ if (next === void 0) throw new Error(`Trailing backslash in regex pattern: ${pattern}`);
115
+ if (!REGEX_LITERAL_ESCAPES.has(next)) throw new Error(`Unsupported regex escape '\\${next}' in pattern '${pattern}' — only literal-meaning escapes are supported by the SQLite LIKE translation`);
116
+ out += likeEscape(next);
117
+ i++;
118
+ continue;
119
+ }
120
+ if (c === ".") {
121
+ if (pattern[i + 1] === "*") {
122
+ out += "%";
123
+ i++;
124
+ } else out += "_";
125
+ continue;
126
+ }
127
+ if (REGEX_UNSUPPORTED.has(c)) throw new Error(`Unsupported regex feature '${c}' in pattern '${pattern}' — only anchors, '.', '.*', and escaped literals are supported by the SQLite LIKE translation`);
128
+ out += likeEscape(c);
129
+ }
130
+ if (hasStart && hasEnd) return out;
131
+ if (hasStart) return `${out}%`;
132
+ if (hasEnd) return `%${out}`;
133
+ return `%${out}%`;
134
+ }
135
+ /**
136
+ * `$` at the end of the pattern is the end-anchor only if it is not preceded by
137
+ * an odd number of backslashes (i.e. not escaped). `\$` and `\\\$` are literal,
138
+ * `$` and `\\$` are anchors.
139
+ */
140
+ function endsWithUnescapedDollar(s) {
141
+ const m = s.match(/(\\*)\$$/);
142
+ return m !== null && m[1].length % 2 === 0;
85
143
  }
86
144
  const sqliteDialect = {
87
145
  quoteIdentifier(name) {
@@ -100,7 +158,7 @@ const sqliteDialect = {
100
158
  const { pattern, flags } = (0, _atscript_db_sql_tools.parseRegexString)(value);
101
159
  const likePattern = regexToLike(pattern);
102
160
  return {
103
- sql: flags.includes("i") ? `${quotedCol} LIKE ? COLLATE NOCASE` : `${quotedCol} LIKE ?`,
161
+ sql: `${quotedCol} LIKE ? ESCAPE '\\'${flags.includes("i") ? " COLLATE NOCASE" : ""}`,
104
162
  params: [likePattern]
105
163
  };
106
164
  },
package/dist/index.mjs CHANGED
@@ -63,24 +63,82 @@ function esc(name) {
63
63
  return name.replace(/"/g, "\"\"");
64
64
  }
65
65
  /**
66
- * Basic regex-to-LIKE conversion.
67
- * - `^abc` `abc%`
68
- * - `abc$` `%abc`
69
- * - `^abc$` → `abc`
70
- * - `abc` `%abc%`
66
+ * Regex characters whose backslash form means "literal char". The walker emits
67
+ * the unescaped char (with re-escaping for SQL LIKE wildcards). Anything outside
68
+ * this set `\d`, `\w`, `\s`, `\b`, etc. — is rejected as an unsupported feature.
69
+ */
70
+ const REGEX_LITERAL_ESCAPES = /* @__PURE__ */ new Set(".^$()[]{}|/+*?-\\");
71
+ /**
72
+ * Unescaped regex metacharacters with no LIKE equivalent. Subset of
73
+ * {@link REGEX_LITERAL_ESCAPES}: the same chars are rejected unescaped here
74
+ * but accepted as literals when preceded by `\`.
75
+ */
76
+ const REGEX_UNSUPPORTED = /* @__PURE__ */ new Set("()[]{}|*+?");
77
+ /**
78
+ * Re-escape a literal char for SQL LIKE under `ESCAPE '\'`. `%`, `_`, and `\`
79
+ * are LIKE metachars and need a backslash prefix; everything else passes through.
80
+ */
81
+ function likeEscape(ch) {
82
+ return ch === "%" || ch === "_" || ch === "\\" ? `\\${ch}` : ch;
83
+ }
84
+ /**
85
+ * Translates a regex pattern into a SQLite LIKE pattern. The dialect emits the
86
+ * resulting SQL with `ESCAPE '\'`, so `\%` and `\_` in the output denote literal
87
+ * `%` / `_`, and `\\` denotes a literal backslash.
88
+ *
89
+ * Supported subset:
90
+ * - anchors `^` and `$` (must appear at the very start / end of the pattern)
91
+ * - `.` (any single char) and `.*` (any run of chars)
92
+ * - escaped literals: `\.`, `\^`, `\$`, `\(`, `\)`, `\[`, `\]`, `\{`, `\}`,
93
+ * `\|`, `\/`, `\+`, `\*`, `\?`, `\-`, `\\`
94
+ *
95
+ * Throws on character classes, alternation, groups, quantifiers other than `.*`,
96
+ * and shorthand classes (`\d`, `\w`, `\s`, `\b`, …) — these would silently match
97
+ * the wrong rows under a naive translation, and a Node-side fallback would break
98
+ * pagination, ordering, and aggregation pushdown.
99
+ *
100
+ * `^` and `$` outside the start/end of the pattern are treated as literal chars
101
+ * (multiline anchors aren't supported).
71
102
  */
72
103
  function regexToLike(pattern) {
73
104
  const hasStart = pattern.startsWith("^");
74
- const hasEnd = pattern.endsWith("$");
75
- let core = pattern;
76
- if (hasStart) core = core.slice(1);
77
- if (hasEnd) core = core.slice(0, -1);
78
- core = core.replace(/%/g, "\\%").replace(/_/g, "\\_");
79
- core = core.replace(/\.\*/g, "%").replace(/\./g, "_");
80
- if (hasStart && hasEnd) return core;
81
- if (hasStart) return `${core}%`;
82
- if (hasEnd) return `%${core}`;
83
- return `%${core}%`;
105
+ const hasEnd = endsWithUnescapedDollar(pattern);
106
+ const start = hasStart ? 1 : 0;
107
+ const end = hasEnd ? pattern.length - 1 : pattern.length;
108
+ let out = "";
109
+ for (let i = start; i < end; i++) {
110
+ const c = pattern[i];
111
+ if (c === "\\") {
112
+ const next = pattern[i + 1];
113
+ if (next === void 0) throw new Error(`Trailing backslash in regex pattern: ${pattern}`);
114
+ if (!REGEX_LITERAL_ESCAPES.has(next)) throw new Error(`Unsupported regex escape '\\${next}' in pattern '${pattern}' — only literal-meaning escapes are supported by the SQLite LIKE translation`);
115
+ out += likeEscape(next);
116
+ i++;
117
+ continue;
118
+ }
119
+ if (c === ".") {
120
+ if (pattern[i + 1] === "*") {
121
+ out += "%";
122
+ i++;
123
+ } else out += "_";
124
+ continue;
125
+ }
126
+ if (REGEX_UNSUPPORTED.has(c)) throw new Error(`Unsupported regex feature '${c}' in pattern '${pattern}' — only anchors, '.', '.*', and escaped literals are supported by the SQLite LIKE translation`);
127
+ out += likeEscape(c);
128
+ }
129
+ if (hasStart && hasEnd) return out;
130
+ if (hasStart) return `${out}%`;
131
+ if (hasEnd) return `%${out}`;
132
+ return `%${out}%`;
133
+ }
134
+ /**
135
+ * `$` at the end of the pattern is the end-anchor only if it is not preceded by
136
+ * an odd number of backslashes (i.e. not escaped). `\$` and `\\\$` are literal,
137
+ * `$` and `\\$` are anchors.
138
+ */
139
+ function endsWithUnescapedDollar(s) {
140
+ const m = s.match(/(\\*)\$$/);
141
+ return m !== null && m[1].length % 2 === 0;
84
142
  }
85
143
  const sqliteDialect = {
86
144
  quoteIdentifier(name) {
@@ -99,7 +157,7 @@ const sqliteDialect = {
99
157
  const { pattern, flags } = parseRegexString(value);
100
158
  const likePattern = regexToLike(pattern);
101
159
  return {
102
- sql: flags.includes("i") ? `${quotedCol} LIKE ? COLLATE NOCASE` : `${quotedCol} LIKE ?`,
160
+ sql: `${quotedCol} LIKE ? ESCAPE '\\'${flags.includes("i") ? " COLLATE NOCASE" : ""}`,
103
161
  params: [likePattern]
104
162
  };
105
163
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atscript/db-sqlite",
3
- "version": "0.1.65",
3
+ "version": "0.1.66",
4
4
  "description": "SQLite adapter for @atscript/db with swappable driver support.",
5
5
  "keywords": [
6
6
  "atscript",
@@ -48,8 +48,8 @@
48
48
  "@atscript/typescript": "^0.1.50",
49
49
  "@uniqu/core": "^0.1.6",
50
50
  "better-sqlite3": ">=11.0.0",
51
- "@atscript/db": "^0.1.65",
52
- "@atscript/db-sql-tools": "^0.1.65"
51
+ "@atscript/db": "^0.1.66",
52
+ "@atscript/db-sql-tools": "^0.1.66"
53
53
  },
54
54
  "peerDependenciesMeta": {
55
55
  "better-sqlite3": {