ts-prorm-orm 1.1.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/CHANGELOG.md +1111 -0
- package/LICENSE +21 -0
- package/README.md +573 -0
- package/dist/audit/history-query.js +180 -0
- package/dist/audit/index.js +23 -0
- package/dist/audit/logger.js +236 -0
- package/dist/cache/cache-manager.js +84 -0
- package/dist/cache/index.js +16 -0
- package/dist/cache/redis-cluster-cache.js +557 -0
- package/dist/cli.js +2200 -0
- package/dist/compliance/audit-trail.js +68 -0
- package/dist/compliance/backup-verification.js +685 -0
- package/dist/compliance/breach-detector.js +505 -0
- package/dist/compliance/consent-record.js +227 -0
- package/dist/compliance/consent-versioning.js +331 -0
- package/dist/compliance/cross-border-log.js +530 -0
- package/dist/compliance/data-classifier.js +258 -0
- package/dist/compliance/data-lineage.js +565 -0
- package/dist/compliance/data-masker.js +303 -0
- package/dist/compliance/data-portability.js +265 -0
- package/dist/compliance/data-retention.js +195 -0
- package/dist/compliance/dsar-workflow.js +342 -0
- package/dist/compliance/field-encryption.js +223 -0
- package/dist/compliance/immutable-record.js +148 -0
- package/dist/compliance/index.js +260 -0
- package/dist/compliance/privacy-impact-assessment.js +364 -0
- package/dist/compliance/pseudonymization.js +269 -0
- package/dist/compliance/query-firewall.js +1182 -0
- package/dist/compliance/rate-limiter.js +580 -0
- package/dist/compliance/right-to-erasure.js +117 -0
- package/dist/compliance/row-level-security.js +246 -0
- package/dist/compliance/security-decorator.js +574 -0
- package/dist/compliance/security-monitor.js +525 -0
- package/dist/compliance/sensitive-data-discovery.js +476 -0
- package/dist/compliance/session-isolation.js +472 -0
- package/dist/compliance/tls-enforcer.js +108 -0
- package/dist/compliance/worm-storage.js +712 -0
- package/dist/connection-manager.js +198 -0
- package/dist/connection-pool.js +519 -0
- package/dist/decorators/audit.js +136 -0
- package/dist/decorators/belongs-to-many.js +115 -0
- package/dist/decorators/belongs-to.js +115 -0
- package/dist/decorators/check.js +435 -0
- package/dist/decorators/collate.js +329 -0
- package/dist/decorators/comment.js +205 -0
- package/dist/decorators/database-settings.js +236 -0
- package/dist/decorators/default.js +244 -0
- package/dist/decorators/encryption.js +235 -0
- package/dist/decorators/engine.js +97 -0
- package/dist/decorators/fk-constraints.js +594 -0
- package/dist/decorators/foreign-table.js +136 -0
- package/dist/decorators/generated.js +274 -0
- package/dist/decorators/has-many.js +127 -0
- package/dist/decorators/has-one.js +116 -0
- package/dist/decorators/hstore.js +129 -0
- package/dist/decorators/index.js +355 -0
- package/dist/decorators/json-column.js +83 -0
- package/dist/decorators/jsonb.js +101 -0
- package/dist/decorators/orm-decorators.js +425 -0
- package/dist/decorators/permissions.js +294 -0
- package/dist/decorators/procedure.js +210 -0
- package/dist/decorators/query-options.js +556 -0
- package/dist/decorators/range.js +167 -0
- package/dist/decorators/set-column.js +83 -0
- package/dist/decorators/spatial.js +114 -0
- package/dist/decorators/storage.js +580 -0
- package/dist/decorators/timezone.js +512 -0
- package/dist/decorators/trigger.js +90 -0
- package/dist/decorators/uuid.js +135 -0
- package/dist/decorators/view.js +258 -0
- package/dist/diagrams/chen-diagram.js +354 -0
- package/dist/diagrams/class-diagram.js +384 -0
- package/dist/diagrams/dependency-diagram.js +432 -0
- package/dist/diagrams/er-diagram.js +605 -0
- package/dist/diagrams/flow-diagram.js +394 -0
- package/dist/diagrams/gantt-diagram.js +411 -0
- package/dist/diagrams/index-diagram.js +353 -0
- package/dist/diagrams/index.js +184 -0
- package/dist/diagrams/migration-diagram.js +316 -0
- package/dist/diagrams/model-diagram.js +616 -0
- package/dist/diagrams/package-diagram.js +376 -0
- package/dist/diagrams/palette.js +85 -0
- package/dist/diagrams/relational-diagram.js +455 -0
- package/dist/diagrams/schemadoc-diagram.js +309 -0
- package/dist/diagrams/sequence-diagram.js +307 -0
- package/dist/diagrams/state-diagram.js +344 -0
- package/dist/diagrams/svg-dom.js +111 -0
- package/dist/diagrams/tree-diagram.js +250 -0
- package/dist/dialects/clickhouse/index.js +1742 -0
- package/dist/dialects/cockroachdb/index.js +4677 -0
- package/dist/dialects/cratedb/index.js +441 -0
- package/dist/dialects/databricks/index.js +517 -0
- package/dist/dialects/db2/index.js +2225 -0
- package/dist/dialects/dialect.js +720 -0
- package/dist/dialects/duckdb/index.js +2014 -0
- package/dist/dialects/exasol/index.js +357 -0
- package/dist/dialects/firebird/index.js +495 -0
- package/dist/dialects/greenplum/index.js +457 -0
- package/dist/dialects/hana/index.js +1741 -0
- package/dist/dialects/mariadb/index.js +3590 -0
- package/dist/dialects/mssql/index.js +2296 -0
- package/dist/dialects/mysql/index.js +4058 -0
- package/dist/dialects/oracle/index.js +2999 -0
- package/dist/dialects/postgres/index.js +5319 -0
- package/dist/dialects/query-stream-helper.js +152 -0
- package/dist/dialects/questdb/index.js +433 -0
- package/dist/dialects/redshift/index.js +2277 -0
- package/dist/dialects/singlestore/index.js +354 -0
- package/dist/dialects/snowflake/index.js +2082 -0
- package/dist/dialects/spanner/index.js +1768 -0
- package/dist/dialects/sqlite/index.js +3382 -0
- package/dist/dialects/tidb/index.js +377 -0
- package/dist/dialects/timescaledb/index.js +164 -0
- package/dist/dialects/trino/index.js +420 -0
- package/dist/dialects/turso/index.js +227 -0
- package/dist/dialects/vertica/index.js +317 -0
- package/dist/dialects/yugabytedb/index.js +424 -0
- package/dist/errors/index.js +472 -0
- package/dist/errors/utils.js +340 -0
- package/dist/errors.js +21 -0
- package/dist/extensions/catalog/cloud-warehouse-features.js +207 -0
- package/dist/extensions/catalog/mssql-features.js +147 -0
- package/dist/extensions/catalog/mysql-mariadb-plugins.js +229 -0
- package/dist/extensions/catalog/oracle-db2-features.js +196 -0
- package/dist/extensions/catalog/postgres-extensions.js +516 -0
- package/dist/extensions/index.js +71 -0
- package/dist/extensions/types.js +13 -0
- package/dist/foreign-data.js +173 -0
- package/dist/hooks/hooks-manager.js +350 -0
- package/dist/hooks/index.js +37 -0
- package/dist/index.js +463 -0
- package/dist/logging.js +335 -0
- package/dist/migrations/index.js +40 -0
- package/dist/migrations/migration.js +196 -0
- package/dist/migrations/migrator.js +411 -0
- package/dist/migrations/prormmigration.js +275 -0
- package/dist/migrations/query-interface.js +435 -0
- package/dist/migrations/seeder.js +353 -0
- package/dist/models/associations.js +852 -0
- package/dist/models/constraints.js +288 -0
- package/dist/models/data-types.js +2518 -0
- package/dist/models/decorators.js +445 -0
- package/dist/models/index.js +33 -0
- package/dist/models/indexes.js +531 -0
- package/dist/models/methods.js +382 -0
- package/dist/models/model-manager.js +103 -0
- package/dist/models/model.js +5349 -0
- package/dist/models/operators.js +67 -0
- package/dist/models/scopes.js +189 -0
- package/dist/models/typescript-types.js +26 -0
- package/dist/nosql/aerospike/index.js +205 -0
- package/dist/nosql/allegrograph/index.js +169 -0
- package/dist/nosql/arangodb/index.js +364 -0
- package/dist/nosql/azure-blob/index.js +206 -0
- package/dist/nosql/beanstalkd/index.js +231 -0
- package/dist/nosql/beequeue/index.js +210 -0
- package/dist/nosql/bigchaindb/index.js +195 -0
- package/dist/nosql/bigtable/index.js +224 -0
- package/dist/nosql/blazegraph/index.js +173 -0
- package/dist/nosql/bullmq/index.js +191 -0
- package/dist/nosql/cassandra/index.js +174 -0
- package/dist/nosql/chroma/index.js +190 -0
- package/dist/nosql/cloudflare-kv/index.js +220 -0
- package/dist/nosql/coherence/index.js +200 -0
- package/dist/nosql/cosmosdb/index.js +157 -0
- package/dist/nosql/couchbase/index.js +213 -0
- package/dist/nosql/dax/index.js +212 -0
- package/dist/nosql/deno-kv/index.js +206 -0
- package/dist/nosql/dgraph/index.js +171 -0
- package/dist/nosql/doris/index.js +169 -0
- package/dist/nosql/druid/index.js +162 -0
- package/dist/nosql/dynamodb/index.js +937 -0
- package/dist/nosql/elasticsearch/index.js +377 -0
- package/dist/nosql/etcd/index.js +502 -0
- package/dist/nosql/eventhubs/index.js +213 -0
- package/dist/nosql/eventstore/index.js +254 -0
- package/dist/nosql/faunadb/index.js +188 -0
- package/dist/nosql/firestore/index.js +177 -0
- package/dist/nosql/fluree/index.js +148 -0
- package/dist/nosql/fuseki/index.js +170 -0
- package/dist/nosql/gcs/index.js +172 -0
- package/dist/nosql/gearman/index.js +160 -0
- package/dist/nosql/geode/index.js +196 -0
- package/dist/nosql/graphdb/index.js +169 -0
- package/dist/nosql/graylog/index.js +188 -0
- package/dist/nosql/gridgain/index.js +171 -0
- package/dist/nosql/hazelcast/index.js +162 -0
- package/dist/nosql/hbase/index.js +230 -0
- package/dist/nosql/ignite/index.js +173 -0
- package/dist/nosql/immudb/index.js +184 -0
- package/dist/nosql/index.js +232 -0
- package/dist/nosql/infinispan/index.js +200 -0
- package/dist/nosql/influxdb/index.js +0 -0
- package/dist/nosql/kafka/index.js +234 -0
- package/dist/nosql/keyspaces/index.js +189 -0
- package/dist/nosql/kinesis/index.js +253 -0
- package/dist/nosql/leveldb/index.js +153 -0
- package/dist/nosql/lmdb/index.js +160 -0
- package/dist/nosql/loki/index.js +201 -0
- package/dist/nosql/marklogic/index.js +204 -0
- package/dist/nosql/materialize/index.js +145 -0
- package/dist/nosql/meilisearch/index.js +154 -0
- package/dist/nosql/memcached/index.js +223 -0
- package/dist/nosql/milvus/index.js +410 -0
- package/dist/nosql/minio/index.js +265 -0
- package/dist/nosql/momento/index.js +179 -0
- package/dist/nosql/mongodb/index.js +461 -0
- package/dist/nosql/nats/index.js +247 -0
- package/dist/nosql/nedb/index.js +164 -0
- package/dist/nosql/neo4j/index.js +450 -0
- package/dist/nosql/neptune/index.js +470 -0
- package/dist/nosql/nsq/index.js +200 -0
- package/dist/nosql/opensearch/index.js +186 -0
- package/dist/nosql/orientdb/index.js +175 -0
- package/dist/nosql/papertrail/index.js +200 -0
- package/dist/nosql/pinecone/index.js +0 -0
- package/dist/nosql/pinot/index.js +133 -0
- package/dist/nosql/pouchdb/index.js +172 -0
- package/dist/nosql/prometheus/index.js +174 -0
- package/dist/nosql/provendb/index.js +147 -0
- package/dist/nosql/pubsub/index.js +187 -0
- package/dist/nosql/pulsar/index.js +232 -0
- package/dist/nosql/qdrant/index.js +295 -0
- package/dist/nosql/qldb/index.js +192 -0
- package/dist/nosql/r2/index.js +281 -0
- package/dist/nosql/rabbitmq/index.js +237 -0
- package/dist/nosql/ravendb/index.js +175 -0
- package/dist/nosql/redis/index.js +607 -0
- package/dist/nosql/redpanda/index.js +237 -0
- package/dist/nosql/resque/index.js +203 -0
- package/dist/nosql/rethinkdb/index.js +232 -0
- package/dist/nosql/rocksdb/index.js +152 -0
- package/dist/nosql/rockset/index.js +126 -0
- package/dist/nosql/s3/index.js +298 -0
- package/dist/nosql/scylladb/index.js +178 -0
- package/dist/nosql/signoz/index.js +227 -0
- package/dist/nosql/sns/index.js +201 -0
- package/dist/nosql/solr/index.js +231 -0
- package/dist/nosql/splunk/index.js +227 -0
- package/dist/nosql/sqs/index.js +246 -0
- package/dist/nosql/stardog/index.js +169 -0
- package/dist/nosql/starrocks/index.js +170 -0
- package/dist/nosql/store.js +2 -0
- package/dist/nosql/sumologic/index.js +213 -0
- package/dist/nosql/surrealdb/index.js +179 -0
- package/dist/nosql/terminusdb/index.js +151 -0
- package/dist/nosql/tigergraph/index.js +469 -0
- package/dist/nosql/typesense/index.js +148 -0
- package/dist/nosql/unqlite/index.js +157 -0
- package/dist/nosql/upstash/index.js +167 -0
- package/dist/nosql/vercel-kv/index.js +166 -0
- package/dist/nosql/victoriametrics/index.js +213 -0
- package/dist/nosql/virtuoso/index.js +169 -0
- package/dist/nosql/weaviate/index.js +276 -0
- package/dist/operators/index.js +108 -0
- package/dist/operators.js +2690 -0
- package/dist/prisma-migrate/index.js +38 -0
- package/dist/prisma-migrate/migration-generator.js +125 -0
- package/dist/prisma-migrate/model-generator.js +172 -0
- package/dist/prisma-migrate/relations.js +100 -0
- package/dist/prisma-migrate/schema-parser.js +167 -0
- package/dist/prisma-migrate/type-mapper.js +40 -0
- package/dist/prorm.js +6832 -0
- package/dist/query-builders/cte-builder.js +80 -0
- package/dist/query-builders/functions/aggregate.js +390 -0
- package/dist/query-builders/functions/conditional.js +503 -0
- package/dist/query-builders/functions/datetime.js +695 -0
- package/dist/query-builders/functions/fulltext.js +439 -0
- package/dist/query-builders/functions/index.js +93 -0
- package/dist/query-builders/functions/json.js +427 -0
- package/dist/query-builders/functions/math.js +399 -0
- package/dist/query-builders/functions/string.js +518 -0
- package/dist/query-builders/functions/window.js +328 -0
- package/dist/query-builders/include-builder.js +323 -0
- package/dist/query-builders/index-expression-builder.js +242 -0
- package/dist/query-builders/index.js +161 -0
- package/dist/query-builders/insert-builder.js +164 -0
- package/dist/query-builders/model-helpers.js +38 -0
- package/dist/query-builders/order-limit-builder.js +239 -0
- package/dist/query-builders/sql-compiler.js +2040 -0
- package/dist/query-builders/subquery-builder.js +152 -0
- package/dist/query-builders/update-builder.js +182 -0
- package/dist/query-builders/view-builder.js +218 -0
- package/dist/query-builders/where-builder.js +1532 -0
- package/dist/query-interface.js +622 -0
- package/dist/query-optimizers/batch-optimizer.js +426 -0
- package/dist/query-optimizers/explain-plans.js +415 -0
- package/dist/query-optimizers/index.js +51 -0
- package/dist/query-optimizers/prepared-statement-cache.js +423 -0
- package/dist/query-optimizers/query-hints.js +438 -0
- package/dist/query-optimizers/query-optimizer.js +278 -0
- package/dist/query-optimizers/slow-query-logger.js +271 -0
- package/dist/replica-manager.js +507 -0
- package/dist/schema/index.js +15 -0
- package/dist/schema/migration-generator.js +374 -0
- package/dist/schema/schema-differ.js +549 -0
- package/dist/schema/types.js +6 -0
- package/dist/sql-constants.js +300 -0
- package/dist/sqlite-advanced.js +1047 -0
- package/dist/streams/index.js +12 -0
- package/dist/streams/transforms.js +178 -0
- package/dist/transaction.js +456 -0
- package/dist/types/index.js +180 -0
- package/dist/user-management.js +127 -0
- package/dist/utils/date.js +300 -0
- package/dist/utils/index.js +1079 -0
- package/dist/utils/string.js +161 -0
- package/dist/validators/index.js +19 -0
- package/dist/validators/validator.js +911 -0
- package/package.json +190 -0
|
@@ -0,0 +1,1741 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* SAP HANA dialect implementation for the TypeScript ORM.
|
|
4
|
+
* Uses the `hdb` driver - SAP's pure-JavaScript, callback-based HANA client
|
|
5
|
+
* (no native bindings, no bundled TypeScript types - see `src/dialects/hana/hdb.d.ts`
|
|
6
|
+
* for the minimal ambient shim written for it).
|
|
7
|
+
*
|
|
8
|
+
* Notable SAP HANA SQL characteristics implemented here:
|
|
9
|
+
* - Double-quoted, case-sensitive delimited identifiers (unquoted
|
|
10
|
+
* identifiers are folded to uppercase by HANA, like Oracle/Db2).
|
|
11
|
+
* - `LIMIT n OFFSET m` for pagination (standard, unlike Oracle/Db2's
|
|
12
|
+
* `OFFSET ... FETCH FIRST ...`) - HANA additionally requires a `LIMIT`
|
|
13
|
+
* whenever `OFFSET` is used, so a sentinel max `LIMIT` is emitted when
|
|
14
|
+
* only an offset is supplied.
|
|
15
|
+
* - `CREATE COLUMN TABLE` / `CREATE ROW TABLE` - HANA's column-store
|
|
16
|
+
* (default, controlled here via `TableOptions.columnStore`, default
|
|
17
|
+
* `true`) vs row-store table creation.
|
|
18
|
+
* - `GENERATED ALWAYS AS IDENTITY` for auto-increment columns.
|
|
19
|
+
* - `CREATE SEQUENCE` / `<sequence>.NEXTVAL` (via the single-row `DUMMY`
|
|
20
|
+
* pseudo-table, HANA's equivalent of Oracle's `DUAL`) for sequences.
|
|
21
|
+
* - Native `UPSERT <table> (...) VALUES (...) WITH PRIMARY KEY` statement
|
|
22
|
+
* (a genuine HANA-only construct, distinct from the `MERGE INTO` every
|
|
23
|
+
* other enterprise dialect here uses for upserts) plus full `MERGE INTO
|
|
24
|
+
* ... WHEN MATCHED / WHEN NOT MATCHED` support for more complex cases.
|
|
25
|
+
* - Parenthesized, multi-column `ALTER TABLE ... ADD (...)` / `DROP (...)`
|
|
26
|
+
* / `ALTER (...)` column DDL, and a dedicated `RENAME COLUMN t.old TO
|
|
27
|
+
* new` statement - all distinct from the single-column-at-a-time ALTER
|
|
28
|
+
* syntax used by most other dialects.
|
|
29
|
+
* - Mature `WITH` CTE (including recursive) and window/OLAP function
|
|
30
|
+
* support.
|
|
31
|
+
* - HANA-specific types: `NVARCHAR`, `SHORTTEXT`, `ALPHANUM`, `SECONDDATE`,
|
|
32
|
+
* `TINYINT`, `ST_GEOMETRY`/`ST_POINT` (spatial).
|
|
33
|
+
*
|
|
34
|
+
* Explicitly out of scope (not meaningful for a CRUD-oriented relational
|
|
35
|
+
* ORM layer):
|
|
36
|
+
* - `CREATE CALCULATION SCENARIO` / graphical Calculation Views - these are
|
|
37
|
+
* modeled objects normally authored in HANA's web-based modeler /
|
|
38
|
+
* Business Application Studio, not plain SQL DDL a CRUD ORM would emit.
|
|
39
|
+
* - Legacy `CE_` (Calculation Engine) functions - deprecated by SAP since
|
|
40
|
+
* HANA SPS09 in favor of plain SQL, and never had a "create" step an ORM
|
|
41
|
+
* would need to manage.
|
|
42
|
+
* - Full Smart Data Access federation (`CREATE REMOTE SOURCE` / `CREATE
|
|
43
|
+
* VIRTUAL TABLE`) - HANA's real analogue of a Postgres Foreign Data
|
|
44
|
+
* Wrapper, but out of scope for the generic (Postgres-shaped) FDW
|
|
45
|
+
* interface threaded through the shared `Dialect` type; the FDW methods
|
|
46
|
+
* below throw with a pointer to the real HANA feature instead of forcing
|
|
47
|
+
* a lossy 1:1 mapping.
|
|
48
|
+
*/
|
|
49
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
50
|
+
if (k2 === undefined) k2 = k;
|
|
51
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
52
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
53
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
54
|
+
}
|
|
55
|
+
Object.defineProperty(o, k2, desc);
|
|
56
|
+
}) : (function(o, m, k, k2) {
|
|
57
|
+
if (k2 === undefined) k2 = k;
|
|
58
|
+
o[k2] = m[k];
|
|
59
|
+
}));
|
|
60
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
61
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
62
|
+
}) : function(o, v) {
|
|
63
|
+
o["default"] = v;
|
|
64
|
+
});
|
|
65
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
66
|
+
var ownKeys = function(o) {
|
|
67
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
68
|
+
var ar = [];
|
|
69
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
70
|
+
return ar;
|
|
71
|
+
};
|
|
72
|
+
return ownKeys(o);
|
|
73
|
+
};
|
|
74
|
+
return function (mod) {
|
|
75
|
+
if (mod && mod.__esModule) return mod;
|
|
76
|
+
var result = {};
|
|
77
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
78
|
+
__setModuleDefault(result, mod);
|
|
79
|
+
return result;
|
|
80
|
+
};
|
|
81
|
+
})();
|
|
82
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
83
|
+
exports.HanaDialect = void 0;
|
|
84
|
+
const hdb = __importStar(require("hdb"));
|
|
85
|
+
const query_stream_helper_1 = require("../query-stream-helper");
|
|
86
|
+
const prorm_1 = require("../../prorm");
|
|
87
|
+
/** HANA requires a LIMIT whenever OFFSET is used; this sentinel stands in for "no limit". */
|
|
88
|
+
const HANA_MAX_LIMIT = 2147483647;
|
|
89
|
+
/**
|
|
90
|
+
* HANA-specific transaction class. Since `hdb` has no built-in connection
|
|
91
|
+
* pool, a transaction is backed by its own dedicated client connection
|
|
92
|
+
* (autocommit disabled) so its statements never interleave with unrelated
|
|
93
|
+
* queries running against the dialect's shared client.
|
|
94
|
+
*/
|
|
95
|
+
class HanaTransaction {
|
|
96
|
+
constructor(client, options) {
|
|
97
|
+
this.finished = false;
|
|
98
|
+
this.parent = null;
|
|
99
|
+
this.savepoints = [];
|
|
100
|
+
this.client = null;
|
|
101
|
+
this.id = `tx_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
102
|
+
this.options = options || {};
|
|
103
|
+
this.client = client;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Commit the transaction's own dedicated connection directly. This mirrors
|
|
107
|
+
* `HanaDialect.commitTransaction()` (the path used when committing via the
|
|
108
|
+
* dialect) so that calling `tx.commit()` directly also actually commits and
|
|
109
|
+
* closes the connection, rather than merely flipping `finished` and
|
|
110
|
+
* leaking an open, uncommitted connection.
|
|
111
|
+
*/
|
|
112
|
+
async commit() {
|
|
113
|
+
if (this.finished)
|
|
114
|
+
return;
|
|
115
|
+
if (this.client) {
|
|
116
|
+
await new Promise((resolve, reject) => {
|
|
117
|
+
this.client.commit((err) => (err ? reject(err) : resolve()));
|
|
118
|
+
});
|
|
119
|
+
this.client.close();
|
|
120
|
+
}
|
|
121
|
+
this.finished = true;
|
|
122
|
+
}
|
|
123
|
+
/** See `commit()` - mirrors `HanaDialect.rollbackTransaction()`. */
|
|
124
|
+
async rollback() {
|
|
125
|
+
if (this.finished)
|
|
126
|
+
return;
|
|
127
|
+
if (this.client) {
|
|
128
|
+
await new Promise((resolve, reject) => {
|
|
129
|
+
this.client.rollback((err) => (err ? reject(err) : resolve()));
|
|
130
|
+
});
|
|
131
|
+
this.client.close();
|
|
132
|
+
}
|
|
133
|
+
this.finished = true;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* SAP HANA dialect class implementing the shared Dialect interface.
|
|
138
|
+
*/
|
|
139
|
+
class HanaDialect {
|
|
140
|
+
constructor(config) {
|
|
141
|
+
this.name = 'hana';
|
|
142
|
+
this.library = 'hdb';
|
|
143
|
+
this.client = null;
|
|
144
|
+
this._isConnected = false;
|
|
145
|
+
this.config = {
|
|
146
|
+
host: 'localhost',
|
|
147
|
+
port: 30015,
|
|
148
|
+
database: undefined,
|
|
149
|
+
username: 'SYSTEM',
|
|
150
|
+
password: '',
|
|
151
|
+
...config,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
createClient() {
|
|
155
|
+
const options = {
|
|
156
|
+
host: this.config.host,
|
|
157
|
+
port: this.config.port,
|
|
158
|
+
user: this.config.username,
|
|
159
|
+
password: this.config.password,
|
|
160
|
+
...(this.config.database ? { databaseName: this.config.database } : {}),
|
|
161
|
+
...(this.config.useTLS ? { useTLS: true } : {}),
|
|
162
|
+
...(this.config.extraOptions || {}),
|
|
163
|
+
};
|
|
164
|
+
return hdb.createClient(options);
|
|
165
|
+
}
|
|
166
|
+
connectClient(client) {
|
|
167
|
+
return new Promise((resolve, reject) => {
|
|
168
|
+
client.connect((err) => (err ? reject(err) : resolve()));
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
async connect() {
|
|
172
|
+
const client = this.createClient();
|
|
173
|
+
await this.connectClient(client);
|
|
174
|
+
this.client = client;
|
|
175
|
+
this._isConnected = true;
|
|
176
|
+
}
|
|
177
|
+
async disconnect() {
|
|
178
|
+
if (this.client) {
|
|
179
|
+
await new Promise((resolve, reject) => {
|
|
180
|
+
this.client.disconnect((err) => (err ? reject(err) : resolve()));
|
|
181
|
+
});
|
|
182
|
+
this.client = null;
|
|
183
|
+
this._isConnected = false;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
getConnection() {
|
|
187
|
+
return this.client;
|
|
188
|
+
}
|
|
189
|
+
isConnected() {
|
|
190
|
+
return this._isConnected && this.client !== null && this.client.readyState === 'connected';
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Execute `sql` against a specific hdb client, normalizing hdb's
|
|
194
|
+
* three-shaped callback result (nothing for DDL, an affected-row count
|
|
195
|
+
* for DML, an array of rows for a query) into the shared `QueryResult`
|
|
196
|
+
* shape.
|
|
197
|
+
*
|
|
198
|
+
* `CALL` statements are a distinct fourth shape (see README "Calling
|
|
199
|
+
* Stored Procedures"): hdb always invokes the callback as
|
|
200
|
+
* `cb(err, parameters, ...resultSets)` - a scalar OUT/INOUT parameters
|
|
201
|
+
* object (`{}` if none are declared) followed by zero or more result-set
|
|
202
|
+
* row arrays. Since `parameters` is a plain object rather than an array,
|
|
203
|
+
* it must be handled separately from the DML/query branches above or it
|
|
204
|
+
* silently falls into the `{ rowCount: 0, rows: [] }` default and both the
|
|
205
|
+
* output parameters and any result sets are lost.
|
|
206
|
+
*/
|
|
207
|
+
executeOnClient(client, sql, params) {
|
|
208
|
+
const isCall = /^\s*CALL\b/i.test(sql.trim());
|
|
209
|
+
return new Promise((resolve, reject) => {
|
|
210
|
+
const handle = (err, ...rest) => {
|
|
211
|
+
if (err)
|
|
212
|
+
return reject(err);
|
|
213
|
+
if (isCall) {
|
|
214
|
+
const [outputParams, ...resultSets] = rest;
|
|
215
|
+
const [primaryResultSet, ...additionalResultSets] = resultSets;
|
|
216
|
+
const primaryRows = Array.isArray(primaryResultSet) ? primaryResultSet : [];
|
|
217
|
+
return resolve({
|
|
218
|
+
rows: primaryRows,
|
|
219
|
+
rowCount: primaryRows.length,
|
|
220
|
+
fields: [],
|
|
221
|
+
outputParams: outputParams && typeof outputParams === 'object' ? outputParams : {},
|
|
222
|
+
...(additionalResultSets.length > 0 ? { resultSets: additionalResultSets } : {}),
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
const [result] = rest;
|
|
226
|
+
if (Array.isArray(result)) {
|
|
227
|
+
return resolve({ rows: result, rowCount: result.length, fields: [] });
|
|
228
|
+
}
|
|
229
|
+
// DML statements resolve with an affected-row count (or `undefined`
|
|
230
|
+
// for DDL); a plain SELECT that happens to hit an empty result set
|
|
231
|
+
// still resolves with an array from hdb.
|
|
232
|
+
return resolve({
|
|
233
|
+
rows: [],
|
|
234
|
+
rowCount: typeof result === 'number' ? result : 0,
|
|
235
|
+
fields: [],
|
|
236
|
+
});
|
|
237
|
+
};
|
|
238
|
+
if (params.length > 0) {
|
|
239
|
+
client.prepare(sql, (err, statement) => {
|
|
240
|
+
if (err)
|
|
241
|
+
return reject(err);
|
|
242
|
+
statement.exec(params, handle);
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
else {
|
|
246
|
+
client.exec(sql, handle);
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Stream query results by paging through `sql` via repeated
|
|
252
|
+
* dialect-appropriate LIMIT/OFFSET queries (see
|
|
253
|
+
* `createPaginatedQueryStream()` in `src/dialects/query-stream-helper.ts`)
|
|
254
|
+
* instead of loading the whole result set into memory at once.
|
|
255
|
+
* @param sql - The SELECT statement to stream
|
|
256
|
+
* @param options - Streaming options (batch size, backpressure watermark, model mapping)
|
|
257
|
+
*/
|
|
258
|
+
queryStream(sql, options) {
|
|
259
|
+
return (0, query_stream_helper_1.createPaginatedQueryStream)(this, sql, options);
|
|
260
|
+
}
|
|
261
|
+
async query(sql, options) {
|
|
262
|
+
const bindValues = options?.replacements ?? options?.bindings ?? options?.bind ?? [];
|
|
263
|
+
const params = Array.isArray(bindValues) ? bindValues : Object.values(bindValues);
|
|
264
|
+
const tx = options?.transaction;
|
|
265
|
+
if (tx) {
|
|
266
|
+
if (tx.finished) {
|
|
267
|
+
throw new Error('Cannot execute query: the transaction has already been committed or rolled back.');
|
|
268
|
+
}
|
|
269
|
+
if (!tx.client) {
|
|
270
|
+
throw new Error('Cannot execute query: the transaction has no associated connection.');
|
|
271
|
+
}
|
|
272
|
+
return this.executeOnClient(tx.client, sql, params);
|
|
273
|
+
}
|
|
274
|
+
if (!this.client) {
|
|
275
|
+
throw new Error('Not connected to database. Call connect() first.');
|
|
276
|
+
}
|
|
277
|
+
return this.executeOnClient(this.client, sql, params);
|
|
278
|
+
}
|
|
279
|
+
escape(value) {
|
|
280
|
+
if (value instanceof prorm_1.Literal) {
|
|
281
|
+
return value.val;
|
|
282
|
+
}
|
|
283
|
+
if (value === null || value === undefined)
|
|
284
|
+
return 'NULL';
|
|
285
|
+
if (typeof value === 'boolean')
|
|
286
|
+
return value ? 'TRUE' : 'FALSE';
|
|
287
|
+
if (typeof value === 'number')
|
|
288
|
+
return String(value);
|
|
289
|
+
if (value instanceof Date)
|
|
290
|
+
return this.formatTimestamp(value);
|
|
291
|
+
if (Buffer.isBuffer(value))
|
|
292
|
+
return `X'${value.toString('hex')}'`;
|
|
293
|
+
if (Array.isArray(value) || typeof value === 'object') {
|
|
294
|
+
return `'${JSON.stringify(value).replace(/'/g, "''")}'`;
|
|
295
|
+
}
|
|
296
|
+
return `'${String(value).replace(/'/g, "''")}'`;
|
|
297
|
+
}
|
|
298
|
+
formatTimestamp(date) {
|
|
299
|
+
const pad = (n, len = 2) => String(n).padStart(len, '0');
|
|
300
|
+
const year = date.getFullYear();
|
|
301
|
+
const month = pad(date.getMonth() + 1);
|
|
302
|
+
const day = pad(date.getDate());
|
|
303
|
+
const hours = pad(date.getHours());
|
|
304
|
+
const minutes = pad(date.getMinutes());
|
|
305
|
+
const seconds = pad(date.getSeconds());
|
|
306
|
+
const millis = pad(date.getMilliseconds(), 3);
|
|
307
|
+
return `TIMESTAMP '${year}-${month}-${day} ${hours}:${minutes}:${seconds}.${millis}'`;
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* HANA uses double-quoted, case-sensitive delimited identifiers.
|
|
311
|
+
* Unquoted identifiers are folded to uppercase by HANA, so identifiers
|
|
312
|
+
* created via quoteIdentifier()/escapeId() must always be quoted the same
|
|
313
|
+
* way on every reference.
|
|
314
|
+
*/
|
|
315
|
+
escapeId(identifier) {
|
|
316
|
+
const id = String(identifier ?? '');
|
|
317
|
+
return `"${id.replace(/"/g, '""')}"`;
|
|
318
|
+
}
|
|
319
|
+
quoteIdentifier(identifier) {
|
|
320
|
+
return this.escapeId(identifier);
|
|
321
|
+
}
|
|
322
|
+
quoteTable(tableName, schema) {
|
|
323
|
+
const schemaName = schema || this.config.schema;
|
|
324
|
+
if (schemaName) {
|
|
325
|
+
return `${this.escapeId(schemaName)}.${this.escapeId(tableName)}`;
|
|
326
|
+
}
|
|
327
|
+
return this.escapeId(tableName);
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* Build a `SCHEMA_NAME` predicate for `SYS.*` catalog-view introspection
|
|
331
|
+
* queries. Without this, queries filtered only by e.g. `TABLE_NAME` match
|
|
332
|
+
* every same-named table across every schema on a multi-schema HANA
|
|
333
|
+
* system, silently merging/corrupting metadata. Scopes to the configured
|
|
334
|
+
* default schema when set, otherwise to the session's `CURRENT_SCHEMA`
|
|
335
|
+
* (mirrors the fallback `quoteTable()` uses for unqualified references).
|
|
336
|
+
*/
|
|
337
|
+
schemaFilterSql(schema) {
|
|
338
|
+
const schemaName = schema || this.config.schema;
|
|
339
|
+
return schemaName
|
|
340
|
+
? `AND SCHEMA_NAME = '${schemaName.toUpperCase()}'`
|
|
341
|
+
: 'AND SCHEMA_NAME = CURRENT_SCHEMA';
|
|
342
|
+
}
|
|
343
|
+
async getDatabaseVersion() {
|
|
344
|
+
const result = await this.query('SELECT VERSION FROM SYS.M_DATABASE');
|
|
345
|
+
return result.rows[0]?.VERSION || result.rows[0]?.version || 'unknown';
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* Parses `SYS.M_DATABASE.VERSION` to tell HANA Cloud apart from
|
|
349
|
+
* on-premise HANA. HANA Cloud is continuously delivered and reports a
|
|
350
|
+
* `4.x`-prefixed version (e.g. `4.00.000.00.1234567890`); on-premise HANA
|
|
351
|
+
* 2.0 SPS releases report `2.x` (e.g. `2.00.059.00.1234567890`, where the
|
|
352
|
+
* `059` third segment is the SPS/revision number) and legacy HANA 1.0
|
|
353
|
+
* reports `1.x`. This is purely additive metadata for callers that want
|
|
354
|
+
* to branch on edition-specific feature availability (e.g. Cloud-only
|
|
355
|
+
* features) - it doesn't change behavior of any existing method.
|
|
356
|
+
*/
|
|
357
|
+
async getEditionInfo() {
|
|
358
|
+
const version = await this.getDatabaseVersion();
|
|
359
|
+
const match = version.match(/^(\d+)\.(\d+)\.(\d+)/);
|
|
360
|
+
if (!match) {
|
|
361
|
+
return { edition: 'unknown', version };
|
|
362
|
+
}
|
|
363
|
+
const major = Number(match[1]);
|
|
364
|
+
const minor = Number(match[2]);
|
|
365
|
+
const revision = Number(match[3]);
|
|
366
|
+
const edition = major >= 4 ? 'cloud' : major >= 1 ? 'on-premise' : 'unknown';
|
|
367
|
+
return { edition, version, major, minor, revision };
|
|
368
|
+
}
|
|
369
|
+
/** Convenience wrapper over `getEditionInfo()` for the common case of a boolean check. */
|
|
370
|
+
async isCloudEdition() {
|
|
371
|
+
const { edition } = await this.getEditionInfo();
|
|
372
|
+
return edition === 'cloud';
|
|
373
|
+
}
|
|
374
|
+
// ==================== Schema Operations ====================
|
|
375
|
+
async createSchema(schema) {
|
|
376
|
+
await this.query(`CREATE SCHEMA ${this.escapeId(schema)}`);
|
|
377
|
+
}
|
|
378
|
+
async dropSchema(schema, options) {
|
|
379
|
+
if (options?.ifExists) {
|
|
380
|
+
const exists = await this.query(`SELECT SCHEMA_NAME FROM SYS.SCHEMAS WHERE SCHEMA_NAME = '${schema.toUpperCase()}'`);
|
|
381
|
+
if (!exists.rows || exists.rows.length === 0)
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
let sql = `DROP SCHEMA ${this.escapeId(schema)}`;
|
|
385
|
+
if (options?.cascade)
|
|
386
|
+
sql += ' CASCADE';
|
|
387
|
+
await this.query(sql);
|
|
388
|
+
}
|
|
389
|
+
async showAllSchemas() {
|
|
390
|
+
const result = await this.query('SELECT SCHEMA_NAME FROM SYS.SCHEMAS ORDER BY SCHEMA_NAME');
|
|
391
|
+
return result.rows.map((row) => row.SCHEMA_NAME || row.schema_name);
|
|
392
|
+
}
|
|
393
|
+
async listSchemas() {
|
|
394
|
+
return this.showAllSchemas();
|
|
395
|
+
}
|
|
396
|
+
createDatabaseSQL(options) {
|
|
397
|
+
// Tenant databases in a HANA MDC landscape are created via
|
|
398
|
+
// `CREATE DATABASE <name> SYSTEM USER PASSWORD "..."` issued against the
|
|
399
|
+
// SYSTEMDB, not a per-tenant SQL session - this returns the DDL shape for
|
|
400
|
+
// completeness, but most deployments provision tenants operationally
|
|
401
|
+
// rather than through application code.
|
|
402
|
+
return `CREATE DATABASE ${this.escapeId(options.name)} SYSTEM USER PASSWORD "Change_Me1"`;
|
|
403
|
+
}
|
|
404
|
+
dropDatabaseSQL(name) {
|
|
405
|
+
return `DROP DATABASE ${this.escapeId(name)}`;
|
|
406
|
+
}
|
|
407
|
+
createSavepointSQL(name) {
|
|
408
|
+
const savepointName = name || `sp_${Date.now()}`;
|
|
409
|
+
return `SAVEPOINT ${savepointName}`;
|
|
410
|
+
}
|
|
411
|
+
releaseSavepointSQL(name) {
|
|
412
|
+
return `RELEASE SAVEPOINT ${name}`;
|
|
413
|
+
}
|
|
414
|
+
rollbackToSavepointSQL(name) {
|
|
415
|
+
return `ROLLBACK TO SAVEPOINT ${name}`;
|
|
416
|
+
}
|
|
417
|
+
// ==================== Extensions (PostgreSQL-only, not supported) ====================
|
|
418
|
+
async createExtension() {
|
|
419
|
+
throw new Error('Extensions are a PostgreSQL-specific feature. HANA has no CREATE EXTENSION equivalent.');
|
|
420
|
+
}
|
|
421
|
+
async dropExtension() {
|
|
422
|
+
throw new Error('Extensions are a PostgreSQL-specific feature. HANA has no CREATE EXTENSION equivalent.');
|
|
423
|
+
}
|
|
424
|
+
async getExtensions() {
|
|
425
|
+
throw new Error('Extensions are a PostgreSQL-specific feature. HANA has no CREATE EXTENSION equivalent.');
|
|
426
|
+
}
|
|
427
|
+
async hasExtension() {
|
|
428
|
+
throw new Error('Extensions are a PostgreSQL-specific feature. HANA has no CREATE EXTENSION equivalent.');
|
|
429
|
+
}
|
|
430
|
+
// ==================== Table Operations ====================
|
|
431
|
+
buildColumnDefinition(columnName, definition) {
|
|
432
|
+
let sql = `${this.escapeId(columnName)} ${this.getDataTypeSql(definition.type)}`;
|
|
433
|
+
if (definition.autoIncrement) {
|
|
434
|
+
sql += ' GENERATED ALWAYS AS IDENTITY (START WITH 1 INCREMENT BY 1)';
|
|
435
|
+
}
|
|
436
|
+
if (definition.allowNull === false) {
|
|
437
|
+
sql += ' NOT NULL';
|
|
438
|
+
}
|
|
439
|
+
if (definition.defaultValue !== undefined && !definition.autoIncrement) {
|
|
440
|
+
if (definition.defaultValue === null) {
|
|
441
|
+
sql += ' DEFAULT NULL';
|
|
442
|
+
}
|
|
443
|
+
else if (definition.defaultValue instanceof prorm_1.Literal) {
|
|
444
|
+
sql += ` DEFAULT ${definition.defaultValue.val}`;
|
|
445
|
+
}
|
|
446
|
+
else {
|
|
447
|
+
sql += ` DEFAULT ${this.escape(definition.defaultValue)}`;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
if (definition.primaryKey) {
|
|
451
|
+
sql += ' PRIMARY KEY';
|
|
452
|
+
}
|
|
453
|
+
if (definition.unique) {
|
|
454
|
+
sql += ' UNIQUE';
|
|
455
|
+
}
|
|
456
|
+
if (definition.references) {
|
|
457
|
+
const refField = definition.references.field;
|
|
458
|
+
const refFieldSql = Array.isArray(refField)
|
|
459
|
+
? `(${refField.map((f) => this.escapeId(f)).join(', ')})`
|
|
460
|
+
: `(${this.escapeId(refField)})`;
|
|
461
|
+
sql += ` REFERENCES ${this.quoteTable(definition.references.table)}${refFieldSql}`;
|
|
462
|
+
if (definition.references.onDelete) {
|
|
463
|
+
sql += ` ON DELETE ${definition.references.onDelete.toUpperCase()}`;
|
|
464
|
+
}
|
|
465
|
+
if (definition.references.onUpdate) {
|
|
466
|
+
sql += ` ON UPDATE ${definition.references.onUpdate.toUpperCase()}`;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
return sql;
|
|
470
|
+
}
|
|
471
|
+
buildConstraintSql(constraint) {
|
|
472
|
+
const name = constraint.name ? `CONSTRAINT ${this.escapeId(constraint.name)} ` : '';
|
|
473
|
+
const fields = constraint.fields?.map((f) => this.escapeId(f)).join(', ') || '';
|
|
474
|
+
switch (constraint.type) {
|
|
475
|
+
case 'PRIMARY KEY':
|
|
476
|
+
return `${name}PRIMARY KEY (${fields})`;
|
|
477
|
+
case 'UNIQUE':
|
|
478
|
+
return `${name}UNIQUE (${fields})`;
|
|
479
|
+
case 'CHECK':
|
|
480
|
+
return `${name}CHECK (${constraint.check})`;
|
|
481
|
+
case 'FOREIGN KEY': {
|
|
482
|
+
if (!constraint.references)
|
|
483
|
+
return null;
|
|
484
|
+
const refField = constraint.references.field;
|
|
485
|
+
const refFieldSql = Array.isArray(refField)
|
|
486
|
+
? `(${refField.map((f) => this.escapeId(f)).join(', ')})`
|
|
487
|
+
: `(${this.escapeId(refField)})`;
|
|
488
|
+
let fkSql = `${name}FOREIGN KEY (${fields}) REFERENCES ${this.quoteTable(constraint.references.table)}${refFieldSql}`;
|
|
489
|
+
if (constraint.references.onDelete) {
|
|
490
|
+
fkSql += ` ON DELETE ${constraint.references.onDelete.toUpperCase()}`;
|
|
491
|
+
}
|
|
492
|
+
if (constraint.references.onUpdate) {
|
|
493
|
+
fkSql += ` ON UPDATE ${constraint.references.onUpdate.toUpperCase()}`;
|
|
494
|
+
}
|
|
495
|
+
return fkSql;
|
|
496
|
+
}
|
|
497
|
+
default:
|
|
498
|
+
return null;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
/**
|
|
502
|
+
* Normalizes `TableOptions.systemVersioning` - which may be a bare
|
|
503
|
+
* `boolean` (the shared shape also used by MariaDB/SQL Server-style
|
|
504
|
+
* dialects) or a HANA-specific `{ historyTable?, validFromColumn?,
|
|
505
|
+
* validToColumn? }` config object - down to `undefined` (versioning off)
|
|
506
|
+
* or a config object (versioning on, using period-column defaults when a
|
|
507
|
+
* bare `true` was passed).
|
|
508
|
+
*/
|
|
509
|
+
normalizeSystemVersioningOptions(sv) {
|
|
510
|
+
if (!sv)
|
|
511
|
+
return undefined;
|
|
512
|
+
return sv === true ? {} : sv;
|
|
513
|
+
}
|
|
514
|
+
/**
|
|
515
|
+
* Builds the three DDL fragments HANA needs for a system-versioned
|
|
516
|
+
* (temporal) table: the `GENERATED ALWAYS AS ROW START`/`ROW END` period
|
|
517
|
+
* columns and the `PERIOD FOR SYSTEM_TIME (...)` clause. Shared between
|
|
518
|
+
* `createTable` (inline, inside the column list) and
|
|
519
|
+
* `alterTableSystemVersioning` (issued as separate `ALTER TABLE`
|
|
520
|
+
* statements against an existing table).
|
|
521
|
+
*
|
|
522
|
+
* Only `TIMESTAMP` is a valid period-column type in HANA, and the
|
|
523
|
+
* precision is fixed at `TIMESTAMP(6)` (microseconds) to match SAP's own
|
|
524
|
+
* examples for `PERIOD FOR SYSTEM_TIME`.
|
|
525
|
+
*/
|
|
526
|
+
buildSystemVersioningPeriodSql(sv) {
|
|
527
|
+
const validFrom = sv.validFromColumn || 'valid_from';
|
|
528
|
+
const validTo = sv.validToColumn || 'valid_to';
|
|
529
|
+
return {
|
|
530
|
+
validFrom,
|
|
531
|
+
validTo,
|
|
532
|
+
rowStartColumnSql: `${this.escapeId(validFrom)} TIMESTAMP(6) NOT NULL GENERATED ALWAYS AS ROW START`,
|
|
533
|
+
rowEndColumnSql: `${this.escapeId(validTo)} TIMESTAMP(6) NOT NULL GENERATED ALWAYS AS ROW END`,
|
|
534
|
+
periodClauseSql: `PERIOD FOR SYSTEM_TIME (${this.escapeId(validFrom)}, ${this.escapeId(validTo)})`,
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
/**
|
|
538
|
+
* Create a table. HANA tables are column-store by default; pass
|
|
539
|
+
* `{ columnStore: false }` (an extra, HANA-specific option threaded
|
|
540
|
+
* through the shared `TableOptions` as an untyped property, the same
|
|
541
|
+
* pattern other dialect files use for their own extensions) to create a
|
|
542
|
+
* legacy row-store table instead via `CREATE ROW TABLE`.
|
|
543
|
+
*
|
|
544
|
+
* `options.systemVersioning` opts the table into HANA's system-versioned
|
|
545
|
+
* (temporal history) support: the row-start/row-end period columns and
|
|
546
|
+
* `PERIOD FOR SYSTEM_TIME (...)` clause are appended to the column list,
|
|
547
|
+
* and the statement is closed with `WITH SYSTEM VERSIONING [HISTORY TABLE
|
|
548
|
+
* ...]`, e.g.:
|
|
549
|
+
* CREATE COLUMN TABLE "t" (..., "valid_from" TIMESTAMP(6) NOT NULL
|
|
550
|
+
* GENERATED ALWAYS AS ROW START, "valid_to" TIMESTAMP(6) NOT NULL
|
|
551
|
+
* GENERATED ALWAYS AS ROW END, PERIOD FOR SYSTEM_TIME ("valid_from",
|
|
552
|
+
* "valid_to")) WITH SYSTEM VERSIONING HISTORY TABLE "t_history"
|
|
553
|
+
* When `historyTable` is omitted the `HISTORY TABLE` clause is left off
|
|
554
|
+
* entirely and HANA auto-generates (and manages) the history table itself.
|
|
555
|
+
* System-versioned tables are only supported on column-store tables, so
|
|
556
|
+
* this throws if combined with `{ columnStore: false }`.
|
|
557
|
+
*/
|
|
558
|
+
async createTable(tableName, columns, options) {
|
|
559
|
+
const columnDefs = [];
|
|
560
|
+
for (const [columnName, definition] of Object.entries(columns)) {
|
|
561
|
+
columnDefs.push(this.buildColumnDefinition(columnName, definition));
|
|
562
|
+
}
|
|
563
|
+
if (options?.constraints) {
|
|
564
|
+
for (const constraint of options.constraints) {
|
|
565
|
+
const constraintSql = this.buildConstraintSql(constraint);
|
|
566
|
+
if (constraintSql)
|
|
567
|
+
columnDefs.push(constraintSql);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
const columnStore = options?.columnStore !== false;
|
|
571
|
+
const tableKind = columnStore ? 'COLUMN' : 'ROW';
|
|
572
|
+
const systemVersioning = this.normalizeSystemVersioningOptions(options?.systemVersioning);
|
|
573
|
+
if (systemVersioning && !columnStore) {
|
|
574
|
+
throw new Error('HANA system-versioned tables require a column-store table (columnStore: false was set)');
|
|
575
|
+
}
|
|
576
|
+
if (systemVersioning) {
|
|
577
|
+
const { rowStartColumnSql, rowEndColumnSql, periodClauseSql } = this.buildSystemVersioningPeriodSql(systemVersioning);
|
|
578
|
+
columnDefs.push(rowStartColumnSql, rowEndColumnSql, periodClauseSql);
|
|
579
|
+
}
|
|
580
|
+
let sql = `CREATE ${tableKind} TABLE ${this.quoteTable(tableName)} (${columnDefs.join(', ')})`;
|
|
581
|
+
if (systemVersioning) {
|
|
582
|
+
sql += ' WITH SYSTEM VERSIONING';
|
|
583
|
+
if (systemVersioning.historyTable) {
|
|
584
|
+
sql += ` HISTORY TABLE ${this.quoteTable(systemVersioning.historyTable)}`;
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
await this.query(sql);
|
|
588
|
+
if (options?.indexes) {
|
|
589
|
+
for (const index of options.indexes) {
|
|
590
|
+
await this.createIndex(tableName, {
|
|
591
|
+
name: index.name || `${tableName}_${index.fields.join('_')}_idx`,
|
|
592
|
+
fields: index.fields,
|
|
593
|
+
unique: index.unique,
|
|
594
|
+
type: index.type,
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
/**
|
|
600
|
+
* Enables system-versioning (temporal history) on an *already-existing*
|
|
601
|
+
* table. Unlike `CREATE TABLE ... WITH SYSTEM VERSIONING`, HANA has no
|
|
602
|
+
* single-statement form for retrofitting an existing table - the
|
|
603
|
+
* row-start/row-end period columns, the `PERIOD FOR SYSTEM_TIME` clause,
|
|
604
|
+
* and the `SYSTEM VERSIONING` clause itself must each be added via their
|
|
605
|
+
* own `ALTER TABLE` statement, in this order:
|
|
606
|
+
* ALTER TABLE "t" ADD ("valid_from" TIMESTAMP(6) NOT NULL GENERATED ALWAYS AS ROW START)
|
|
607
|
+
* ALTER TABLE "t" ADD ("valid_to" TIMESTAMP(6) NOT NULL GENERATED ALWAYS AS ROW END)
|
|
608
|
+
* ALTER TABLE "t" ADD PERIOD FOR SYSTEM_TIME ("valid_from", "valid_to")
|
|
609
|
+
* ALTER TABLE "t" ADD SYSTEM VERSIONING [HISTORY TABLE "t_history"]
|
|
610
|
+
* As with `createTable`, an omitted `historyTable` leaves the `HISTORY
|
|
611
|
+
* TABLE` clause off and lets HANA auto-generate/manage the history table.
|
|
612
|
+
*/
|
|
613
|
+
async alterTableSystemVersioning(tableName, options) {
|
|
614
|
+
const sv = options || {};
|
|
615
|
+
const { rowStartColumnSql, rowEndColumnSql, periodClauseSql } = this.buildSystemVersioningPeriodSql(sv);
|
|
616
|
+
const table = this.quoteTable(tableName);
|
|
617
|
+
await this.query(`ALTER TABLE ${table} ADD (${rowStartColumnSql})`);
|
|
618
|
+
await this.query(`ALTER TABLE ${table} ADD (${rowEndColumnSql})`);
|
|
619
|
+
await this.query(`ALTER TABLE ${table} ADD ${periodClauseSql}`);
|
|
620
|
+
let sql = `ALTER TABLE ${table} ADD SYSTEM VERSIONING`;
|
|
621
|
+
if (sv.historyTable) {
|
|
622
|
+
sql += ` HISTORY TABLE ${this.quoteTable(sv.historyTable)}`;
|
|
623
|
+
}
|
|
624
|
+
await this.query(sql);
|
|
625
|
+
}
|
|
626
|
+
async dropTable(tableName, options) {
|
|
627
|
+
if (options?.ifExists) {
|
|
628
|
+
const exists = await this.query(`SELECT TABLE_NAME FROM SYS.TABLES WHERE TABLE_NAME = '${tableName.toUpperCase()}' ${this.schemaFilterSql()}`);
|
|
629
|
+
if (!exists.rows || exists.rows.length === 0)
|
|
630
|
+
return;
|
|
631
|
+
}
|
|
632
|
+
let sql = `DROP TABLE ${this.quoteTable(tableName)}`;
|
|
633
|
+
if (options?.cascade)
|
|
634
|
+
sql += ' CASCADE';
|
|
635
|
+
await this.query(sql);
|
|
636
|
+
}
|
|
637
|
+
/**
|
|
638
|
+
* HANA range/hash partitioning is expressed via a `PARTITION BY` clause on
|
|
639
|
+
* the `CREATE (COLUMN|ROW) TABLE` statement itself. HANA also supports
|
|
640
|
+
* `ROUNDROBIN` partitioning, which isn't modeled by the shared
|
|
641
|
+
* `partitionBy.type` union.
|
|
642
|
+
*/
|
|
643
|
+
async createPartitionedTable(tableName, columns, options) {
|
|
644
|
+
if (!options?.partitionBy || (options.partitionBy.type !== 'range' && options.partitionBy.type !== 'hash')) {
|
|
645
|
+
throw new Error('HANA supports RANGE, HASH and ROUNDROBIN partitioning; only "range" and "hash" are modeled by createPartitionedTable (use raw SQL via query() for ROUNDROBIN).');
|
|
646
|
+
}
|
|
647
|
+
const columnDefs = Object.entries(columns).map(([columnName, definition]) => this.buildColumnDefinition(columnName, definition));
|
|
648
|
+
const columnStore = options?.columnStore !== false;
|
|
649
|
+
const tableKind = columnStore ? 'COLUMN' : 'ROW';
|
|
650
|
+
const partitionColumns = Array.isArray(options.partitionBy.column)
|
|
651
|
+
? options.partitionBy.column.map((c) => this.escapeId(c)).join(', ')
|
|
652
|
+
: this.escapeId(options.partitionBy.column);
|
|
653
|
+
let partitionClause;
|
|
654
|
+
if (options.partitionBy.type === 'hash') {
|
|
655
|
+
const count = (options.partitions || []).length || 4;
|
|
656
|
+
partitionClause = `PARTITION BY HASH (${partitionColumns}) PARTITIONS ${count}`;
|
|
657
|
+
}
|
|
658
|
+
else {
|
|
659
|
+
const parts = (options.partitions || []).map((part) => {
|
|
660
|
+
if (part.bound?.to !== undefined) {
|
|
661
|
+
return `PARTITION ${this.formatPartitionValue(part.bound.to)}`;
|
|
662
|
+
}
|
|
663
|
+
return 'PARTITION OTHERS';
|
|
664
|
+
});
|
|
665
|
+
partitionClause = `PARTITION BY RANGE (${partitionColumns}) (${parts.join(', ')})`;
|
|
666
|
+
}
|
|
667
|
+
const sql = `CREATE ${tableKind} TABLE ${this.quoteTable(tableName)} (${columnDefs.join(', ')}) ${partitionClause}`;
|
|
668
|
+
await this.query(sql);
|
|
669
|
+
}
|
|
670
|
+
formatPartitionValue(value) {
|
|
671
|
+
if (value instanceof Date)
|
|
672
|
+
return `VALUE <= ${this.escape(value)}`;
|
|
673
|
+
if (typeof value === 'number')
|
|
674
|
+
return `VALUE <= ${value}`;
|
|
675
|
+
return `VALUE <= ${this.escape(value)}`;
|
|
676
|
+
}
|
|
677
|
+
async createPartition(options) {
|
|
678
|
+
const bound = options.bound && 'to' in options.bound ? this.formatPartitionValue(options.bound.to) : 'OTHERS';
|
|
679
|
+
await this.query(`ALTER TABLE ${this.quoteTable(options.parentTable)} ADD PARTITION ${bound}`);
|
|
680
|
+
}
|
|
681
|
+
async attachPartition(_options) {
|
|
682
|
+
throw new Error('HANA does not support attaching a standalone table as a partition; use ALTER TABLE ... ADD PARTITION instead.');
|
|
683
|
+
}
|
|
684
|
+
async detachPartition(_options) {
|
|
685
|
+
throw new Error('HANA does not support detaching a partition into a standalone table; use ALTER TABLE ... DROP PARTITION or MERGE PARTITION instead.');
|
|
686
|
+
}
|
|
687
|
+
async dropPartition(partitionName) {
|
|
688
|
+
await this.query(`ALTER TABLE ${this.quoteTable(partitionName)} MERGE PARTITIONS`);
|
|
689
|
+
}
|
|
690
|
+
/**
|
|
691
|
+
* Bulk insert records. HANA supports multi-row `VALUES (...), (...), ...`
|
|
692
|
+
* in a single `INSERT INTO` statement (as does most SQL-2003-compliant
|
|
693
|
+
* engines), so all rows are sent in one statement.
|
|
694
|
+
*/
|
|
695
|
+
async bulkInsert(tableName, records, options) {
|
|
696
|
+
if (records.length === 0) {
|
|
697
|
+
return { rows: [], rowCount: 0, fields: [] };
|
|
698
|
+
}
|
|
699
|
+
const columns = Object.keys(records[0]);
|
|
700
|
+
const values = [];
|
|
701
|
+
const rowPlaceholders = [];
|
|
702
|
+
for (const record of records) {
|
|
703
|
+
const placeholders = [];
|
|
704
|
+
for (const column of columns) {
|
|
705
|
+
placeholders.push('?');
|
|
706
|
+
values.push(record[column]);
|
|
707
|
+
}
|
|
708
|
+
rowPlaceholders.push(`(${placeholders.join(', ')})`);
|
|
709
|
+
}
|
|
710
|
+
const sql = `INSERT INTO ${this.quoteTable(tableName, options?.schema)} (${columns.map((c) => this.escapeId(c)).join(', ')}) VALUES ${rowPlaceholders.join(', ')}`;
|
|
711
|
+
return this.query(sql, { replacements: values });
|
|
712
|
+
}
|
|
713
|
+
// ==================== View Operations ====================
|
|
714
|
+
async createView(viewName, query, options) {
|
|
715
|
+
if (options?.replace) {
|
|
716
|
+
await this.query(`DROP VIEW ${this.escapeId(viewName)}`).catch(() => undefined);
|
|
717
|
+
}
|
|
718
|
+
await this.query(`CREATE VIEW ${this.escapeId(viewName)} AS ${query}`);
|
|
719
|
+
}
|
|
720
|
+
async dropView(viewName, options) {
|
|
721
|
+
if (options?.ifExists) {
|
|
722
|
+
const exists = await this.query(`SELECT VIEW_NAME FROM SYS.VIEWS WHERE VIEW_NAME = '${viewName.toUpperCase()}' ${this.schemaFilterSql()}`);
|
|
723
|
+
if (!exists.rows || exists.rows.length === 0)
|
|
724
|
+
return;
|
|
725
|
+
}
|
|
726
|
+
await this.query(`DROP VIEW ${this.escapeId(viewName)}`);
|
|
727
|
+
}
|
|
728
|
+
async showViews() {
|
|
729
|
+
const result = await this.query('SELECT VIEW_NAME FROM SYS.VIEWS ORDER BY VIEW_NAME');
|
|
730
|
+
return result.rows.map((row) => row.VIEW_NAME || row.view_name);
|
|
731
|
+
}
|
|
732
|
+
// HANA has no `CREATE MATERIALIZED VIEW` SQL statement - the closest
|
|
733
|
+
// concepts are Calculation Views (modeled objects, out of scope for this
|
|
734
|
+
// ORM) or a manually-maintained "snapshot" table populated by a
|
|
735
|
+
// stored-procedure-driven refresh job. These throw rather than emit
|
|
736
|
+
// invalid SQL.
|
|
737
|
+
async createMaterializedView(_options) {
|
|
738
|
+
throw new Error('HANA has no CREATE MATERIALIZED VIEW statement. Use a Calculation View or a manually-refreshed snapshot table instead.');
|
|
739
|
+
}
|
|
740
|
+
async refreshMaterializedView(_viewName, _options) {
|
|
741
|
+
throw new Error('HANA has no materialized views to refresh.');
|
|
742
|
+
}
|
|
743
|
+
async dropMaterializedView(_viewName, _options) {
|
|
744
|
+
throw new Error('HANA has no materialized views to drop.');
|
|
745
|
+
}
|
|
746
|
+
async hasMaterializedView(_viewName) {
|
|
747
|
+
return false;
|
|
748
|
+
}
|
|
749
|
+
async showMaterializedViews() {
|
|
750
|
+
return [];
|
|
751
|
+
}
|
|
752
|
+
// ==================== Column Operations ====================
|
|
753
|
+
// HANA's ALTER TABLE column DDL is parenthesized and can batch multiple
|
|
754
|
+
// columns in one clause - distinct from the single-column-at-a-time ALTER
|
|
755
|
+
// COLUMN syntax used by most other dialects.
|
|
756
|
+
async addColumn(tableName, columnName, definition) {
|
|
757
|
+
await this.query(`ALTER TABLE ${this.quoteTable(tableName)} ADD (${this.buildColumnDefinition(columnName, definition)})`);
|
|
758
|
+
}
|
|
759
|
+
async removeColumn(tableName, columnName) {
|
|
760
|
+
await this.query(`ALTER TABLE ${this.quoteTable(tableName)} DROP (${this.escapeId(columnName)})`);
|
|
761
|
+
}
|
|
762
|
+
async changeColumn(tableName, columnName, definition) {
|
|
763
|
+
let clause = `${this.escapeId(columnName)} ${this.getDataTypeSql(definition.type)}`;
|
|
764
|
+
if (definition.defaultValue !== undefined) {
|
|
765
|
+
clause += ` DEFAULT ${definition.defaultValue === null ? 'NULL' : this.escape(definition.defaultValue)}`;
|
|
766
|
+
}
|
|
767
|
+
if (definition.allowNull === false) {
|
|
768
|
+
clause += ' NOT NULL';
|
|
769
|
+
}
|
|
770
|
+
await this.query(`ALTER TABLE ${this.quoteTable(tableName)} ALTER (${clause})`);
|
|
771
|
+
}
|
|
772
|
+
/** HANA's dedicated column-rename statement: `RENAME COLUMN t.old TO new`. */
|
|
773
|
+
async renameColumn(tableName, oldColumnName, newColumnName) {
|
|
774
|
+
await this.query(`RENAME COLUMN ${this.quoteTable(tableName)}.${this.escapeId(oldColumnName)} TO ${this.escapeId(newColumnName)}`);
|
|
775
|
+
}
|
|
776
|
+
async addForeignKey(tableName, columnName, referencedTableName, referencedColumnName, options) {
|
|
777
|
+
const constraintName = options?.name
|
|
778
|
+
? this.escapeId(options.name)
|
|
779
|
+
: this.escapeId(`fk_${tableName}_${columnName}`);
|
|
780
|
+
let sql = `ALTER TABLE ${this.quoteTable(tableName)} ADD CONSTRAINT ${constraintName} FOREIGN KEY (${this.escapeId(columnName)}) REFERENCES ${this.quoteTable(referencedTableName)} (${this.escapeId(referencedColumnName)})`;
|
|
781
|
+
if (options?.onDelete)
|
|
782
|
+
sql += ` ON DELETE ${options.onDelete.toUpperCase()}`;
|
|
783
|
+
if (options?.onUpdate)
|
|
784
|
+
sql += ` ON UPDATE ${options.onUpdate.toUpperCase()}`;
|
|
785
|
+
await this.query(sql);
|
|
786
|
+
}
|
|
787
|
+
async describeTable(tableName) {
|
|
788
|
+
const result = await this.query(`SELECT COLUMN_NAME, DATA_TYPE_NAME, IS_NULLABLE, DEFAULT_VALUE, GENERATED_ALWAYS_AS_IDENTITY
|
|
789
|
+
FROM SYS.TABLE_COLUMNS
|
|
790
|
+
WHERE TABLE_NAME = '${tableName.toUpperCase()}' ${this.schemaFilterSql()}
|
|
791
|
+
ORDER BY POSITION`);
|
|
792
|
+
const pkResult = await this.query(`SELECT COLUMN_NAME FROM SYS.CONSTRAINTS WHERE TABLE_NAME = '${tableName.toUpperCase()}' AND IS_PRIMARY_KEY = 'TRUE' ${this.schemaFilterSql()}`);
|
|
793
|
+
const pkColumns = new Set(pkResult.rows.map((row) => (row.COLUMN_NAME || row.column_name || '').toUpperCase()));
|
|
794
|
+
const description = {};
|
|
795
|
+
for (const row of result.rows) {
|
|
796
|
+
const rawName = row.COLUMN_NAME || row.column_name || '';
|
|
797
|
+
const colName = rawName.toLowerCase();
|
|
798
|
+
description[colName] = {
|
|
799
|
+
type: row.DATA_TYPE_NAME || row.data_type_name,
|
|
800
|
+
allowNull: (row.IS_NULLABLE ?? row.is_nullable) === 'TRUE',
|
|
801
|
+
defaultValue: row.DEFAULT_VALUE ?? row.default_value ?? null,
|
|
802
|
+
primaryKey: pkColumns.has(rawName.toUpperCase()),
|
|
803
|
+
autoIncrement: (row.GENERATED_ALWAYS_AS_IDENTITY ?? row.generated_always_as_identity) === 'TRUE',
|
|
804
|
+
};
|
|
805
|
+
}
|
|
806
|
+
return description;
|
|
807
|
+
}
|
|
808
|
+
async renameTable(oldName, newName) {
|
|
809
|
+
await this.query(`RENAME TABLE ${this.quoteTable(oldName)} TO ${this.escapeId(newName)}`);
|
|
810
|
+
}
|
|
811
|
+
async showTables() {
|
|
812
|
+
const result = await this.query(`SELECT TABLE_NAME FROM SYS.TABLES WHERE 1 = 1 ${this.schemaFilterSql()} ORDER BY TABLE_NAME`);
|
|
813
|
+
return result.rows.map((row) => row.TABLE_NAME || row.table_name);
|
|
814
|
+
}
|
|
815
|
+
async showConstraints(tableName) {
|
|
816
|
+
const result = await this.query(`SELECT CONSTRAINT_NAME AS name, TABLE_NAME AS tableName, IS_PRIMARY_KEY AS isPrimaryKey, IS_UNIQUE_KEY AS isUniqueKey
|
|
817
|
+
FROM SYS.CONSTRAINTS
|
|
818
|
+
WHERE TABLE_NAME = '${tableName.toUpperCase()}' ${this.schemaFilterSql()}
|
|
819
|
+
ORDER BY CONSTRAINT_NAME`);
|
|
820
|
+
return result.rows;
|
|
821
|
+
}
|
|
822
|
+
async addConstraint(tableName, options) {
|
|
823
|
+
await this.createConstraint(tableName, {
|
|
824
|
+
name: options.name || `${tableName}_${options.type.toLowerCase().replace(/\s+/g, '_')}`,
|
|
825
|
+
type: options.type,
|
|
826
|
+
fields: options.fields,
|
|
827
|
+
references: options.references
|
|
828
|
+
? { table: options.references.table, field: options.references.fields }
|
|
829
|
+
: undefined,
|
|
830
|
+
check: options.check,
|
|
831
|
+
});
|
|
832
|
+
}
|
|
833
|
+
async removeConstraint(tableName, constraintName) {
|
|
834
|
+
await this.dropConstraint(tableName, constraintName);
|
|
835
|
+
}
|
|
836
|
+
async showIndexes(tableName) {
|
|
837
|
+
const result = await this.query(`SELECT INDEX_NAME AS name, TABLE_NAME AS tableName, CONSTRAINT AS constraintType
|
|
838
|
+
FROM SYS.INDEXES
|
|
839
|
+
WHERE TABLE_NAME = '${tableName.toUpperCase()}' ${this.schemaFilterSql()}
|
|
840
|
+
ORDER BY INDEX_NAME`);
|
|
841
|
+
return result.rows;
|
|
842
|
+
}
|
|
843
|
+
// ==================== Index Operations ====================
|
|
844
|
+
buildCreateIndexSql(tableName, indexName, fields, options) {
|
|
845
|
+
const fieldsSql = (fields || []).map((f) => this.escapeId(f)).join(', ');
|
|
846
|
+
let sql = 'CREATE';
|
|
847
|
+
if (options?.unique)
|
|
848
|
+
sql += ' UNIQUE';
|
|
849
|
+
sql += ` INDEX ${this.escapeId(indexName)} ON ${this.quoteTable(tableName)} (${fieldsSql})`;
|
|
850
|
+
return sql;
|
|
851
|
+
}
|
|
852
|
+
async addIndex(tableName, indexName, fields, options) {
|
|
853
|
+
await this.query(this.buildCreateIndexSql(tableName, indexName, fields, options));
|
|
854
|
+
}
|
|
855
|
+
async removeIndex(_tableName, indexName) {
|
|
856
|
+
await this.query(`DROP INDEX ${this.escapeId(indexName)}`);
|
|
857
|
+
}
|
|
858
|
+
async createIndex(tableName, indexDef) {
|
|
859
|
+
await this.addIndex(tableName, indexDef.name, indexDef.fields, {
|
|
860
|
+
unique: indexDef.unique,
|
|
861
|
+
type: indexDef.type,
|
|
862
|
+
using: indexDef.using,
|
|
863
|
+
});
|
|
864
|
+
}
|
|
865
|
+
async dropIndex(tableName, indexName, _options) {
|
|
866
|
+
await this.removeIndex(tableName, indexName);
|
|
867
|
+
}
|
|
868
|
+
async createFulltextIndex(tableName, indexName, fields, _options) {
|
|
869
|
+
// HANA's full-text search is implemented via CREATE FULLTEXT INDEX,
|
|
870
|
+
// backed by the Full-Text Search Engine, and operates on a single
|
|
871
|
+
// NVARCHAR/NCLOB column at a time.
|
|
872
|
+
const field = fields[0];
|
|
873
|
+
await this.query(`CREATE FULLTEXT INDEX ${this.escapeId(indexName)} ON ${this.quoteTable(tableName)} (${this.escapeId(field)})`);
|
|
874
|
+
}
|
|
875
|
+
async createSpatialIndex(tableName, indexName, fields, _options) {
|
|
876
|
+
// HANA spatial indexes are created on an ST_GEOMETRY/ST_POINT column.
|
|
877
|
+
const field = fields[0];
|
|
878
|
+
await this.query(`CREATE SPATIAL INDEX ${this.escapeId(indexName)} ON ${this.quoteTable(tableName)} (${this.escapeId(field)})`);
|
|
879
|
+
}
|
|
880
|
+
async createPartialIndex() {
|
|
881
|
+
throw new Error('HANA does not support partial (filtered) indexes with a WHERE clause.');
|
|
882
|
+
}
|
|
883
|
+
async createExpressionIndex(tableName, indexName, expression, options) {
|
|
884
|
+
let sql = 'CREATE';
|
|
885
|
+
if (options?.unique)
|
|
886
|
+
sql += ' UNIQUE';
|
|
887
|
+
sql += ` INDEX ${this.escapeId(indexName)} ON ${this.quoteTable(tableName)} (${expression})`;
|
|
888
|
+
await this.query(sql);
|
|
889
|
+
}
|
|
890
|
+
// ==================== Constraint Operations ====================
|
|
891
|
+
async createConstraint(tableName, constraintDef) {
|
|
892
|
+
const constraintSql = this.buildConstraintSql({
|
|
893
|
+
name: constraintDef.name,
|
|
894
|
+
type: constraintDef.type,
|
|
895
|
+
fields: constraintDef.fields,
|
|
896
|
+
references: constraintDef.references
|
|
897
|
+
? {
|
|
898
|
+
table: constraintDef.references.table,
|
|
899
|
+
field: constraintDef.references.field,
|
|
900
|
+
onDelete: constraintDef.references.onDelete,
|
|
901
|
+
onUpdate: constraintDef.references.onUpdate,
|
|
902
|
+
}
|
|
903
|
+
: undefined,
|
|
904
|
+
check: constraintDef.check,
|
|
905
|
+
});
|
|
906
|
+
await this.query(`ALTER TABLE ${this.quoteTable(tableName)} ADD ${constraintSql}`);
|
|
907
|
+
}
|
|
908
|
+
async dropConstraint(tableName, constraintName, _options) {
|
|
909
|
+
await this.query(`ALTER TABLE ${this.quoteTable(tableName)} DROP CONSTRAINT ${this.escapeId(constraintName)}`);
|
|
910
|
+
}
|
|
911
|
+
async changeOwner() {
|
|
912
|
+
throw new Error('HANA has no ALTER ... OWNER TO equivalent; ownership is managed via object privileges/roles instead.');
|
|
913
|
+
}
|
|
914
|
+
// ==================== Federation (Smart Data Access - out of scope) ====================
|
|
915
|
+
// HANA's real federation feature is Smart Data Access: `CREATE REMOTE
|
|
916
|
+
// SOURCE` registers a remote data source/adapter, and `CREATE VIRTUAL
|
|
917
|
+
// TABLE` maps a local object onto a remote table through it - conceptually
|
|
918
|
+
// similar to Postgres' FDW/foreign-server/foreign-table trio, but not a
|
|
919
|
+
// clean 1:1 mapping onto the generic (Postgres-shaped) FDW interface
|
|
920
|
+
// threaded through the shared Dialect type. These throw with a pointer to
|
|
921
|
+
// the real feature rather than emitting invalid SQL.
|
|
922
|
+
async createForeignDataWrapper() {
|
|
923
|
+
throw new Error('HANA has no FDW concept; use Smart Data Access (CREATE REMOTE SOURCE / CREATE VIRTUAL TABLE) instead.');
|
|
924
|
+
}
|
|
925
|
+
async dropForeignDataWrapper() {
|
|
926
|
+
throw new Error('HANA has no FDW concept; use Smart Data Access (CREATE REMOTE SOURCE / CREATE VIRTUAL TABLE) instead.');
|
|
927
|
+
}
|
|
928
|
+
async createForeignServer() {
|
|
929
|
+
throw new Error('HANA has no CREATE SERVER equivalent; use Smart Data Access (CREATE REMOTE SOURCE) instead.');
|
|
930
|
+
}
|
|
931
|
+
async dropForeignServer() {
|
|
932
|
+
throw new Error('HANA has no DROP SERVER equivalent; use Smart Data Access (DROP REMOTE SOURCE) instead.');
|
|
933
|
+
}
|
|
934
|
+
async createForeignTable() {
|
|
935
|
+
throw new Error('HANA has no CREATE FOREIGN TABLE equivalent; use Smart Data Access (CREATE VIRTUAL TABLE) instead.');
|
|
936
|
+
}
|
|
937
|
+
buildCreateServerQuery(_name, _opts) {
|
|
938
|
+
throw new Error('HANA has no FDW concept; use Smart Data Access (CREATE REMOTE SOURCE) instead.');
|
|
939
|
+
}
|
|
940
|
+
buildAlterServerQuery(_name, _opts) {
|
|
941
|
+
throw new Error('HANA has no FDW concept; use Smart Data Access (ALTER REMOTE SOURCE) instead.');
|
|
942
|
+
}
|
|
943
|
+
buildDropServerQuery(_name) {
|
|
944
|
+
throw new Error('HANA has no FDW concept; use Smart Data Access (DROP REMOTE SOURCE) instead.');
|
|
945
|
+
}
|
|
946
|
+
buildCreateUserMappingQuery(_opts) {
|
|
947
|
+
throw new Error('HANA has no user-mapping concept; Smart Data Access credentials are configured on the remote source itself.');
|
|
948
|
+
}
|
|
949
|
+
buildAlterUserMappingQuery(_opts) {
|
|
950
|
+
throw new Error('HANA has no user-mapping concept; Smart Data Access credentials are configured on the remote source itself.');
|
|
951
|
+
}
|
|
952
|
+
buildDropUserMappingQuery(_serverName, _user) {
|
|
953
|
+
throw new Error('HANA has no user-mapping concept; Smart Data Access credentials are configured on the remote source itself.');
|
|
954
|
+
}
|
|
955
|
+
async createUserMapping() {
|
|
956
|
+
throw new Error('HANA has no user-mapping concept; Smart Data Access credentials are configured on the remote source itself.');
|
|
957
|
+
}
|
|
958
|
+
async dropUserMapping() {
|
|
959
|
+
throw new Error('HANA has no user-mapping concept; Smart Data Access credentials are configured on the remote source itself.');
|
|
960
|
+
}
|
|
961
|
+
buildCreateForeignTableQuery(_tableName, _opts) {
|
|
962
|
+
throw new Error('HANA has no CREATE FOREIGN TABLE equivalent; use Smart Data Access (CREATE VIRTUAL TABLE) instead.');
|
|
963
|
+
}
|
|
964
|
+
buildDropForeignTableQuery(_tableName) {
|
|
965
|
+
throw new Error('HANA has no DROP FOREIGN TABLE equivalent; use Smart Data Access (DROP VIRTUAL TABLE) instead.');
|
|
966
|
+
}
|
|
967
|
+
buildImportForeignSchemaQuery(_remoteSchema, _serverName, _opts) {
|
|
968
|
+
throw new Error('HANA has no IMPORT FOREIGN SCHEMA equivalent; virtual tables must be created individually.');
|
|
969
|
+
}
|
|
970
|
+
getServersQuery() {
|
|
971
|
+
// Smart Data Access remote sources, for reference/parity even though
|
|
972
|
+
// the FDW-shaped methods above throw.
|
|
973
|
+
return 'SELECT REMOTE_SOURCE_NAME, ADAPTER_NAME FROM SYS.REMOTE_SOURCES ORDER BY REMOTE_SOURCE_NAME';
|
|
974
|
+
}
|
|
975
|
+
// ==================== Security Policies (Row-level security) ====================
|
|
976
|
+
// HANA has no simple predicate-based row-level-security policy statement
|
|
977
|
+
// like MSSQL/Postgres RLS; row-level restrictions are implemented via
|
|
978
|
+
// Structured Privileges / Analytic Privileges, which are administrative
|
|
979
|
+
// objects rather than plain SQL DDL suitable for this generic interface.
|
|
980
|
+
async createSecurityPolicy() {
|
|
981
|
+
throw new Error('HANA has no CREATE SECURITY POLICY equivalent; use Structured/Analytic Privileges instead.');
|
|
982
|
+
}
|
|
983
|
+
async dropSecurityPolicy() {
|
|
984
|
+
throw new Error('HANA has no CREATE SECURITY POLICY equivalent; use Structured/Analytic Privileges instead.');
|
|
985
|
+
}
|
|
986
|
+
// ==================== Transaction Operations ====================
|
|
987
|
+
async startTransaction(options) {
|
|
988
|
+
const client = this.createClient();
|
|
989
|
+
await this.connectClient(client);
|
|
990
|
+
client.setAutoCommit(false);
|
|
991
|
+
if (options?.isolationLevel) {
|
|
992
|
+
// HANA sets the isolation level for the *next* transaction via
|
|
993
|
+
// `SET TRANSACTION ISOLATION LEVEL ...` on the session, executed here
|
|
994
|
+
// before any statements run on the dedicated transaction connection.
|
|
995
|
+
await this.executeOnClient(client, `SET TRANSACTION ISOLATION LEVEL ${options.isolationLevel}`, []);
|
|
996
|
+
}
|
|
997
|
+
return new HanaTransaction(client, options);
|
|
998
|
+
}
|
|
999
|
+
async commitTransaction(transaction) {
|
|
1000
|
+
await transaction.commit();
|
|
1001
|
+
}
|
|
1002
|
+
async rollbackTransaction(transaction) {
|
|
1003
|
+
await transaction.rollback();
|
|
1004
|
+
}
|
|
1005
|
+
// ==================== Data Type Mapping ====================
|
|
1006
|
+
getDataTypeSql(dataType) {
|
|
1007
|
+
if (typeof dataType === 'string') {
|
|
1008
|
+
return this.mapDataType(dataType, undefined);
|
|
1009
|
+
}
|
|
1010
|
+
const dt = dataType;
|
|
1011
|
+
return this.mapDataType(String(dt.key ?? dataType), dt);
|
|
1012
|
+
}
|
|
1013
|
+
mapDataType(typeKey, dt) {
|
|
1014
|
+
switch (typeKey.toUpperCase()) {
|
|
1015
|
+
case 'STRING':
|
|
1016
|
+
if (dt?.length === 'max')
|
|
1017
|
+
return 'NCLOB';
|
|
1018
|
+
return `NVARCHAR(${String(dt?.length || 255)})`;
|
|
1019
|
+
case 'NVARCHAR':
|
|
1020
|
+
return `NVARCHAR(${String(dt?.length || 255)})`;
|
|
1021
|
+
case 'CHAR':
|
|
1022
|
+
return `NCHAR(${String(dt?.length || 1)})`;
|
|
1023
|
+
case 'SHORTTEXT':
|
|
1024
|
+
return `SHORTTEXT(${String(dt?.length || 255)})`;
|
|
1025
|
+
case 'ALPHANUM':
|
|
1026
|
+
return `ALPHANUM(${String(dt?.length || 10)})`;
|
|
1027
|
+
case 'TEXT':
|
|
1028
|
+
return 'NCLOB';
|
|
1029
|
+
case 'TINYTEXT':
|
|
1030
|
+
return 'NVARCHAR(255)';
|
|
1031
|
+
case 'MEDIUMTEXT':
|
|
1032
|
+
case 'LONGTEXT':
|
|
1033
|
+
return 'NCLOB';
|
|
1034
|
+
case 'INTEGER': {
|
|
1035
|
+
if (dt?.length === 1)
|
|
1036
|
+
return 'TINYINT';
|
|
1037
|
+
if (dt?.length === 2)
|
|
1038
|
+
return 'SMALLINT';
|
|
1039
|
+
if (dt?.length === 8)
|
|
1040
|
+
return 'BIGINT';
|
|
1041
|
+
return 'INTEGER';
|
|
1042
|
+
}
|
|
1043
|
+
case 'TINYINT':
|
|
1044
|
+
return 'TINYINT';
|
|
1045
|
+
case 'BIGINT':
|
|
1046
|
+
return 'BIGINT';
|
|
1047
|
+
case 'FLOAT':
|
|
1048
|
+
return 'REAL';
|
|
1049
|
+
case 'DOUBLE':
|
|
1050
|
+
return 'DOUBLE';
|
|
1051
|
+
case 'NUMBER':
|
|
1052
|
+
case 'DECIMAL':
|
|
1053
|
+
case 'NUMERIC':
|
|
1054
|
+
return `DECIMAL(${String(dt?.precision || 10)}, ${String(dt?.scale || 0)})`;
|
|
1055
|
+
case 'BOOLEAN':
|
|
1056
|
+
return 'BOOLEAN';
|
|
1057
|
+
case 'DATE':
|
|
1058
|
+
case 'DATETIME':
|
|
1059
|
+
case 'TIMESTAMP':
|
|
1060
|
+
return 'TIMESTAMP';
|
|
1061
|
+
case 'SECONDDATE':
|
|
1062
|
+
return 'SECONDDATE';
|
|
1063
|
+
case 'DATEONLY':
|
|
1064
|
+
return 'DATE';
|
|
1065
|
+
case 'TIME':
|
|
1066
|
+
return 'TIME';
|
|
1067
|
+
case 'BLOB':
|
|
1068
|
+
return 'BLOB';
|
|
1069
|
+
case 'BINARY':
|
|
1070
|
+
case 'VARBINARY':
|
|
1071
|
+
return `VARBINARY(${String(dt?.length || 255)})`;
|
|
1072
|
+
case 'BIT':
|
|
1073
|
+
return 'BOOLEAN';
|
|
1074
|
+
case 'JSON':
|
|
1075
|
+
return 'NCLOB';
|
|
1076
|
+
case 'JSONB':
|
|
1077
|
+
return 'BLOB';
|
|
1078
|
+
case 'UUID':
|
|
1079
|
+
return 'NVARCHAR(36)';
|
|
1080
|
+
case 'GEOMETRY':
|
|
1081
|
+
return dt?.srid ? `ST_GEOMETRY(${String(dt.srid)})` : 'ST_GEOMETRY';
|
|
1082
|
+
case 'ST_POINT':
|
|
1083
|
+
return dt?.srid ? `ST_POINT(${String(dt.srid)})` : 'ST_POINT';
|
|
1084
|
+
case 'ENUM':
|
|
1085
|
+
return 'NVARCHAR(255)';
|
|
1086
|
+
case 'VIRTUAL':
|
|
1087
|
+
return '';
|
|
1088
|
+
default:
|
|
1089
|
+
// Pass through unrecognized (often HANA-native) type strings as-is
|
|
1090
|
+
// rather than defaulting to a generic string type - e.g. callers may
|
|
1091
|
+
// legitimately pass 'SECONDDATE', 'ALPHANUM(10)' etc directly.
|
|
1092
|
+
return typeKey;
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
// ==================== Query Builders ====================
|
|
1096
|
+
buildWhereClause(where, _options) {
|
|
1097
|
+
const values = [];
|
|
1098
|
+
if (!where || Object.keys(where).length === 0) {
|
|
1099
|
+
return { sql: '', values };
|
|
1100
|
+
}
|
|
1101
|
+
const buildCondition = (condition) => {
|
|
1102
|
+
if (!condition || typeof condition !== 'object') {
|
|
1103
|
+
values.push(condition);
|
|
1104
|
+
return '?';
|
|
1105
|
+
}
|
|
1106
|
+
const cond = condition;
|
|
1107
|
+
if (cond.$and || cond.$or || cond.$not) {
|
|
1108
|
+
const conditions = [];
|
|
1109
|
+
if (cond.$and) {
|
|
1110
|
+
conditions.push(`(${cond.$and.map((c) => buildCondition(c)).join(' AND ')})`);
|
|
1111
|
+
}
|
|
1112
|
+
if (cond.$or) {
|
|
1113
|
+
conditions.push(`(${cond.$or.map((c) => buildCondition(c)).join(' OR ')})`);
|
|
1114
|
+
}
|
|
1115
|
+
if (cond.$not) {
|
|
1116
|
+
conditions.push(`NOT (${buildCondition(cond.$not)})`);
|
|
1117
|
+
}
|
|
1118
|
+
return conditions.join(' AND ');
|
|
1119
|
+
}
|
|
1120
|
+
const fieldConditions = [];
|
|
1121
|
+
for (const [key, value] of Object.entries(cond)) {
|
|
1122
|
+
if (key.startsWith('$'))
|
|
1123
|
+
continue;
|
|
1124
|
+
if (value && typeof value === 'object' && !(value instanceof Date) && !Array.isArray(value)) {
|
|
1125
|
+
const valueObj = value;
|
|
1126
|
+
if (valueObj.$eq !== undefined) {
|
|
1127
|
+
values.push(valueObj.$eq);
|
|
1128
|
+
fieldConditions.push(`${this.escapeId(key)} = ?`);
|
|
1129
|
+
}
|
|
1130
|
+
else if (valueObj.$ne !== undefined) {
|
|
1131
|
+
values.push(valueObj.$ne);
|
|
1132
|
+
fieldConditions.push(`${this.escapeId(key)} != ?`);
|
|
1133
|
+
}
|
|
1134
|
+
else if (valueObj.$gt !== undefined) {
|
|
1135
|
+
values.push(valueObj.$gt);
|
|
1136
|
+
fieldConditions.push(`${this.escapeId(key)} > ?`);
|
|
1137
|
+
}
|
|
1138
|
+
else if (valueObj.$gte !== undefined) {
|
|
1139
|
+
values.push(valueObj.$gte);
|
|
1140
|
+
fieldConditions.push(`${this.escapeId(key)} >= ?`);
|
|
1141
|
+
}
|
|
1142
|
+
else if (valueObj.$lt !== undefined) {
|
|
1143
|
+
values.push(valueObj.$lt);
|
|
1144
|
+
fieldConditions.push(`${this.escapeId(key)} < ?`);
|
|
1145
|
+
}
|
|
1146
|
+
else if (valueObj.$lte !== undefined) {
|
|
1147
|
+
values.push(valueObj.$lte);
|
|
1148
|
+
fieldConditions.push(`${this.escapeId(key)} <= ?`);
|
|
1149
|
+
}
|
|
1150
|
+
else if (valueObj.$like !== undefined) {
|
|
1151
|
+
values.push(valueObj.$like);
|
|
1152
|
+
fieldConditions.push(`${this.escapeId(key)} LIKE ?`);
|
|
1153
|
+
}
|
|
1154
|
+
else if (valueObj.$notLike !== undefined) {
|
|
1155
|
+
values.push(valueObj.$notLike);
|
|
1156
|
+
fieldConditions.push(`${this.escapeId(key)} NOT LIKE ?`);
|
|
1157
|
+
}
|
|
1158
|
+
else if (valueObj.$in) {
|
|
1159
|
+
const inValues = valueObj.$in;
|
|
1160
|
+
values.push(...inValues);
|
|
1161
|
+
fieldConditions.push(`${this.escapeId(key)} IN (${inValues.map(() => '?').join(', ')})`);
|
|
1162
|
+
}
|
|
1163
|
+
else if (valueObj.$notIn) {
|
|
1164
|
+
const notInValues = valueObj.$notIn;
|
|
1165
|
+
values.push(...notInValues);
|
|
1166
|
+
fieldConditions.push(`${this.escapeId(key)} NOT IN (${notInValues.map(() => '?').join(', ')})`);
|
|
1167
|
+
}
|
|
1168
|
+
else if (valueObj.$between) {
|
|
1169
|
+
const between = valueObj.$between;
|
|
1170
|
+
values.push(between[0], between[1]);
|
|
1171
|
+
fieldConditions.push(`${this.escapeId(key)} BETWEEN ? AND ?`);
|
|
1172
|
+
}
|
|
1173
|
+
else if (valueObj.$notBetween) {
|
|
1174
|
+
const notBetween = valueObj.$notBetween;
|
|
1175
|
+
values.push(notBetween[0], notBetween[1]);
|
|
1176
|
+
fieldConditions.push(`${this.escapeId(key)} NOT BETWEEN ? AND ?`);
|
|
1177
|
+
}
|
|
1178
|
+
else if (valueObj.$isNull !== undefined) {
|
|
1179
|
+
fieldConditions.push(valueObj.$isNull ? `${this.escapeId(key)} IS NULL` : `${this.escapeId(key)} IS NOT NULL`);
|
|
1180
|
+
}
|
|
1181
|
+
else {
|
|
1182
|
+
values.push(value);
|
|
1183
|
+
fieldConditions.push(`${this.escapeId(key)} = ?`);
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
else if (value === null) {
|
|
1187
|
+
fieldConditions.push(`${this.escapeId(key)} IS NULL`);
|
|
1188
|
+
}
|
|
1189
|
+
else if (Array.isArray(value)) {
|
|
1190
|
+
values.push(...value);
|
|
1191
|
+
fieldConditions.push(`${this.escapeId(key)} IN (${value.map(() => '?').join(', ')})`);
|
|
1192
|
+
}
|
|
1193
|
+
else {
|
|
1194
|
+
values.push(value);
|
|
1195
|
+
fieldConditions.push(`${this.escapeId(key)} = ?`);
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
return fieldConditions.join(' AND ');
|
|
1199
|
+
};
|
|
1200
|
+
const sql = buildCondition(where);
|
|
1201
|
+
return { sql, values };
|
|
1202
|
+
}
|
|
1203
|
+
buildOrderClause(order, _options) {
|
|
1204
|
+
const orderArray = order;
|
|
1205
|
+
if (!orderArray || !Array.isArray(orderArray) || orderArray.length === 0) {
|
|
1206
|
+
return '';
|
|
1207
|
+
}
|
|
1208
|
+
const orderParts = [];
|
|
1209
|
+
for (const item of orderArray) {
|
|
1210
|
+
if (Array.isArray(item)) {
|
|
1211
|
+
const field = typeof item[0] === 'string' ? this.escapeId(item[0]) : item[0];
|
|
1212
|
+
const direction = item[1] ? ` ${item[1]}` : '';
|
|
1213
|
+
orderParts.push(`${field}${direction}`);
|
|
1214
|
+
}
|
|
1215
|
+
else if (typeof item === 'string') {
|
|
1216
|
+
orderParts.push(this.escapeId(item));
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
return orderParts.length > 0 ? `ORDER BY ${orderParts.join(', ')}` : '';
|
|
1220
|
+
}
|
|
1221
|
+
/**
|
|
1222
|
+
* HANA pagination uses the standard `LIMIT n OFFSET m` clause (unlike
|
|
1223
|
+
* Oracle/Db2's `OFFSET ... FETCH FIRST ...`). Unlike PostgreSQL/MySQL,
|
|
1224
|
+
* HANA does not allow a bare `OFFSET` without a `LIMIT` - when only an
|
|
1225
|
+
* offset is supplied, a sentinel maximum `LIMIT` is emitted alongside it.
|
|
1226
|
+
*/
|
|
1227
|
+
buildLimitOffset(limit, offset) {
|
|
1228
|
+
if (limit === undefined && offset === undefined)
|
|
1229
|
+
return '';
|
|
1230
|
+
const limitValue = limit !== undefined ? Number(limit) : HANA_MAX_LIMIT;
|
|
1231
|
+
let sql = ` LIMIT ${limitValue}`;
|
|
1232
|
+
if (offset !== undefined) {
|
|
1233
|
+
sql += ` OFFSET ${Number(offset)}`;
|
|
1234
|
+
}
|
|
1235
|
+
return sql;
|
|
1236
|
+
}
|
|
1237
|
+
buildInsertQuery(tableName, values, options) {
|
|
1238
|
+
const columns = Object.keys(values);
|
|
1239
|
+
const processedValues = [];
|
|
1240
|
+
const placeholders = [];
|
|
1241
|
+
for (const value of Object.values(values)) {
|
|
1242
|
+
if (value instanceof prorm_1.Literal) {
|
|
1243
|
+
placeholders.push(value.val);
|
|
1244
|
+
}
|
|
1245
|
+
else {
|
|
1246
|
+
placeholders.push('?');
|
|
1247
|
+
processedValues.push(value);
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
const sql = `INSERT INTO ${this.quoteTable(tableName, options?.schema)} (${columns.map((c) => this.escapeId(c)).join(', ')}) VALUES (${placeholders.join(', ')})`;
|
|
1251
|
+
if (options?.returning) {
|
|
1252
|
+
// HANA has no INSERT ... RETURNING clause; callers must re-select the
|
|
1253
|
+
// row after the insert (e.g. by identity value or supplied key).
|
|
1254
|
+
throw new Error('HANA does not support a RETURNING clause on INSERT. Re-fetch the row after the insert instead.');
|
|
1255
|
+
}
|
|
1256
|
+
return { sql, values: processedValues };
|
|
1257
|
+
}
|
|
1258
|
+
buildUpdateQuery(tableName, values, where, options) {
|
|
1259
|
+
const processedValues = [];
|
|
1260
|
+
const setClauses = [];
|
|
1261
|
+
for (const [key, value] of Object.entries(values)) {
|
|
1262
|
+
if (value instanceof prorm_1.Literal) {
|
|
1263
|
+
setClauses.push(`${this.escapeId(key)} = ${value.val}`);
|
|
1264
|
+
}
|
|
1265
|
+
else {
|
|
1266
|
+
setClauses.push(`${this.escapeId(key)} = ?`);
|
|
1267
|
+
processedValues.push(value);
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
let sql = `UPDATE ${this.quoteTable(tableName)} SET ${setClauses.join(', ')}`;
|
|
1271
|
+
const whereClause = this.buildWhereClause(where);
|
|
1272
|
+
if (whereClause.sql) {
|
|
1273
|
+
sql += ` WHERE ${whereClause.sql}`;
|
|
1274
|
+
processedValues.push(...whereClause.values);
|
|
1275
|
+
}
|
|
1276
|
+
if (options?.returning) {
|
|
1277
|
+
throw new Error('HANA does not support a RETURNING clause on UPDATE. Re-fetch the row after the update instead.');
|
|
1278
|
+
}
|
|
1279
|
+
return { sql, values: processedValues };
|
|
1280
|
+
}
|
|
1281
|
+
buildDeleteQuery(tableName, where, options) {
|
|
1282
|
+
if (options?.truncate) {
|
|
1283
|
+
return { sql: `TRUNCATE TABLE ${this.quoteTable(tableName)}`, values: [] };
|
|
1284
|
+
}
|
|
1285
|
+
let sql = `DELETE FROM ${this.quoteTable(tableName)}`;
|
|
1286
|
+
const processedValues = [];
|
|
1287
|
+
const whereClause = this.buildWhereClause(where);
|
|
1288
|
+
if (whereClause.sql) {
|
|
1289
|
+
sql += ` WHERE ${whereClause.sql}`;
|
|
1290
|
+
processedValues.push(...whereClause.values);
|
|
1291
|
+
}
|
|
1292
|
+
if (options?.returning) {
|
|
1293
|
+
throw new Error('HANA does not support a RETURNING clause on DELETE. Re-fetch/capture the row before deleting instead.');
|
|
1294
|
+
}
|
|
1295
|
+
return { sql, values: processedValues };
|
|
1296
|
+
}
|
|
1297
|
+
/**
|
|
1298
|
+
* Build a `WITH name(columns) AS (base UNION ALL recursive)` prefix for a
|
|
1299
|
+
* recursive common table expression. HANA has full standard recursive CTE
|
|
1300
|
+
* support since SPS09.
|
|
1301
|
+
*/
|
|
1302
|
+
buildRecursiveCteClause(cte) {
|
|
1303
|
+
const columnList = cte.columns && cte.columns.length > 0
|
|
1304
|
+
? `(${cte.columns.map((c) => this.escapeId(c)).join(', ')})`
|
|
1305
|
+
: '';
|
|
1306
|
+
const unionKeyword = cte.unionAll === false ? 'UNION' : 'UNION ALL';
|
|
1307
|
+
return `WITH ${this.escapeId(cte.name)}${columnList} AS (${cte.baseQuery} ${unionKeyword} ${cte.recursiveQuery})`;
|
|
1308
|
+
}
|
|
1309
|
+
buildRecursiveCteQuery(cte, outer) {
|
|
1310
|
+
return this.buildSelectQuery({
|
|
1311
|
+
...outer,
|
|
1312
|
+
tableName: outer.tableName || cte.name,
|
|
1313
|
+
cte,
|
|
1314
|
+
});
|
|
1315
|
+
}
|
|
1316
|
+
/**
|
|
1317
|
+
* Build an OLAP window/analytic function expression, e.g.:
|
|
1318
|
+
* ROW_NUMBER() OVER (PARTITION BY "dept" ORDER BY "salary" DESC)
|
|
1319
|
+
*
|
|
1320
|
+
* HANA has mature, standard window-function support.
|
|
1321
|
+
*/
|
|
1322
|
+
buildWindowFunction(options) {
|
|
1323
|
+
const args = (options.args || []).join(', ');
|
|
1324
|
+
const overParts = [];
|
|
1325
|
+
if (options.partitionBy) {
|
|
1326
|
+
const cols = Array.isArray(options.partitionBy) ? options.partitionBy : [options.partitionBy];
|
|
1327
|
+
overParts.push(`PARTITION BY ${cols.map((c) => this.escapeId(c)).join(', ')}`);
|
|
1328
|
+
}
|
|
1329
|
+
if (options.orderBy && options.orderBy.length > 0) {
|
|
1330
|
+
const orderParts = options.orderBy.map((o) => typeof o === 'string' ? this.escapeId(o) : `${this.escapeId(o.column)}${o.direction ? ` ${o.direction}` : ''}`);
|
|
1331
|
+
overParts.push(`ORDER BY ${orderParts.join(', ')}`);
|
|
1332
|
+
}
|
|
1333
|
+
if (options.frame) {
|
|
1334
|
+
overParts.push(options.frame);
|
|
1335
|
+
}
|
|
1336
|
+
return `${options.fn.toUpperCase()}(${args}) OVER (${overParts.join(' ')})`;
|
|
1337
|
+
}
|
|
1338
|
+
buildSelectQuery(options) {
|
|
1339
|
+
const values = [];
|
|
1340
|
+
const cte = options.cte;
|
|
1341
|
+
let sql = cte ? `${this.buildRecursiveCteClause(cte)} ` : '';
|
|
1342
|
+
sql += 'SELECT ';
|
|
1343
|
+
if (options.distinct) {
|
|
1344
|
+
sql += 'DISTINCT ';
|
|
1345
|
+
}
|
|
1346
|
+
if (options.attributes) {
|
|
1347
|
+
if (Array.isArray(options.attributes)) {
|
|
1348
|
+
sql += options.attributes
|
|
1349
|
+
.map((a) => {
|
|
1350
|
+
if (a === '*')
|
|
1351
|
+
return a;
|
|
1352
|
+
if (typeof a === 'object' && a !== null && 'window' in a) {
|
|
1353
|
+
const windowAttr = a;
|
|
1354
|
+
return `${this.buildWindowFunction(windowAttr.window)} AS ${this.escapeId(windowAttr.as)}`;
|
|
1355
|
+
}
|
|
1356
|
+
return this.escapeId(a);
|
|
1357
|
+
})
|
|
1358
|
+
.join(', ');
|
|
1359
|
+
}
|
|
1360
|
+
else {
|
|
1361
|
+
const include = options.attributes.include?.map((a) => this.escapeId(a)).join(', ');
|
|
1362
|
+
sql += include || '*';
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
else {
|
|
1366
|
+
sql += '*';
|
|
1367
|
+
}
|
|
1368
|
+
sql += ` FROM ${this.quoteTable(options.tableName, options.schema)}`;
|
|
1369
|
+
if (options.include && options.include.length > 0) {
|
|
1370
|
+
for (const include of options.include) {
|
|
1371
|
+
const joinType = include.required ? 'INNER JOIN' : 'LEFT JOIN';
|
|
1372
|
+
const joinModel = include.model;
|
|
1373
|
+
const joinTableName = joinModel.tableName || joinModel.name || '';
|
|
1374
|
+
const joinAlias = include.as || joinTableName;
|
|
1375
|
+
sql += ` ${joinType} ${this.quoteTable(joinTableName)} AS ${this.escapeId(joinAlias)}`;
|
|
1376
|
+
if (include.on) {
|
|
1377
|
+
const onClause = this.buildWhereClause(include.on);
|
|
1378
|
+
sql += ` ON ${onClause.sql}`;
|
|
1379
|
+
values.push(...onClause.values);
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
if (options.where && Object.keys(options.where).length > 0) {
|
|
1384
|
+
const whereClause = this.buildWhereClause(options.where);
|
|
1385
|
+
if (whereClause.sql) {
|
|
1386
|
+
sql += ` WHERE ${whereClause.sql}`;
|
|
1387
|
+
values.push(...whereClause.values);
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
if (options.group) {
|
|
1391
|
+
const groupBy = Array.isArray(options.group) ? options.group : [options.group];
|
|
1392
|
+
sql += ` GROUP BY ${groupBy.map((g) => this.escapeId(g)).join(', ')}`;
|
|
1393
|
+
if (options.having && Object.keys(options.having).length > 0) {
|
|
1394
|
+
const havingClause = this.buildWhereClause(options.having);
|
|
1395
|
+
sql += ` HAVING ${havingClause.sql}`;
|
|
1396
|
+
values.push(...havingClause.values);
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
if (options.order) {
|
|
1400
|
+
const orderClause = this.buildOrderClause(options.order);
|
|
1401
|
+
if (orderClause)
|
|
1402
|
+
sql += ` ${orderClause}`;
|
|
1403
|
+
}
|
|
1404
|
+
if (options.limit !== undefined || options.offset !== undefined) {
|
|
1405
|
+
sql += this.buildLimitOffset(options.limit, options.offset);
|
|
1406
|
+
}
|
|
1407
|
+
if (options.lock) {
|
|
1408
|
+
sql += ' FOR UPDATE';
|
|
1409
|
+
}
|
|
1410
|
+
return { sql, values };
|
|
1411
|
+
}
|
|
1412
|
+
/**
|
|
1413
|
+
* Build an UPSERT query using HANA's native `UPSERT ... WITH PRIMARY KEY`
|
|
1414
|
+
* statement - a genuine HANA-only construct (distinct from the `MERGE
|
|
1415
|
+
* INTO` syntax every other enterprise dialect in this codebase uses for
|
|
1416
|
+
* upserts) that inserts a row or updates it in place when a row with a
|
|
1417
|
+
* matching primary key already exists. When `conflictFields` names
|
|
1418
|
+
* columns other than the primary key, a `MERGE INTO` statement is emitted
|
|
1419
|
+
* instead, since `UPSERT ... WITH PRIMARY KEY` can only match on the
|
|
1420
|
+
* table's actual primary key.
|
|
1421
|
+
*/
|
|
1422
|
+
buildUpsertQuery(tableName, values, options) {
|
|
1423
|
+
const columns = Object.keys(values);
|
|
1424
|
+
const queryValues = Object.values(values);
|
|
1425
|
+
if (!options?.conflictFields || options.conflictFields.length === 0) {
|
|
1426
|
+
const sourceColumns = columns.map((c) => this.escapeId(c)).join(', ');
|
|
1427
|
+
const placeholders = columns.map(() => '?').join(', ');
|
|
1428
|
+
const sql = `UPSERT ${this.quoteTable(tableName, options?.schema)} (${sourceColumns}) VALUES (${placeholders}) WITH PRIMARY KEY`;
|
|
1429
|
+
return { sql, values: queryValues };
|
|
1430
|
+
}
|
|
1431
|
+
// Explicit conflict target: fall back to MERGE INTO, which can match on
|
|
1432
|
+
// any column set, not just the primary key.
|
|
1433
|
+
const conflictFields = options.conflictFields;
|
|
1434
|
+
const updateFields = options.updateOnDuplicate && options.updateOnDuplicate.length > 0
|
|
1435
|
+
? options.updateOnDuplicate
|
|
1436
|
+
: columns.filter((c) => !conflictFields.includes(c));
|
|
1437
|
+
const sourceColumns = columns.map((c) => this.escapeId(c)).join(', ');
|
|
1438
|
+
const sourceValues = columns.map(() => '?').join(', ');
|
|
1439
|
+
const onClauses = conflictFields
|
|
1440
|
+
.map((c) => `target.${this.escapeId(c)} = source.${this.escapeId(c)}`)
|
|
1441
|
+
.join(' AND ');
|
|
1442
|
+
const updateSet = updateFields.length > 0
|
|
1443
|
+
? updateFields.map((c) => `${this.escapeId(c)} = source.${this.escapeId(c)}`).join(', ')
|
|
1444
|
+
: columns.map((c) => `${this.escapeId(c)} = source.${this.escapeId(c)}`).join(', ');
|
|
1445
|
+
const sql = `MERGE INTO ${this.quoteTable(tableName, options?.schema)} AS target ` +
|
|
1446
|
+
`USING (SELECT ${columns.map((c, i) => `? AS ${this.escapeId(c)}`).join(', ')} FROM DUMMY) AS source ` +
|
|
1447
|
+
`ON ${onClauses} ` +
|
|
1448
|
+
`WHEN MATCHED THEN UPDATE SET ${updateSet} ` +
|
|
1449
|
+
`WHEN NOT MATCHED THEN INSERT (${sourceColumns}) VALUES (${columns.map((c) => `source.${this.escapeId(c)}`).join(', ')})`;
|
|
1450
|
+
void sourceValues;
|
|
1451
|
+
return { sql, values: queryValues };
|
|
1452
|
+
}
|
|
1453
|
+
buildIncrementQuery(tableName, fields, where, options) {
|
|
1454
|
+
const by = options?.by ?? 1;
|
|
1455
|
+
const setClauses = [];
|
|
1456
|
+
const queryValues = [];
|
|
1457
|
+
if (typeof fields === 'string') {
|
|
1458
|
+
setClauses.push(`${this.escapeId(fields)} = ${this.escapeId(fields)} + ?`);
|
|
1459
|
+
queryValues.push(by);
|
|
1460
|
+
}
|
|
1461
|
+
else if (Array.isArray(fields)) {
|
|
1462
|
+
for (const field of fields) {
|
|
1463
|
+
setClauses.push(`${this.escapeId(field)} = ${this.escapeId(field)} + ?`);
|
|
1464
|
+
queryValues.push(by);
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
else {
|
|
1468
|
+
for (const [field, value] of Object.entries(fields)) {
|
|
1469
|
+
setClauses.push(`${this.escapeId(field)} = ${this.escapeId(field)} + ?`);
|
|
1470
|
+
queryValues.push(value);
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
let sql = `UPDATE ${this.quoteTable(tableName)} SET ${setClauses.join(', ')}`;
|
|
1474
|
+
const whereClause = this.buildWhereClause(where);
|
|
1475
|
+
if (whereClause.sql) {
|
|
1476
|
+
sql += ` WHERE ${whereClause.sql}`;
|
|
1477
|
+
queryValues.push(...whereClause.values);
|
|
1478
|
+
}
|
|
1479
|
+
return { sql, values: queryValues };
|
|
1480
|
+
}
|
|
1481
|
+
replaceReplacements(sql, replacements) {
|
|
1482
|
+
if (!replacements)
|
|
1483
|
+
return sql;
|
|
1484
|
+
if (Array.isArray(replacements)) {
|
|
1485
|
+
let result = sql;
|
|
1486
|
+
for (const value of replacements) {
|
|
1487
|
+
result = result.replace('?', this.escape(value));
|
|
1488
|
+
}
|
|
1489
|
+
return result;
|
|
1490
|
+
}
|
|
1491
|
+
let result = sql;
|
|
1492
|
+
for (const [key, value] of Object.entries(replacements)) {
|
|
1493
|
+
result = result.replace(new RegExp(`:${key}\\b`, 'g'), this.escape(value));
|
|
1494
|
+
}
|
|
1495
|
+
return result;
|
|
1496
|
+
}
|
|
1497
|
+
// ==================== User / Privilege Management ====================
|
|
1498
|
+
// Unlike Db2 (OS/LDAP-authenticated only), HANA has real, native SQL user
|
|
1499
|
+
// and role management.
|
|
1500
|
+
buildCreateUserQuery(username, options) {
|
|
1501
|
+
let sql = `CREATE USER ${this.escapeId(username)}`;
|
|
1502
|
+
if (options?.password) {
|
|
1503
|
+
sql += ` PASSWORD "${options.password.replace(/"/g, '""')}"`;
|
|
1504
|
+
}
|
|
1505
|
+
if (options?.forceFirstPasswordChange === false) {
|
|
1506
|
+
sql += ' NO FORCE_FIRST_PASSWORD_CHANGE';
|
|
1507
|
+
}
|
|
1508
|
+
return sql;
|
|
1509
|
+
}
|
|
1510
|
+
buildAlterUserQuery(username, options) {
|
|
1511
|
+
let sql = `ALTER USER ${this.escapeId(username)}`;
|
|
1512
|
+
if (options?.password) {
|
|
1513
|
+
sql += ` PASSWORD "${options.password.replace(/"/g, '""')}"`;
|
|
1514
|
+
}
|
|
1515
|
+
return sql;
|
|
1516
|
+
}
|
|
1517
|
+
buildDropUserQuery(username) {
|
|
1518
|
+
return `DROP USER ${this.escapeId(username)}`;
|
|
1519
|
+
}
|
|
1520
|
+
getUsersQuery() {
|
|
1521
|
+
return 'SELECT USER_NAME FROM SYS.USERS ORDER BY USER_NAME';
|
|
1522
|
+
}
|
|
1523
|
+
buildGrantQuery(options) {
|
|
1524
|
+
const privileges = Array.isArray(options.privileges) ? options.privileges.join(', ') : options.privileges;
|
|
1525
|
+
let sql = `GRANT ${privileges}`;
|
|
1526
|
+
if (options.on)
|
|
1527
|
+
sql += ` ON ${options.on}`;
|
|
1528
|
+
sql += ` TO ${this.escapeId(options.to)}`;
|
|
1529
|
+
return sql;
|
|
1530
|
+
}
|
|
1531
|
+
buildRevokeQuery(options) {
|
|
1532
|
+
const privileges = Array.isArray(options.privileges) ? options.privileges.join(', ') : options.privileges;
|
|
1533
|
+
let sql = `REVOKE ${privileges}`;
|
|
1534
|
+
if (options.on)
|
|
1535
|
+
sql += ` ON ${options.on}`;
|
|
1536
|
+
sql += ` FROM ${this.escapeId(options.from)}`;
|
|
1537
|
+
return sql;
|
|
1538
|
+
}
|
|
1539
|
+
buildShowGrantsQuery(username) {
|
|
1540
|
+
return `SELECT * FROM SYS.GRANTED_PRIVILEGES WHERE GRANTEE = '${username.toUpperCase()}'`;
|
|
1541
|
+
}
|
|
1542
|
+
buildFlushPrivilegesQuery() {
|
|
1543
|
+
throw new Error('HANA applies privilege changes immediately; there is no FLUSH PRIVILEGES equivalent.');
|
|
1544
|
+
}
|
|
1545
|
+
buildCreateRoleQuery(roleName) {
|
|
1546
|
+
return `CREATE ROLE ${this.escapeId(roleName)}`;
|
|
1547
|
+
}
|
|
1548
|
+
buildDropRoleQuery(roleName) {
|
|
1549
|
+
return `DROP ROLE ${this.escapeId(roleName)}`;
|
|
1550
|
+
}
|
|
1551
|
+
buildGrantRoleQuery(role, to) {
|
|
1552
|
+
return `GRANT ${this.escapeId(role)} TO ${this.escapeId(to)}`;
|
|
1553
|
+
}
|
|
1554
|
+
buildRevokeRoleQuery(role, from) {
|
|
1555
|
+
return `REVOKE ${this.escapeId(role)} FROM ${this.escapeId(from)}`;
|
|
1556
|
+
}
|
|
1557
|
+
getRolesQuery() {
|
|
1558
|
+
return 'SELECT ROLE_NAME FROM SYS.ROLES ORDER BY ROLE_NAME';
|
|
1559
|
+
}
|
|
1560
|
+
// ==================== Sequences ====================
|
|
1561
|
+
async createSequence(options) {
|
|
1562
|
+
const seqName = options.schema
|
|
1563
|
+
? `${this.escapeId(options.schema)}.${this.escapeId(options.name)}`
|
|
1564
|
+
: this.escapeId(options.name);
|
|
1565
|
+
if (options.ifNotExists) {
|
|
1566
|
+
const exists = await this.hasSequence(options.name, options.schema);
|
|
1567
|
+
if (exists)
|
|
1568
|
+
return;
|
|
1569
|
+
}
|
|
1570
|
+
let sql = `CREATE SEQUENCE ${seqName}`;
|
|
1571
|
+
if (options.startWith !== undefined)
|
|
1572
|
+
sql += ` START WITH ${options.startWith}`;
|
|
1573
|
+
if (options.incrementBy !== undefined)
|
|
1574
|
+
sql += ` INCREMENT BY ${options.incrementBy}`;
|
|
1575
|
+
if (options.minvalue !== undefined)
|
|
1576
|
+
sql += ` MINVALUE ${options.minvalue}`;
|
|
1577
|
+
if (options.maxvalue !== undefined)
|
|
1578
|
+
sql += ` MAXVALUE ${options.maxvalue}`;
|
|
1579
|
+
sql += options.cycle ? ' CYCLE' : ' NO CYCLE';
|
|
1580
|
+
await this.query(sql);
|
|
1581
|
+
}
|
|
1582
|
+
async dropSequence(sequenceName, options) {
|
|
1583
|
+
const seqName = options?.schema
|
|
1584
|
+
? `${this.escapeId(options.schema)}.${this.escapeId(sequenceName)}`
|
|
1585
|
+
: this.escapeId(sequenceName);
|
|
1586
|
+
if (options?.ifExists) {
|
|
1587
|
+
const exists = await this.hasSequence(sequenceName, options.schema);
|
|
1588
|
+
if (!exists)
|
|
1589
|
+
return;
|
|
1590
|
+
}
|
|
1591
|
+
await this.query(`DROP SEQUENCE ${seqName}`);
|
|
1592
|
+
}
|
|
1593
|
+
/**
|
|
1594
|
+
* Get the next value of a sequence via HANA's Oracle-style pseudo-column
|
|
1595
|
+
* access (`<sequence>.NEXTVAL`), selected from the single-row `DUMMY`
|
|
1596
|
+
* pseudo-table (HANA's equivalent of Oracle's `DUAL`).
|
|
1597
|
+
*/
|
|
1598
|
+
async nextSequenceValue(sequenceName) {
|
|
1599
|
+
const result = await this.query(`SELECT ${this.escapeId(sequenceName)}.NEXTVAL AS NEXTVAL FROM DUMMY`);
|
|
1600
|
+
const row = result.rows[0];
|
|
1601
|
+
return Number(row?.NEXTVAL ?? row?.nextval);
|
|
1602
|
+
}
|
|
1603
|
+
async currSequenceValue(sequenceName) {
|
|
1604
|
+
const result = await this.query(`SELECT ${this.escapeId(sequenceName)}.CURRVAL AS CURRVAL FROM DUMMY`);
|
|
1605
|
+
const row = result.rows[0];
|
|
1606
|
+
return Number(row?.CURRVAL ?? row?.currval);
|
|
1607
|
+
}
|
|
1608
|
+
async hasSequence(sequenceName, schema) {
|
|
1609
|
+
let sql = `SELECT SEQUENCE_NAME FROM SYS.SEQUENCES WHERE SEQUENCE_NAME = '${sequenceName.toUpperCase()}'`;
|
|
1610
|
+
if (schema)
|
|
1611
|
+
sql += ` AND SCHEMA_NAME = '${schema.toUpperCase()}'`;
|
|
1612
|
+
const result = await this.query(sql);
|
|
1613
|
+
return result.rows.length > 0;
|
|
1614
|
+
}
|
|
1615
|
+
async listSequences() {
|
|
1616
|
+
const result = await this.query('SELECT SCHEMA_NAME, SEQUENCE_NAME FROM SYS.SEQUENCES ORDER BY SCHEMA_NAME, SEQUENCE_NAME');
|
|
1617
|
+
return result.rows.map((row) => `${row.SCHEMA_NAME || row.schema_name}.${row.SEQUENCE_NAME || row.sequence_name}`);
|
|
1618
|
+
}
|
|
1619
|
+
// ==================== Identity & Computed Columns ====================
|
|
1620
|
+
async createIdentityColumn(tableName, columnName, options) {
|
|
1621
|
+
const startWith = options?.startWith ?? 1;
|
|
1622
|
+
const incrementBy = options?.incrementBy ?? 1;
|
|
1623
|
+
await this.query(`ALTER TABLE ${this.quoteTable(tableName)} ALTER (${this.escapeId(columnName)} GENERATED ALWAYS AS IDENTITY (START WITH ${startWith} INCREMENT BY ${incrementBy}))`);
|
|
1624
|
+
}
|
|
1625
|
+
/** HANA calculated (computed) columns: `col type GENERATED ALWAYS AS (expr)`. */
|
|
1626
|
+
async createComputedColumn(tableName, columnName, expression, options) {
|
|
1627
|
+
const type = options?.type || 'NVARCHAR(255)';
|
|
1628
|
+
const persisted = options?.persisted ? ' PERSISTED' : '';
|
|
1629
|
+
await this.query(`ALTER TABLE ${this.quoteTable(tableName)} ADD (${this.escapeId(columnName)} ${type} GENERATED ALWAYS AS (${expression})${persisted})`);
|
|
1630
|
+
}
|
|
1631
|
+
// ==================== Stored Procedures ====================
|
|
1632
|
+
// HANA supports both SQLScript stored procedures and functions; only
|
|
1633
|
+
// procedures are modeled here per the shared StoredProcedureOptions shape.
|
|
1634
|
+
async createStoredProcedure(options) {
|
|
1635
|
+
const procName = options.schema
|
|
1636
|
+
? `${this.escapeId(options.schema)}.${this.escapeId(options.name)}`
|
|
1637
|
+
: this.escapeId(options.name);
|
|
1638
|
+
if (options.ifNotExists && !options.replace) {
|
|
1639
|
+
const exists = await this.hasStoredProcedure(options.name, options.schema);
|
|
1640
|
+
if (exists)
|
|
1641
|
+
return;
|
|
1642
|
+
}
|
|
1643
|
+
const paramList = (options.params || [])
|
|
1644
|
+
.map((param) => {
|
|
1645
|
+
const dir = param.mode === 'OUT' ? 'OUT' : param.mode === 'INOUT' ? 'INOUT' : 'IN';
|
|
1646
|
+
return `${dir} ${this.escapeId(param.name)} ${param.type}`;
|
|
1647
|
+
})
|
|
1648
|
+
.join(', ');
|
|
1649
|
+
const replace = options.replace ? 'OR REPLACE ' : '';
|
|
1650
|
+
const sql = `CREATE ${replace}PROCEDURE ${procName} (${paramList})\nLANGUAGE SQLSCRIPT\nSQL SECURITY INVOKER\nAS\nBEGIN\n${options.body}\nEND`;
|
|
1651
|
+
await this.query(sql);
|
|
1652
|
+
}
|
|
1653
|
+
async createProcedure(options) {
|
|
1654
|
+
return this.createStoredProcedure(options);
|
|
1655
|
+
}
|
|
1656
|
+
async dropStoredProcedure(procedureName, options) {
|
|
1657
|
+
const procName = options?.schema
|
|
1658
|
+
? `${this.escapeId(options.schema)}.${this.escapeId(procedureName)}`
|
|
1659
|
+
: this.escapeId(procedureName);
|
|
1660
|
+
if (options?.ifExists) {
|
|
1661
|
+
const exists = await this.hasStoredProcedure(procedureName, options.schema);
|
|
1662
|
+
if (!exists)
|
|
1663
|
+
return;
|
|
1664
|
+
}
|
|
1665
|
+
await this.query(`DROP PROCEDURE ${procName}`);
|
|
1666
|
+
}
|
|
1667
|
+
async dropProcedure(procedureName, options) {
|
|
1668
|
+
return this.dropStoredProcedure(procedureName, options);
|
|
1669
|
+
}
|
|
1670
|
+
async executeStoredProcedure(options) {
|
|
1671
|
+
const procName = options.schema
|
|
1672
|
+
? `${this.escapeId(options.schema)}.${this.escapeId(options.procedureName)}`
|
|
1673
|
+
: this.escapeId(options.procedureName);
|
|
1674
|
+
const paramValues = options.params ? Object.values(options.params) : [];
|
|
1675
|
+
const placeholders = paramValues.map(() => '?').join(', ');
|
|
1676
|
+
const sql = `CALL ${procName}(${placeholders})`;
|
|
1677
|
+
return this.query(sql, { replacements: paramValues });
|
|
1678
|
+
}
|
|
1679
|
+
async hasStoredProcedure(procedureName, schema) {
|
|
1680
|
+
let sql = `SELECT PROCEDURE_NAME FROM SYS.PROCEDURES WHERE PROCEDURE_NAME = '${procedureName.toUpperCase()}'`;
|
|
1681
|
+
if (schema)
|
|
1682
|
+
sql += ` AND SCHEMA_NAME = '${schema.toUpperCase()}'`;
|
|
1683
|
+
const result = await this.query(sql);
|
|
1684
|
+
return result.rows.length > 0;
|
|
1685
|
+
}
|
|
1686
|
+
// ==================== Triggers ====================
|
|
1687
|
+
async createTrigger(options) {
|
|
1688
|
+
const triggerName = options.schema
|
|
1689
|
+
? `${this.escapeId(options.schema)}.${this.escapeId(options.name)}`
|
|
1690
|
+
: this.escapeId(options.name);
|
|
1691
|
+
const tableName = options.schema
|
|
1692
|
+
? `${this.escapeId(options.schema)}.${this.escapeId(options.tableName)}`
|
|
1693
|
+
: this.quoteTable(options.tableName);
|
|
1694
|
+
const events = (options.events || []).join(', ');
|
|
1695
|
+
const level = options.level === 'STATEMENT' ? '' : ' FOR EACH ROW';
|
|
1696
|
+
const sql = `CREATE TRIGGER ${triggerName}\n${options.timing} ${events} ON ${tableName}${level}\nBEGIN\n${options.body}\nEND`;
|
|
1697
|
+
await this.query(sql);
|
|
1698
|
+
}
|
|
1699
|
+
async dropTrigger(triggerName, _tableName, options) {
|
|
1700
|
+
const name = options?.schema
|
|
1701
|
+
? `${this.escapeId(options.schema)}.${this.escapeId(triggerName)}`
|
|
1702
|
+
: this.escapeId(triggerName);
|
|
1703
|
+
if (options?.ifExists) {
|
|
1704
|
+
const exists = await this.hasTrigger(triggerName, _tableName, options.schema);
|
|
1705
|
+
if (!exists)
|
|
1706
|
+
return;
|
|
1707
|
+
}
|
|
1708
|
+
await this.query(`DROP TRIGGER ${name}`);
|
|
1709
|
+
}
|
|
1710
|
+
async hasTrigger(triggerName, _tableName, schema) {
|
|
1711
|
+
let sql = `SELECT TRIGGER_NAME FROM SYS.TRIGGERS WHERE TRIGGER_NAME = '${triggerName.toUpperCase()}'`;
|
|
1712
|
+
if (schema)
|
|
1713
|
+
sql += ` AND SCHEMA_NAME = '${schema.toUpperCase()}'`;
|
|
1714
|
+
const result = await this.query(sql);
|
|
1715
|
+
return result.rows.length > 0;
|
|
1716
|
+
}
|
|
1717
|
+
// ==================== Row-Level Security stubs (see createSecurityPolicy) ====================
|
|
1718
|
+
async createPolicy(_options) {
|
|
1719
|
+
throw new Error('HANA has no row-level-security policy statement; use Structured/Analytic Privileges instead.');
|
|
1720
|
+
}
|
|
1721
|
+
async dropPolicy(_policyName, _tableName, _options) {
|
|
1722
|
+
throw new Error('HANA has no row-level-security policy statement; use Structured/Analytic Privileges instead.');
|
|
1723
|
+
}
|
|
1724
|
+
async enableRLS() {
|
|
1725
|
+
throw new Error('HANA has no row-level-security toggle; use Structured/Analytic Privileges instead.');
|
|
1726
|
+
}
|
|
1727
|
+
async disableRLS() {
|
|
1728
|
+
throw new Error('HANA has no row-level-security toggle; use Structured/Analytic Privileges instead.');
|
|
1729
|
+
}
|
|
1730
|
+
async hasPolicy() {
|
|
1731
|
+
return false;
|
|
1732
|
+
}
|
|
1733
|
+
// ==================== Comments ====================
|
|
1734
|
+
async commentTable(tableName, comment) {
|
|
1735
|
+
await this.query(`COMMENT ON TABLE ${this.quoteTable(tableName)} IS '${comment.replace(/'/g, "''")}'`);
|
|
1736
|
+
}
|
|
1737
|
+
async commentColumn(tableName, columnName, comment) {
|
|
1738
|
+
await this.query(`COMMENT ON COLUMN ${this.quoteTable(tableName)}.${this.escapeId(columnName)} IS '${comment.replace(/'/g, "''")}'`);
|
|
1739
|
+
}
|
|
1740
|
+
}
|
|
1741
|
+
exports.HanaDialect = HanaDialect;
|