@memberjunction/sql-dialect 0.0.1 → 5.5.0
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/README.md +388 -28
- package/dist/dataTypeMap.d.ts +34 -0
- package/dist/dataTypeMap.d.ts.map +1 -0
- package/dist/dataTypeMap.js +2 -0
- package/dist/dataTypeMap.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/postgresqlDialect.d.ts +43 -0
- package/dist/postgresqlDialect.d.ts.map +1 -0
- package/dist/postgresqlDialect.js +392 -0
- package/dist/postgresqlDialect.js.map +1 -0
- package/dist/sqlDialect.d.ts +267 -0
- package/dist/sqlDialect.d.ts.map +1 -0
- package/dist/sqlDialect.js +37 -0
- package/dist/sqlDialect.js.map +1 -0
- package/dist/sqlServerDialect.d.ts +42 -0
- package/dist/sqlServerDialect.d.ts.map +1 -0
- package/dist/sqlServerDialect.js +329 -0
- package/dist/sqlServerDialect.js.map +1 -0
- package/package.json +25 -7
package/README.md
CHANGED
|
@@ -1,45 +1,405 @@
|
|
|
1
1
|
# @memberjunction/sql-dialect
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**Version**: 5.2.0
|
|
4
|
+
**Zero runtime dependencies**
|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
## Overview
|
|
6
7
|
|
|
7
|
-
|
|
8
|
+
`@memberjunction/sql-dialect` is an abstract SQL dialect layer that enables database-agnostic SQL generation across MemberJunction. It encapsulates every platform-specific SQL syntax pattern -- identifier quoting, pagination, data types, DDL generation, full-text search, and more -- into a single, testable abstraction with zero database driver dependencies.
|
|
8
9
|
|
|
9
|
-
|
|
10
|
+
This package is used by CodeGen, data providers, and SQL converters throughout the MemberJunction monorepo. When code needs to emit SQL that works on both SQL Server and PostgreSQL, it programs against the `SQLDialect` abstract class and lets the concrete dialect handle platform differences.
|
|
10
11
|
|
|
11
|
-
|
|
12
|
-
1. Configure OIDC trusted publishing for the package name `@memberjunction/sql-dialect`
|
|
13
|
-
2. Enable secure, token-less publishing from CI/CD workflows
|
|
14
|
-
3. Establish provenance for packages published under this name
|
|
12
|
+
## Architecture
|
|
15
13
|
|
|
16
|
-
|
|
14
|
+
### Class Hierarchy
|
|
17
15
|
|
|
18
|
-
|
|
16
|
+
```
|
|
17
|
+
SQLDialect (abstract base)
|
|
18
|
+
|-- SQLServerDialect
|
|
19
|
+
|-- PostgreSQLDialect
|
|
20
|
+
```
|
|
19
21
|
|
|
20
|
-
|
|
22
|
+
`SQLDialect` defines approximately 30 abstract methods spanning identifier quoting, pagination, literal expressions, INSERT/UPDATE return patterns, DDL generation, full-text search, data type mapping, and schema introspection. Each concrete dialect implements every method with platform-native SQL.
|
|
21
23
|
|
|
22
|
-
|
|
24
|
+
### Key Interfaces
|
|
23
25
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
| Interface | Purpose |
|
|
27
|
+
|---|---|
|
|
28
|
+
| `LimitClauseResult` | `{ prefix: string; suffix: string }` -- Flexible pagination fragments. SQL Server uses `prefix` (TOP), PostgreSQL uses `suffix` (LIMIT/OFFSET). |
|
|
29
|
+
| `SchemaIntrospectionSQL` | Catalog query templates for discovering tables, columns, constraints, foreign keys, and indexes. |
|
|
30
|
+
| `TriggerOptions` | Configuration for trigger DDL generation (schema, table, timing, events, body, function name, FOR EACH ROW/STATEMENT). |
|
|
31
|
+
| `IndexOptions` | Configuration for index DDL generation (columns, uniqueness, method, partial WHERE, INCLUDE columns). |
|
|
32
|
+
| `DataTypeMap` | Maps source database types to target platform types. |
|
|
33
|
+
| `MappedType` | Describes a mapped type: `typeName`, `supportsLength`, `supportsPrecisionScale`, `defaultLength`. |
|
|
34
|
+
| `DatabasePlatform` | Union type: `'sqlserver' \| 'postgresql'` |
|
|
28
35
|
|
|
29
|
-
##
|
|
36
|
+
## Key Methods
|
|
30
37
|
|
|
31
|
-
|
|
32
|
-
- Contains no executable code
|
|
33
|
-
- Provides no functionality
|
|
34
|
-
- Should not be installed as a dependency
|
|
35
|
-
- Exists only for administrative purposes
|
|
38
|
+
### Identifier Quoting
|
|
36
39
|
|
|
37
|
-
|
|
40
|
+
| Method | Description | SQL Server | PostgreSQL |
|
|
41
|
+
|---|---|---|---|
|
|
42
|
+
| `QuoteIdentifier(name)` | Wraps a single identifier | `[name]` | `"name"` |
|
|
43
|
+
| `QuoteSchema(schema, object)` | Schema-qualified reference | `[schema].[object]` | `schema."object"` |
|
|
38
44
|
|
|
39
|
-
|
|
40
|
-
- [npm Trusted Publishing Documentation](https://docs.npmjs.com/generating-provenance-statements)
|
|
41
|
-
- [GitHub Actions OIDC Documentation](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect)
|
|
45
|
+
### Pagination
|
|
42
46
|
|
|
43
|
-
|
|
47
|
+
| Method | Description | SQL Server | PostgreSQL |
|
|
48
|
+
|---|---|---|---|
|
|
49
|
+
| `LimitClause(limit, offset?)` | Returns `{ prefix, suffix }` | Without offset: `prefix: 'TOP 10'`. With offset: `suffix: 'OFFSET 20 ROWS FETCH NEXT 10 ROWS ONLY'` | `suffix: 'LIMIT 10 OFFSET 20'` |
|
|
44
50
|
|
|
45
|
-
|
|
51
|
+
### Literals and Expressions
|
|
52
|
+
|
|
53
|
+
| Method | Description | SQL Server | PostgreSQL |
|
|
54
|
+
|---|---|---|---|
|
|
55
|
+
| `BooleanLiteral(value)` | Platform boolean | `1` / `0` | `true` / `false` |
|
|
56
|
+
| `CurrentTimestampUTC()` | Current UTC time | `GETUTCDATE()` | `(NOW() AT TIME ZONE 'UTC')` |
|
|
57
|
+
| `NewUUID()` | Generate UUID | `NEWID()` | `gen_random_uuid()` |
|
|
58
|
+
| `CastToText(expr)` | Cast to text type | `CAST(expr AS NVARCHAR(MAX))` | `CAST(expr AS TEXT)` |
|
|
59
|
+
| `CastToUUID(expr)` | Cast to UUID type | `CAST(expr AS UNIQUEIDENTIFIER)` | `CAST(expr AS UUID)` |
|
|
60
|
+
| `Coalesce(expr, fallback)` | Null coalescing (concrete) | `COALESCE(expr, fallback)` | `COALESCE(expr, fallback)` |
|
|
61
|
+
| `IsNull(expr, fallback)` | Alias for Coalesce | `COALESCE(expr, fallback)` | `COALESCE(expr, fallback)` |
|
|
62
|
+
| `IIF(condition, trueVal, falseVal)` | Conditional expression | `IIF(cond, t, f)` | `CASE WHEN cond THEN t ELSE f END` |
|
|
63
|
+
|
|
64
|
+
### INSERT/UPDATE Return Patterns
|
|
65
|
+
|
|
66
|
+
| Method | Description | SQL Server | PostgreSQL |
|
|
67
|
+
|---|---|---|---|
|
|
68
|
+
| `ReturnInsertedClause(columns?)` | Get inserted values back | `OUTPUT INSERTED.*` or `OUTPUT INSERTED.[col]` | `RETURNING *` or `RETURNING "col"` |
|
|
69
|
+
| `AutoIncrementPKExpression()` | Auto-increment DDL | `IDENTITY(1,1)` | `GENERATED ALWAYS AS IDENTITY` |
|
|
70
|
+
| `UUIDPKDefault()` | Default UUID PK expression | `NEWSEQUENTIALID()` | `gen_random_uuid()` |
|
|
71
|
+
| `ScopeIdentityExpression()` | Last inserted identity | `SCOPE_IDENTITY()` | `lastval()` |
|
|
72
|
+
| `RowCountExpression()` | Rows affected | `@@ROWCOUNT` | `ROW_COUNT` (via GET DIAGNOSTICS) |
|
|
73
|
+
|
|
74
|
+
### DDL Generation
|
|
75
|
+
|
|
76
|
+
| Method | Description |
|
|
77
|
+
|---|---|
|
|
78
|
+
| `TriggerDDL(options: TriggerOptions)` | Full trigger creation DDL. SQL Server emits `CREATE TRIGGER ... AS BEGIN ... END`. PostgreSQL emits a companion `CREATE OR REPLACE FUNCTION` plus `CREATE TRIGGER ... EXECUTE FUNCTION`. |
|
|
79
|
+
| `IndexDDL(options: IndexOptions)` | Index creation DDL. PostgreSQL supports `USING method`, partial `WHERE`, and `IF NOT EXISTS`. SQL Server supports `INCLUDE` columns. |
|
|
80
|
+
| `ExistenceCheckSQL(objectType, schema, name)` | Check if a database object exists. SQL Server uses `OBJECT_ID()`. PostgreSQL uses `pg_catalog` queries. Supports TABLE, VIEW, FUNCTION, PROCEDURE, TRIGGER. |
|
|
81
|
+
| `CreateOrReplaceSupported(objectType)` | Whether `CREATE OR REPLACE` is available. SQL Server: always `false`. PostgreSQL: `true` for FUNCTION, VIEW, PROCEDURE. |
|
|
82
|
+
| `BatchSeparator()` | Statement batch separator. SQL Server: `GO`. PostgreSQL: `""` (empty string). |
|
|
83
|
+
|
|
84
|
+
### Full-Text Search
|
|
85
|
+
|
|
86
|
+
| Method | Description | SQL Server | PostgreSQL |
|
|
87
|
+
|---|---|---|---|
|
|
88
|
+
| `FullTextSearchPredicate(column, searchTerm)` | Search predicate | `CONTAINS([column], term)` | `column @@ plainto_tsquery('english', term)` |
|
|
89
|
+
| `FullTextIndexDDL(table, columns, catalog?)` | Index creation DDL | Fulltext catalog + fulltext index | tsvector column + GIN index + update trigger |
|
|
90
|
+
|
|
91
|
+
### String and JSON Functions
|
|
92
|
+
|
|
93
|
+
| Method | Description | SQL Server | PostgreSQL |
|
|
94
|
+
|---|---|---|---|
|
|
95
|
+
| `StringSplitFunction(value, delimiter)` | Split string to rows | `STRING_SPLIT(value, delim)` | `unnest(string_to_array(value, delim))` |
|
|
96
|
+
| `JsonExtract(column, path)` | Extract JSON value | `JSON_VALUE(column, 'path')` | `column->>'path'` |
|
|
97
|
+
| `ConcatOperator()` | String concatenation | `+` | `\|\|` |
|
|
98
|
+
|
|
99
|
+
### Parameters and Procedure Calls
|
|
100
|
+
|
|
101
|
+
| Method | Description | SQL Server | PostgreSQL |
|
|
102
|
+
|---|---|---|---|
|
|
103
|
+
| `ParameterPlaceholder(index)` | Positional parameter | `@p0`, `@p1`, ... | `$1`, `$2`, ... |
|
|
104
|
+
| `ProcedureCallSyntax(schema, name, params)` | Call stored procedure/function | `EXEC [schema].[name] @p0, @p1` | `SELECT * FROM schema."name"($1, $2)` |
|
|
105
|
+
|
|
106
|
+
### CTE / Recursion
|
|
107
|
+
|
|
108
|
+
| Method | Description | SQL Server | PostgreSQL |
|
|
109
|
+
|---|---|---|---|
|
|
110
|
+
| `RecursiveCTESyntax()` | Recursive CTE keyword | `WITH` | `WITH RECURSIVE` |
|
|
111
|
+
|
|
112
|
+
### Permissions and Comments
|
|
113
|
+
|
|
114
|
+
| Method | Description | SQL Server | PostgreSQL |
|
|
115
|
+
|---|---|---|---|
|
|
116
|
+
| `GrantPermission(permission, objectType, schema, object, role)` | Grant access | `GRANT ... ON [s].[o] TO [r]` | `GRANT ... ON s."o" TO "r"` |
|
|
117
|
+
| `CommentOnObject(objectType, schema, name, comment)` | Add description | `EXEC sp_addextendedproperty ...` | `COMMENT ON TYPE s."name" IS '...'` |
|
|
118
|
+
|
|
119
|
+
### Schema Introspection
|
|
120
|
+
|
|
121
|
+
| Method | Description |
|
|
122
|
+
|---|---|
|
|
123
|
+
| `SchemaIntrospectionQueries()` | Returns a `SchemaIntrospectionSQL` object with platform-specific catalog queries for listing tables, columns, constraints, foreign keys, indexes, and checking object existence. |
|
|
124
|
+
|
|
125
|
+
### Data Type Mapping
|
|
126
|
+
|
|
127
|
+
| Method | Description |
|
|
128
|
+
|---|---|
|
|
129
|
+
| `get TypeMap(): DataTypeMap` | Returns the dialect-specific type mapper instance. |
|
|
130
|
+
| `MapDataType(sourceType, length?, precision?, scale?)` | Convenience wrapper that calls `TypeMap.MapType()`. Returns a `MappedType`. |
|
|
131
|
+
| `MapDataTypeToString(sourceType, length?, precision?, scale?)` | Convenience wrapper that calls `TypeMap.MapTypeToString()`. Returns a formatted type string like `VARCHAR(255)` or `NUMERIC(10,2)`. |
|
|
132
|
+
|
|
133
|
+
## DataTypeMap
|
|
134
|
+
|
|
135
|
+
The `DataTypeMap` interface defines how data types are translated between database platforms:
|
|
136
|
+
|
|
137
|
+
```typescript
|
|
138
|
+
interface MappedType {
|
|
139
|
+
typeName: string; // Target type name (e.g., "UUID", "BOOLEAN")
|
|
140
|
+
supportsLength: boolean; // Whether the type accepts a length parameter
|
|
141
|
+
supportsPrecisionScale: boolean; // Whether the type accepts precision/scale
|
|
142
|
+
defaultLength?: number; // Default length when applicable
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
interface DataTypeMap {
|
|
146
|
+
MapType(sourceType: string, sourceLength?: number,
|
|
147
|
+
sourcePrecision?: number, sourceScale?: number): MappedType;
|
|
148
|
+
|
|
149
|
+
MapTypeToString(sourceType: string, sourceLength?: number,
|
|
150
|
+
sourcePrecision?: number, sourceScale?: number): string;
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
`SQLServerDataTypeMap` is an identity mapper (SQL Server types map to themselves). `PostgreSQLDataTypeMap` maps SQL Server types to their PostgreSQL equivalents.
|
|
155
|
+
|
|
156
|
+
### Type Mapping Reference (SQL Server to PostgreSQL)
|
|
157
|
+
|
|
158
|
+
| SQL Server Type | PostgreSQL Type | Notes |
|
|
159
|
+
|---|---|---|
|
|
160
|
+
| `UNIQUEIDENTIFIER` | `UUID` | |
|
|
161
|
+
| `BIT` | `BOOLEAN` | |
|
|
162
|
+
| `NVARCHAR(n)` | `VARCHAR(n)` | |
|
|
163
|
+
| `NVARCHAR(MAX)` | `TEXT` | Length = -1 or unspecified |
|
|
164
|
+
| `VARCHAR(MAX)` | `TEXT` | Length = -1 or unspecified |
|
|
165
|
+
| `NCHAR` / `CHAR` | `CHAR` | Preserves length |
|
|
166
|
+
| `INT` / `INTEGER` | `INTEGER` | |
|
|
167
|
+
| `BIGINT` | `BIGINT` | |
|
|
168
|
+
| `SMALLINT` | `SMALLINT` | |
|
|
169
|
+
| `TINYINT` | `SMALLINT` | No TINYINT in PostgreSQL |
|
|
170
|
+
| `DECIMAL` / `NUMERIC` | `NUMERIC` | Preserves precision/scale |
|
|
171
|
+
| `FLOAT(1-24)` | `REAL` | |
|
|
172
|
+
| `FLOAT(25-53)` | `DOUBLE PRECISION` | |
|
|
173
|
+
| `REAL` | `REAL` | |
|
|
174
|
+
| `MONEY` | `NUMERIC(19,4)` | |
|
|
175
|
+
| `SMALLMONEY` | `NUMERIC(10,4)` | |
|
|
176
|
+
| `DATE` | `DATE` | |
|
|
177
|
+
| `DATETIME` / `DATETIME2` | `TIMESTAMP` | |
|
|
178
|
+
| `DATETIMEOFFSET` | `TIMESTAMPTZ` | |
|
|
179
|
+
| `SMALLDATETIME` | `TIMESTAMP(0)` | |
|
|
180
|
+
| `TIME` | `TIME` | |
|
|
181
|
+
| `TEXT` / `NTEXT` | `TEXT` | |
|
|
182
|
+
| `IMAGE` | `BYTEA` | |
|
|
183
|
+
| `VARBINARY` / `BINARY` | `BYTEA` | |
|
|
184
|
+
| `XML` | `XML` | |
|
|
185
|
+
|
|
186
|
+
PostgreSQL-native types (`UUID`, `BOOLEAN`, `TIMESTAMPTZ`, `JSONB`, `BYTEA`, `SERIAL`, `BIGSERIAL`, `DOUBLE PRECISION`) pass through unchanged.
|
|
187
|
+
|
|
188
|
+
## Usage Examples
|
|
189
|
+
|
|
190
|
+
```typescript
|
|
191
|
+
import {
|
|
192
|
+
SQLDialect,
|
|
193
|
+
SQLServerDialect,
|
|
194
|
+
PostgreSQLDialect
|
|
195
|
+
} from '@memberjunction/sql-dialect';
|
|
196
|
+
|
|
197
|
+
// Create a dialect instance
|
|
198
|
+
const dialect: SQLDialect = new PostgreSQLDialect();
|
|
199
|
+
|
|
200
|
+
// Identifier quoting
|
|
201
|
+
dialect.QuoteIdentifier('UserName'); // "UserName"
|
|
202
|
+
dialect.QuoteSchema('__mj', 'User'); // __mj."User"
|
|
203
|
+
|
|
204
|
+
// Pagination
|
|
205
|
+
const { prefix, suffix } = dialect.LimitClause(10, 20);
|
|
206
|
+
// prefix: '', suffix: 'LIMIT 10 OFFSET 20'
|
|
207
|
+
|
|
208
|
+
// Boolean and timestamp literals
|
|
209
|
+
dialect.BooleanLiteral(true); // 'true'
|
|
210
|
+
dialect.CurrentTimestampUTC(); // "(NOW() AT TIME ZONE 'UTC')"
|
|
211
|
+
|
|
212
|
+
// UUID generation
|
|
213
|
+
dialect.NewUUID(); // 'gen_random_uuid()'
|
|
214
|
+
|
|
215
|
+
// Data type mapping
|
|
216
|
+
const mapped = dialect.MapDataType('UNIQUEIDENTIFIER');
|
|
217
|
+
// { typeName: 'UUID', supportsLength: false, supportsPrecisionScale: false }
|
|
218
|
+
|
|
219
|
+
dialect.MapDataTypeToString('NVARCHAR', 255);
|
|
220
|
+
// 'VARCHAR(255)'
|
|
221
|
+
|
|
222
|
+
dialect.MapDataTypeToString('NVARCHAR', -1);
|
|
223
|
+
// 'TEXT'
|
|
224
|
+
|
|
225
|
+
dialect.MapDataTypeToString('DECIMAL', undefined, 10, 2);
|
|
226
|
+
// 'NUMERIC(10,2)'
|
|
227
|
+
|
|
228
|
+
// INSERT return clause
|
|
229
|
+
dialect.ReturnInsertedClause(); // 'RETURNING *'
|
|
230
|
+
dialect.ReturnInsertedClause(['ID', 'Name']);
|
|
231
|
+
// 'RETURNING "ID", "Name"'
|
|
232
|
+
|
|
233
|
+
// Conditional expression
|
|
234
|
+
dialect.IIF('x > 0', "'positive'", "'non-positive'");
|
|
235
|
+
// "CASE WHEN x > 0 THEN 'positive' ELSE 'non-positive' END"
|
|
236
|
+
|
|
237
|
+
// Procedure call
|
|
238
|
+
dialect.ProcedureCallSyntax('__mj', 'spCreateUser', ['$1', '$2']);
|
|
239
|
+
// 'SELECT * FROM __mj."spCreateUser"($1, $2)'
|
|
240
|
+
|
|
241
|
+
// Trigger DDL
|
|
242
|
+
const triggerSQL = dialect.TriggerDDL({
|
|
243
|
+
schema: '__mj',
|
|
244
|
+
tableName: 'User',
|
|
245
|
+
triggerName: 'trgUpdateUser',
|
|
246
|
+
timing: 'BEFORE',
|
|
247
|
+
events: ['UPDATE'],
|
|
248
|
+
body: 'NEW.__mj_UpdatedAt = NOW();',
|
|
249
|
+
functionName: 'fn_update_user_timestamp',
|
|
250
|
+
forEach: 'ROW'
|
|
251
|
+
});
|
|
252
|
+
// Generates:
|
|
253
|
+
// CREATE OR REPLACE FUNCTION __mj."fn_update_user_timestamp"()
|
|
254
|
+
// RETURNS TRIGGER AS $$ BEGIN ... END; $$ LANGUAGE plpgsql;
|
|
255
|
+
// DROP TRIGGER IF EXISTS ... ;
|
|
256
|
+
// CREATE TRIGGER "trgUpdateUser" BEFORE UPDATE ON __mj."User"
|
|
257
|
+
// FOR EACH ROW EXECUTE FUNCTION __mj."fn_update_user_timestamp"();
|
|
258
|
+
|
|
259
|
+
// Index DDL
|
|
260
|
+
const indexSQL = dialect.IndexDDL({
|
|
261
|
+
schema: '__mj',
|
|
262
|
+
tableName: 'User',
|
|
263
|
+
indexName: 'idx_user_email',
|
|
264
|
+
columns: ['Email'],
|
|
265
|
+
unique: true,
|
|
266
|
+
method: 'btree'
|
|
267
|
+
});
|
|
268
|
+
// 'CREATE UNIQUE INDEX IF NOT EXISTS "idx_user_email"
|
|
269
|
+
// ON __mj."User" USING btree("Email")'
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
### Polymorphic Usage
|
|
273
|
+
|
|
274
|
+
The key benefit is writing database-agnostic code that works with any dialect:
|
|
275
|
+
|
|
276
|
+
```typescript
|
|
277
|
+
function buildSelectQuery(dialect: SQLDialect, schema: string, table: string,
|
|
278
|
+
columns: string[], limit: number): string {
|
|
279
|
+
const { prefix, suffix } = dialect.LimitClause(limit);
|
|
280
|
+
const qualifiedTable = dialect.QuoteSchema(schema, table);
|
|
281
|
+
const quotedCols = columns.map(c => dialect.QuoteIdentifier(c)).join(', ');
|
|
282
|
+
|
|
283
|
+
return `SELECT ${prefix} ${quotedCols} FROM ${qualifiedTable} ${suffix}`.trim();
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// SQL Server output:
|
|
287
|
+
// SELECT TOP 10 [ID], [Name] FROM [__mj].[User]
|
|
288
|
+
|
|
289
|
+
// PostgreSQL output:
|
|
290
|
+
// SELECT "ID", "Name" FROM __mj."User" LIMIT 10
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
## Adding a New Dialect
|
|
294
|
+
|
|
295
|
+
To add support for a new database platform (e.g., MySQL):
|
|
296
|
+
|
|
297
|
+
1. **Create the type map class** implementing `DataTypeMap`:
|
|
298
|
+
|
|
299
|
+
```typescript
|
|
300
|
+
import { DataTypeMap, MappedType } from '@memberjunction/sql-dialect';
|
|
301
|
+
|
|
302
|
+
class MySQLDataTypeMap implements DataTypeMap {
|
|
303
|
+
MapType(sourceType: string, sourceLength?: number,
|
|
304
|
+
sourcePrecision?: number, sourceScale?: number): MappedType {
|
|
305
|
+
const normalized = sourceType.toUpperCase().trim();
|
|
306
|
+
switch (normalized) {
|
|
307
|
+
case 'UNIQUEIDENTIFIER':
|
|
308
|
+
return { typeName: 'CHAR', supportsLength: true,
|
|
309
|
+
supportsPrecisionScale: false, defaultLength: 36 };
|
|
310
|
+
case 'BIT':
|
|
311
|
+
return { typeName: 'TINYINT(1)', supportsLength: false,
|
|
312
|
+
supportsPrecisionScale: false };
|
|
313
|
+
// ... map remaining types
|
|
314
|
+
default:
|
|
315
|
+
return { typeName: normalized, supportsLength: false,
|
|
316
|
+
supportsPrecisionScale: false };
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
MapTypeToString(sourceType: string, sourceLength?: number,
|
|
321
|
+
sourcePrecision?: number, sourceScale?: number): string {
|
|
322
|
+
const mapped = this.MapType(sourceType, sourceLength, sourcePrecision, sourceScale);
|
|
323
|
+
// Format with length/precision as needed
|
|
324
|
+
return mapped.typeName;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
2. **Create the dialect class** extending `SQLDialect`:
|
|
330
|
+
|
|
331
|
+
```typescript
|
|
332
|
+
import { SQLDialect, DataTypeMap } from '@memberjunction/sql-dialect';
|
|
333
|
+
|
|
334
|
+
export class MySQLDialect extends SQLDialect {
|
|
335
|
+
get PlatformKey(): DatabasePlatform { return 'mysql' as DatabasePlatform; }
|
|
336
|
+
get TypeMap(): DataTypeMap { return new MySQLDataTypeMap(); }
|
|
337
|
+
|
|
338
|
+
QuoteIdentifier(name: string): string { return `\`${name}\``; }
|
|
339
|
+
QuoteSchema(schema: string, object: string): string {
|
|
340
|
+
return `\`${schema}\`.\`${object}\``;
|
|
341
|
+
}
|
|
342
|
+
LimitClause(limit: number, offset?: number): LimitClauseResult {
|
|
343
|
+
const suffix = offset != null
|
|
344
|
+
? `LIMIT ${limit} OFFSET ${offset}`
|
|
345
|
+
: `LIMIT ${limit}`;
|
|
346
|
+
return { prefix: '', suffix };
|
|
347
|
+
}
|
|
348
|
+
BooleanLiteral(value: boolean): string { return value ? '1' : '0'; }
|
|
349
|
+
// ... implement all remaining abstract methods (~25+)
|
|
350
|
+
}
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
3. **Update the `DatabasePlatform` type** in `sqlDialect.ts` to include the new platform key.
|
|
354
|
+
|
|
355
|
+
4. **Export from `index.ts`**:
|
|
356
|
+
|
|
357
|
+
```typescript
|
|
358
|
+
export { MySQLDialect } from './mysqlDialect.js';
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
5. **Add tests** in `src/__tests__/mysqlDialect.test.ts` covering every method. The existing `crossDialect.test.ts` provides a pattern for testing multiple dialects against the same assertions.
|
|
362
|
+
|
|
363
|
+
## Side-by-Side Dialect Comparison
|
|
364
|
+
|
|
365
|
+
| Feature | SQL Server (`SQLServerDialect`) | PostgreSQL (`PostgreSQLDialect`) |
|
|
366
|
+
|---|---|---|
|
|
367
|
+
| **Identifier quoting** | `[name]` | `"name"` |
|
|
368
|
+
| **Schema-qualified** | `[schema].[object]` | `schema."object"` |
|
|
369
|
+
| **Boolean literals** | `1` / `0` | `true` / `false` |
|
|
370
|
+
| **Current UTC time** | `GETUTCDATE()` | `(NOW() AT TIME ZONE 'UTC')` |
|
|
371
|
+
| **New UUID** | `NEWID()` | `gen_random_uuid()` |
|
|
372
|
+
| **UUID PK default** | `NEWSEQUENTIALID()` | `gen_random_uuid()` |
|
|
373
|
+
| **Auto-increment** | `IDENTITY(1,1)` | `GENERATED ALWAYS AS IDENTITY` |
|
|
374
|
+
| **Pagination (no offset)** | `SELECT TOP 10 ...` | `... LIMIT 10` |
|
|
375
|
+
| **Pagination (with offset)** | `OFFSET 20 ROWS FETCH NEXT 10 ROWS ONLY` | `LIMIT 10 OFFSET 20` |
|
|
376
|
+
| **Return inserted** | `OUTPUT INSERTED.*` | `RETURNING *` |
|
|
377
|
+
| **Scope identity** | `SCOPE_IDENTITY()` | `lastval()` |
|
|
378
|
+
| **Row count** | `@@ROWCOUNT` | `ROW_COUNT` (GET DIAGNOSTICS) |
|
|
379
|
+
| **Concatenation** | `+` | `\|\|` |
|
|
380
|
+
| **Parameters** | `@p0`, `@p1`, ... | `$1`, `$2`, ... |
|
|
381
|
+
| **Batch separator** | `GO` | _(none)_ |
|
|
382
|
+
| **Conditional** | `IIF(cond, t, f)` | `CASE WHEN cond THEN t ELSE f END` |
|
|
383
|
+
| **Recursive CTE** | `WITH` | `WITH RECURSIVE` |
|
|
384
|
+
| **Procedure call** | `EXEC [s].[name] @p0` | `SELECT * FROM s."name"($1)` |
|
|
385
|
+
| **CREATE OR REPLACE** | Not supported | FUNCTION, VIEW, PROCEDURE |
|
|
386
|
+
| **Full-text search** | `CONTAINS([col], term)` | `col @@ plainto_tsquery(...)` |
|
|
387
|
+
| **JSON extract** | `JSON_VALUE(col, 'path')` | `col->>'path'` |
|
|
388
|
+
| **String split** | `STRING_SPLIT(val, delim)` | `unnest(string_to_array(val, delim))` |
|
|
389
|
+
| **Cast to text** | `CAST(x AS NVARCHAR(MAX))` | `CAST(x AS TEXT)` |
|
|
390
|
+
| **Cast to UUID** | `CAST(x AS UNIQUEIDENTIFIER)` | `CAST(x AS UUID)` |
|
|
391
|
+
| **Object existence** | `IF OBJECT_ID(...) IS NOT NULL` | `SELECT EXISTS (... pg_catalog ...)` |
|
|
392
|
+
| **Comments/descriptions** | `sp_addextendedproperty` | `COMMENT ON ...` |
|
|
393
|
+
| **Grants** | `GRANT ... ON [s].[o] TO [r]` | `GRANT ... ON s."o" TO "r"` |
|
|
394
|
+
|
|
395
|
+
## Installation
|
|
396
|
+
|
|
397
|
+
```bash
|
|
398
|
+
npm install @memberjunction/sql-dialect
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
Or, in a MemberJunction workspace, add the dependency to your package's `package.json` and run `npm install` from the repo root.
|
|
402
|
+
|
|
403
|
+
## License
|
|
404
|
+
|
|
405
|
+
ISC
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Represents a mapped data type from one database platform to another.
|
|
3
|
+
*/
|
|
4
|
+
export interface MappedType {
|
|
5
|
+
/** The target platform data type name (e.g., "VARCHAR", "UUID", "BOOLEAN") */
|
|
6
|
+
typeName: string;
|
|
7
|
+
/** Whether the type supports a length parameter (e.g., VARCHAR(255)) */
|
|
8
|
+
supportsLength: boolean;
|
|
9
|
+
/** Whether the type supports precision/scale (e.g., NUMERIC(10,2)) */
|
|
10
|
+
supportsPrecisionScale: boolean;
|
|
11
|
+
/** Default length if applicable */
|
|
12
|
+
defaultLength?: number;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Maps data types from one database platform to another.
|
|
16
|
+
* Each dialect provides its own DataTypeMap implementation.
|
|
17
|
+
*/
|
|
18
|
+
export interface DataTypeMap {
|
|
19
|
+
/**
|
|
20
|
+
* Maps a source database type to the target platform type.
|
|
21
|
+
* @param sourceType - The source database type name (case-insensitive)
|
|
22
|
+
* @param sourceLength - Optional length from the source type definition
|
|
23
|
+
* @param sourcePrecision - Optional precision from the source type definition
|
|
24
|
+
* @param sourceScale - Optional scale from the source type definition
|
|
25
|
+
* @returns The mapped type for the target platform
|
|
26
|
+
*/
|
|
27
|
+
MapType(sourceType: string, sourceLength?: number, sourcePrecision?: number, sourceScale?: number): MappedType;
|
|
28
|
+
/**
|
|
29
|
+
* Returns the full type string with length/precision/scale as needed.
|
|
30
|
+
* E.g., "VARCHAR(255)" or "NUMERIC(10,2)" or "UUID"
|
|
31
|
+
*/
|
|
32
|
+
MapTypeToString(sourceType: string, sourceLength?: number, sourcePrecision?: number, sourceScale?: number): string;
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=dataTypeMap.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dataTypeMap.d.ts","sourceRoot":"","sources":["../src/dataTypeMap.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,WAAW,UAAU;IACvB,8EAA8E;IAC9E,QAAQ,EAAE,MAAM,CAAC;IACjB,wEAAwE;IACxE,cAAc,EAAE,OAAO,CAAC;IACxB,sEAAsE;IACtE,sBAAsB,EAAE,OAAO,CAAC;IAChC,mCAAmC;IACnC,aAAa,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED;;;GAGG;AACH,MAAM,WAAW,WAAW;IACxB;;;;;;;OAOG;IACH,OAAO,CAAC,UAAU,EAAE,MAAM,EAAE,YAAY,CAAC,EAAE,MAAM,EAAE,eAAe,CAAC,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,UAAU,CAAC;IAE/G;;;OAGG;IACH,eAAe,CAAC,UAAU,EAAE,MAAM,EAAE,YAAY,CAAC,EAAE,MAAM,EAAE,eAAe,CAAC,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;CACtH"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dataTypeMap.js","sourceRoot":"","sources":["../src/dataTypeMap.ts"],"names":[],"mappings":""}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { DataTypeMap, MappedType } from './dataTypeMap.js';
|
|
2
|
+
export { SQLDialect, DatabasePlatform, LimitClauseResult, SchemaIntrospectionSQL, TriggerOptions, IndexOptions, } from './sqlDialect.js';
|
|
3
|
+
export { SQLServerDialect } from './sqlServerDialect.js';
|
|
4
|
+
export { PostgreSQLDialect } from './postgresqlDialect.js';
|
|
5
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC3D,OAAO,EACH,UAAU,EACV,gBAAgB,EAChB,iBAAiB,EACjB,sBAAsB,EACtB,cAAc,EACd,YAAY,GACf,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AACzD,OAAO,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EACH,UAAU,GAMb,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AACzD,OAAO,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { DataTypeMap } from './dataTypeMap.js';
|
|
2
|
+
import { SQLDialect, DatabasePlatform, LimitClauseResult, SchemaIntrospectionSQL, TriggerOptions, IndexOptions } from './sqlDialect.js';
|
|
3
|
+
/**
|
|
4
|
+
* PostgreSQL dialect implementation.
|
|
5
|
+
* Uses "double-quote" identifiers, LIMIT/OFFSET pagination, native BOOLEAN, PL/pgSQL functions.
|
|
6
|
+
*/
|
|
7
|
+
export declare class PostgreSQLDialect extends SQLDialect {
|
|
8
|
+
get PlatformKey(): DatabasePlatform;
|
|
9
|
+
QuoteIdentifier(name: string): string;
|
|
10
|
+
QuoteSchema(schema: string, object: string): string;
|
|
11
|
+
LimitClause(limit: number, offset?: number): LimitClauseResult;
|
|
12
|
+
BooleanLiteral(value: boolean): string;
|
|
13
|
+
CurrentTimestampUTC(): string;
|
|
14
|
+
NewUUID(): string;
|
|
15
|
+
CastToText(expr: string): string;
|
|
16
|
+
CastToUUID(expr: string): string;
|
|
17
|
+
ReturnInsertedClause(columns?: string[]): string;
|
|
18
|
+
AutoIncrementPKExpression(): string;
|
|
19
|
+
UUIDPKDefault(): string;
|
|
20
|
+
ScopeIdentityExpression(): string;
|
|
21
|
+
RowCountExpression(): string;
|
|
22
|
+
BatchSeparator(): string;
|
|
23
|
+
ExistenceCheckSQL(objectType: string, schema: string, name: string): string;
|
|
24
|
+
CreateOrReplaceSupported(objectType: string): boolean;
|
|
25
|
+
FullTextSearchPredicate(column: string, searchTerm: string): string;
|
|
26
|
+
FullTextIndexDDL(table: string, columns: string[], _catalog?: string): string;
|
|
27
|
+
RecursiveCTESyntax(): string;
|
|
28
|
+
get TypeMap(): DataTypeMap;
|
|
29
|
+
ParameterPlaceholder(index: number): string;
|
|
30
|
+
ConcatOperator(): string;
|
|
31
|
+
StringSplitFunction(value: string, delimiter: string): string;
|
|
32
|
+
JsonExtract(column: string, path: string): string;
|
|
33
|
+
ProcedureCallSyntax(schema: string, name: string, params: string[]): string;
|
|
34
|
+
TriggerDDL(options: TriggerOptions): string;
|
|
35
|
+
IndexDDL(options: IndexOptions): string;
|
|
36
|
+
GrantPermission(permission: string, _objectType: string, schema: string, object: string, role: string): string;
|
|
37
|
+
CommentOnObject(objectType: string, schema: string, name: string, comment: string): string;
|
|
38
|
+
SchemaIntrospectionQueries(): SchemaIntrospectionSQL;
|
|
39
|
+
IIF(condition: string, trueVal: string, falseVal: string): string;
|
|
40
|
+
private extractSchema;
|
|
41
|
+
private extractName;
|
|
42
|
+
}
|
|
43
|
+
//# sourceMappingURL=postgresqlDialect.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"postgresqlDialect.d.ts","sourceRoot":"","sources":["../src/postgresqlDialect.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAc,MAAM,kBAAkB,CAAC;AAC3D,OAAO,EACH,UAAU,EACV,gBAAgB,EAChB,iBAAiB,EACjB,sBAAsB,EACtB,cAAc,EACd,YAAY,EACf,MAAM,iBAAiB,CAAC;AAwIzB;;;GAGG;AACH,qBAAa,iBAAkB,SAAQ,UAAU;IAC7C,IAAI,WAAW,IAAI,gBAAgB,CAElC;IAID,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM;IAIrC,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM;IAMnD,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,iBAAiB;IAU9D,cAAc,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM;IAItC,mBAAmB,IAAI,MAAM;IAI7B,OAAO,IAAI,MAAM;IAIjB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM;IAIhC,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM;IAMhC,oBAAoB,CAAC,OAAO,CAAC,EAAE,MAAM,EAAE,GAAG,MAAM;IAQhD,yBAAyB,IAAI,MAAM;IAInC,aAAa,IAAI,MAAM;IAIvB,uBAAuB,IAAI,MAAM;IAIjC,kBAAkB,IAAI,MAAM;IAO5B,cAAc,IAAI,MAAM;IAIxB,iBAAiB,CAAC,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM;IAkB3E,wBAAwB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO;IAOrD,uBAAuB,CAAC,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM;IAInE,gBAAgB,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM;IAkC7E,kBAAkB,IAAI,MAAM;IAM5B,IAAI,OAAO,IAAI,WAAW,CAEzB;IAID,oBAAoB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM;IAI3C,cAAc,IAAI,MAAM;IAMxB,mBAAmB,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM;IAI7D,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM;IAOjD,mBAAmB,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM;IAO3E,UAAU,CAAC,OAAO,EAAE,cAAc,GAAG,MAAM;IAwB3C,QAAQ,CAAC,OAAO,EAAE,YAAY,GAAG,MAAM;IAavC,eAAe,CAAC,UAAU,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM;IAI9G,eAAe,CAAC,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM;IAQ1F,0BAA0B,IAAI,sBAAsB;IAgEpD,GAAG,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM;IAMjE,OAAO,CAAC,aAAa;IAMrB,OAAO,CAAC,WAAW;CAKtB"}
|