@dbsp/core 1.0.5 → 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/dist/index.d.ts CHANGED
@@ -4915,11 +4915,11 @@ declare class UpsertBuilder<T = void> extends MutationBuilderBase<T, UpsertInten
4915
4915
  * const users = await orm.nql<{ name: string; email: string }>`users | select name, email`.all();
4916
4916
  * ```
4917
4917
  *
4918
- * Each `${value}` interpolation is escaped into a safe NQL literal (string/number/boolean/null).
4919
- * Untrusted string input cannot inject NQL structure because it is always quoted and
4920
- * single-quotes inside are doubled (`''`). For dynamic structural fragments (table names,
4921
- * column names, ORDER BY direction) use the builder API (`orm.select()`). See #134 for
4922
- * upcoming `:param` binding and a `nqlRaw()` escape hatch.
4918
+ * Each non-raw `${value}` interpolation is converted into a generated NQL named
4919
+ * parameter (`:__p0`, `:__p1`, ...) and forwarded to the compiler through its
4920
+ * params map. Values never touch NQL source text. Use `nqlRaw()` only for trusted
4921
+ * dynamic NQL fragments that must participate in parsing, such as an ORDER BY
4922
+ * clause assembled from application-controlled choices.
4923
4923
  *
4924
4924
  * @module nql
4925
4925
  * @since DX-040
@@ -4951,15 +4951,32 @@ interface NqlBuilder<T> {
4951
4951
  * ```
4952
4952
  */
4953
4953
  type NqlTag = <T>(strings: TemplateStringsArray, ...values: unknown[]) => NqlBuilder<T>;
4954
+ declare const NQL_RAW_FRAGMENT_TYPE: unique symbol;
4955
+ /**
4956
+ * Trusted NQL source fragment for `nql` template interpolation.
4957
+ *
4958
+ * Never wrap untrusted input in `nqlRaw()`: raw fragments are spliced verbatim
4959
+ * into NQL source and parsed as query structure.
4960
+ *
4961
+ * @public
4962
+ */
4963
+ interface NqlRawFragment {
4964
+ readonly fragment: string;
4965
+ readonly [NQL_RAW_FRAGMENT_TYPE]: true;
4966
+ }
4967
+ /**
4968
+ * Mark a trusted fragment to splice verbatim into an NQL template.
4969
+ *
4970
+ * Never pass untrusted input to this function.
4971
+ *
4972
+ * @public
4973
+ */
4974
+ declare function nqlRaw(fragment: string): NqlRawFragment;
4954
4975
  /**
4955
4976
  * Create an NQL template tag function.
4956
4977
  *
4957
- * Each `${value}` in the template is escaped into a safe NQL literal before parsing.
4958
- * Supported interpolation types: `string`, `number`, `boolean`, `null`.
4959
- * Untrusted string input is therefore safe from NQL-syntax injection — it is always
4960
- * wrapped in single-quotes with embedded quotes doubled (`''`).
4961
- * For dynamic structural fragments (table names, column names, ORDER BY direction)
4962
- * use the builder API (`orm.select()`). See issue #134 for upcoming `:param` binding.
4978
+ * Each non-raw `${value}` in the template is bound as a generated named param.
4979
+ * Use `nqlRaw()` only for trusted dynamic NQL structure.
4963
4980
  *
4964
4981
  * @param schemaDefinition - Schema definition for validation
4965
4982
  * @param model - ModelIR for plan execution
@@ -7806,6 +7823,26 @@ type SelectField = string | {
7806
7823
  */
7807
7824
  type TraversalDirection = AdjacencyDirection | EdgeTableDirection;
7808
7825
 
7826
+ /**
7827
+ * Shared helpers for relation-dotted include path identity.
7828
+ *
7829
+ * Planner intent paths are positional (`include[0].include[1]`). Join identity
7830
+ * and hydration keys use the relation chain actually traversed
7831
+ * (`definition.file`, `manager.manager`), so every consumer must derive that
7832
+ * string the same way.
7833
+ */
7834
+ type RelationPathIncludeNode = {
7835
+ readonly relation?: unknown;
7836
+ readonly via?: unknown;
7837
+ readonly include?: readonly unknown[];
7838
+ };
7839
+ type RelationPathUsage = {
7840
+ readonly relationName?: string | null | undefined;
7841
+ readonly relationPath?: string | null | undefined;
7842
+ };
7843
+ declare function deriveRelationPathFromIntentPath(includes: readonly unknown[] | undefined, intentPath: string | undefined, fallbackRelation: string | undefined): string | undefined;
7844
+ declare function countDistinctRelationPathsByName(usages: readonly RelationPathUsage[]): Map<string, number>;
7845
+
7809
7846
  /**
7810
7847
  * ResultHydrator - Handles result hydration and recursive include processing.
7811
7848
  *
@@ -8184,4 +8221,4 @@ declare const assertIntentHasOrderBy: (result: AssertionQueryResult, expected: b
8184
8221
  */
8185
8222
  declare function runAssertions(blocks: AssertionBlock[], results: AssertionQueryResult[], queries: string[], hasDb?: boolean): AssertionSummary;
8186
8223
 
8187
- export { ASSERTION_TYPES, AdapterRequiredError, type AdjacencyOptions, type AfterMutationHook, type AfterQueryHook, type AggregateOptions, type AliasedColumn, type AliasedExprColumn, type AllColumns, AmbiguousPlanError, AmbiguousRelationError, type Assertion, type AssertionBlock, type AssertionOutcome, type AssertionQueryResult, type AssertionSummary, type AssertionType, BRAND, type BatchValuesOptions, type BatchValuesRef, type BeforeMutationHook, type BeforeQueryHook, type BrandKey, COLUMN_META, type CardinalityShorthand, CaseBuilder, type CaseValue, type ColumnDef, type ColumnMetaKey, ColumnNotFoundError, type ColumnRef, type ColumnSpec, type ColumnTypeToTS, CteBuilder, type CteDump, CteQueryBuilder, type CursorPaginateOptions, type CursorPaginatedResult, DEFAULT_CONVENTIONS, DEFAULT_FEATURE_CHECKERS, DUCKDB_CAPABILITIES, type DefaultFilters, type DefineModelOptions, DeleteBuilder, type DistinctField, type DumpMetaInput, type EdgeTableOptions, ErrorCode, type ErrorHookContext, Errors, ExecutionError, type ExprInput, ExpressionRef, type ExpressionSpec, type FeatureChecker, type FeatureUsage, type FilterOperators, type FilterValue, type FullTextSearchField, type FullTextSearchOptions, type GeneratedBelongsTo, type GeneratedColumn, type GeneratedColumnType, type GeneratedConventions, type GeneratedHasMany, type GeneratedHint, type GeneratedManyToMany, type GeneratedRelation, type GeneratedRelationKind, type GeneratedSchema, type GeneratedTable, type GetSchemaFromDbOptions, type HierarchyOptions, type HookErrorHandler, type HookManager, type HookPriority, type HydrateOptions, IRREGULAR_PLURALS, type IncludeOptions, type IncludeOptionsWithRecursive, type InferColumn, type InferColumnType, type InferColumnTypes, type InferDB, type InferDBFromSchema, type InferRefColumn, type InferRow, type InferRowType, type InferSchemaDB, type InferTableRow, type InferTables, type InferredRangeValue, InsertBuilder, IntentBuilder, type IntentBuilderState, type IntentSummary, InvalidOperationError, InvalidRelationDefinitionError, type JoinOptions, type JsonValue, type LightweightRelationsDef, type ListHierarchyOptions, type ListIndexOptions, type Logger, MSSQL_CAPABILITIES, MYSQL_CAPABILITIES, ModelIRImpl, type MutationDump, type MutationHookContext, type MutationOperation, NamingConventionMismatchError, type NegotiationResult, type NestedInclude, type SchemaColumnType$1 as NewSchemaColumnType, NotFoundError, type NqlBuilder, type NqlTag, type OnErrorHook, type OrderByInput, type OrderByRecord, type OrderBySpec, type OrmInstance, type OrmOf, type OrmOptions, type OrmOptionsWithAdapter, type OrmOptionsWithModel, type OrmOptionsWithSchema, POSTGRESQL_CAPABILITIES, type PaginateOptions, type PaginatedResult, type ParseError, type ParseResult, type ParsedRelationDef, type ParsedRelationKey, type PathOptions, type PrioritizedHook, type QueryAssertionResult, type QueryBuilder, type QueryHookContext, type QueryResultType, RELATION_META, type RangeType, RawCteQueryBuilder, type RecursiveDump, type RecursiveIncludeConfig, type RecursiveOptions, RecursiveShapeMismatchError, type RefDefinition, type RefOptions, type RelationHints, type RelationKey, type RelationMetaKey, RelationNotFoundError, type RelationObjectDef, type RelationRef, type RelationShorthand, type RelationTupleDef, type ResolvedSchema, ResolvedSchemaValidation, ResultHydrator, SQLITE_CAPABILITIES, SQL_RAW_MARKER, type Schema, type SchemaBelongsToRelation, type SchemaCardinality, type SchemaColumnDefinition, type SchemaColumnType, type SchemaConfigInput, type SchemaConstraints, type SchemaConventionsDefinition, type SchemaConversionResult, type SchemaDefinition, type SchemaDefinitionInput, SchemaValidationError$1 as SchemaError, type SchemaExtras, type SchemaFilterStrategy, type SchemaForeignKeyReference, type SchemaHasManyRelation, type SchemaHintDefinition, type SchemaHintsDefinition, type SchemaIndexDefinition, type SchemaIndexOptions, type SchemaIndexesDefinition, type SchemaManyToManyRelation, type SchemaOnDeleteAction, type SchemaOptions, type SchemaRelationDefinition, type SchemaRelationKind, type SchemaRelationsDefinition, type SchemaTableDefinition, type SchemaTableOptions, type SchemaTablesDefinition, SchemaValidationError, type SelectField, type SelfRefRoles, type SetOperationBuilder, type SimplifiedOrmOptions, type SqlRawExpression, type StreamOptions, SubqueryBuilder, SubqueryExpression, TABLE_META, type TableAssertionData, type TableDDL, type TableDef, type TableIndexes, type TableMetaKey, TableNotFoundError, type TableRef, type TraversalDirection, UnhandledTypeInDialect, UnknownDialectError, UnsafeOperationError, UnsupportedCapabilityError, UnsupportedFeatureError, UnsupportedStrategyError, UpdateBuilder, UpsertBuilder, type ValidatedResolvedSchema, type WhereFilter, WindowBuilder, aggOrderBy, and, any, array, arrayAgg, assertCapability, assertContains, assertDbColumnExists, assertDbOutput, assertDbRowsEquals, assertDbRowsMax, assertDbRowsMin, assertDbValueEquals, assertEquals, assertIntentHasGroupBy, assertIntentHasOrderBy, assertIntentHasWhere, assertIntentTable, assertIntentType, assertIntentWith, assertMatches, assertParamsEquals, assertParamsLength, assertParamsType, assertParamsValue, assertResolvedSchemaToGeneratedSchema, assertSQLColumn, assertSQLEquals, assertSQLJoin, assertSQLTable, assertSuccess, assertTypeSupported, batchValues, buildModelFromResolvedSchema, buildModelFromSchema, capitalize, caseWhen, cast, coalesce, col, composeAfterMutationHooks, composeAfterQueryHooks, composeBeforeMutationHooks, composeBeforeQueryHooks, composeOnErrorHooks, createDialectCapabilities, createHookManager, createNqlTag, createOrm, createPseudoColumnMetadata, createRawCteBuilder, createRecursiveMetadata, decapitalize, defaultLogger, defineModel, defineSchema, denseRank, detectForeignKeys, detectManyToMany, distinct, eq, every, exists, ref as exprRef, extendDialect, extractPseudoColumnKeywords, findClosestMatch, fn, fullTextSearch, getAvailableDialects, getDialectCapabilities, getLogger, getRelationKind, getSchemaFromDb, gt, gte, inArray, inSubquery, inferForeignKey, inferRelationsFromSchema, isAliasedColumn, isAllColumns, isBatchValuesRef, isBelongsTo, isCardinalityShorthand, isColumnRef, isDistinctField, isDistinctFrom, isExpressionSpec, isGeneratedSchema, isHasMany, isKnownDialect, isManyToMany, isNotNull, isNull, isOverallSuccess, isRecursiveIncludeOptions, isRecursiveRelation, isRef, isRelationObjectDef, isRelationRef, isRelationTupleDef, isResolvedSchema, isSelfReferential, isSqlRaw, isSubqueryExpression, isTableRef, isWhereIntent, lag, lead, like, literal, lt, lte, namedArg, negotiateFeatures, neq, none, normalizeSQL, normalizeSchema, not, notExists, objectToWhereIntent, op, or, outerRef, param, parseAssertionFile, parseRelationDef, parseRelationKey, pipeAfterMutationHooks, pipeAfterQueryHooks, pipeBeforeMutationHooks, pipeBeforeQueryHooks, pipeOnErrorHooks, plan, planRecursive, pluralize, rangeContainedBy, rangeContains, rangeOverlaps, rank, raw, rawExists, rawNotExists, ref$1 as ref, registerDialect, relationColumn, requiresDatabase, resetLogger, resolveQueryIndex, resolvedSchemaToGeneratedSchema, rowNumber, runAssertions, schema, schemaToModelIR, setLogger, silentLogger, singularize, some, sortByPriority, sql, star, stringAgg, subquery, supportsDDLGeneration, supportsExecution, supportsIntrospection, supportsRawSql, supportsStreaming, supportsTransactions, textScore, unary, validateAssertionBlocks, validateRecursiveInclude, validateRecursiveShape, validateTypeName, wAvg, wCount, wMax, wMin, wSum, withPriority };
8224
+ export { ASSERTION_TYPES, AdapterRequiredError, type AdjacencyOptions, type AfterMutationHook, type AfterQueryHook, type AggregateOptions, type AliasedColumn, type AliasedExprColumn, type AllColumns, AmbiguousPlanError, AmbiguousRelationError, type Assertion, type AssertionBlock, type AssertionOutcome, type AssertionQueryResult, type AssertionSummary, type AssertionType, BRAND, type BatchValuesOptions, type BatchValuesRef, type BeforeMutationHook, type BeforeQueryHook, type BrandKey, COLUMN_META, type CardinalityShorthand, CaseBuilder, type CaseValue, type ColumnDef, type ColumnMetaKey, ColumnNotFoundError, type ColumnRef, type ColumnSpec, type ColumnTypeToTS, CteBuilder, type CteDump, CteQueryBuilder, type CursorPaginateOptions, type CursorPaginatedResult, DEFAULT_CONVENTIONS, DEFAULT_FEATURE_CHECKERS, DUCKDB_CAPABILITIES, type DefaultFilters, type DefineModelOptions, DeleteBuilder, type DistinctField, type DumpMetaInput, type EdgeTableOptions, ErrorCode, type ErrorHookContext, Errors, ExecutionError, type ExprInput, ExpressionRef, type ExpressionSpec, type FeatureChecker, type FeatureUsage, type FilterOperators, type FilterValue, type FullTextSearchField, type FullTextSearchOptions, type GeneratedBelongsTo, type GeneratedColumn, type GeneratedColumnType, type GeneratedConventions, type GeneratedHasMany, type GeneratedHint, type GeneratedManyToMany, type GeneratedRelation, type GeneratedRelationKind, type GeneratedSchema, type GeneratedTable, type GetSchemaFromDbOptions, type HierarchyOptions, type HookErrorHandler, type HookManager, type HookPriority, type HydrateOptions, IRREGULAR_PLURALS, type IncludeOptions, type IncludeOptionsWithRecursive, type InferColumn, type InferColumnType, type InferColumnTypes, type InferDB, type InferDBFromSchema, type InferRefColumn, type InferRow, type InferRowType, type InferSchemaDB, type InferTableRow, type InferTables, type InferredRangeValue, InsertBuilder, IntentBuilder, type IntentBuilderState, type IntentSummary, InvalidOperationError, InvalidRelationDefinitionError, type JoinOptions, type JsonValue, type LightweightRelationsDef, type ListHierarchyOptions, type ListIndexOptions, type Logger, MSSQL_CAPABILITIES, MYSQL_CAPABILITIES, ModelIRImpl, type MutationDump, type MutationHookContext, type MutationOperation, NamingConventionMismatchError, type NegotiationResult, type NestedInclude, type SchemaColumnType$1 as NewSchemaColumnType, NotFoundError, type NqlBuilder, type NqlRawFragment, type NqlTag, type OnErrorHook, type OrderByInput, type OrderByRecord, type OrderBySpec, type OrmInstance, type OrmOf, type OrmOptions, type OrmOptionsWithAdapter, type OrmOptionsWithModel, type OrmOptionsWithSchema, POSTGRESQL_CAPABILITIES, type PaginateOptions, type PaginatedResult, type ParseError, type ParseResult, type ParsedRelationDef, type ParsedRelationKey, type PathOptions, type PrioritizedHook, type QueryAssertionResult, type QueryBuilder, type QueryHookContext, type QueryResultType, RELATION_META, type RangeType, RawCteQueryBuilder, type RecursiveDump, type RecursiveIncludeConfig, type RecursiveOptions, RecursiveShapeMismatchError, type RefDefinition, type RefOptions, type RelationHints, type RelationKey, type RelationMetaKey, RelationNotFoundError, type RelationObjectDef, type RelationPathIncludeNode, type RelationPathUsage, type RelationRef, type RelationShorthand, type RelationTupleDef, type ResolvedSchema, ResolvedSchemaValidation, ResultHydrator, SQLITE_CAPABILITIES, SQL_RAW_MARKER, type Schema, type SchemaBelongsToRelation, type SchemaCardinality, type SchemaColumnDefinition, type SchemaColumnType, type SchemaConfigInput, type SchemaConstraints, type SchemaConventionsDefinition, type SchemaConversionResult, type SchemaDefinition, type SchemaDefinitionInput, SchemaValidationError$1 as SchemaError, type SchemaExtras, type SchemaFilterStrategy, type SchemaForeignKeyReference, type SchemaHasManyRelation, type SchemaHintDefinition, type SchemaHintsDefinition, type SchemaIndexDefinition, type SchemaIndexOptions, type SchemaIndexesDefinition, type SchemaManyToManyRelation, type SchemaOnDeleteAction, type SchemaOptions, type SchemaRelationDefinition, type SchemaRelationKind, type SchemaRelationsDefinition, type SchemaTableDefinition, type SchemaTableOptions, type SchemaTablesDefinition, SchemaValidationError, type SelectField, type SelfRefRoles, type SetOperationBuilder, type SimplifiedOrmOptions, type SqlRawExpression, type StreamOptions, SubqueryBuilder, SubqueryExpression, TABLE_META, type TableAssertionData, type TableDDL, type TableDef, type TableIndexes, type TableMetaKey, TableNotFoundError, type TableRef, type TraversalDirection, UnhandledTypeInDialect, UnknownDialectError, UnsafeOperationError, UnsupportedCapabilityError, UnsupportedFeatureError, UnsupportedStrategyError, UpdateBuilder, UpsertBuilder, type ValidatedResolvedSchema, type WhereFilter, WindowBuilder, aggOrderBy, and, any, array, arrayAgg, assertCapability, assertContains, assertDbColumnExists, assertDbOutput, assertDbRowsEquals, assertDbRowsMax, assertDbRowsMin, assertDbValueEquals, assertEquals, assertIntentHasGroupBy, assertIntentHasOrderBy, assertIntentHasWhere, assertIntentTable, assertIntentType, assertIntentWith, assertMatches, assertParamsEquals, assertParamsLength, assertParamsType, assertParamsValue, assertResolvedSchemaToGeneratedSchema, assertSQLColumn, assertSQLEquals, assertSQLJoin, assertSQLTable, assertSuccess, assertTypeSupported, batchValues, buildModelFromResolvedSchema, buildModelFromSchema, capitalize, caseWhen, cast, coalesce, col, composeAfterMutationHooks, composeAfterQueryHooks, composeBeforeMutationHooks, composeBeforeQueryHooks, composeOnErrorHooks, countDistinctRelationPathsByName, createDialectCapabilities, createHookManager, createNqlTag, createOrm, createPseudoColumnMetadata, createRawCteBuilder, createRecursiveMetadata, decapitalize, defaultLogger, defineModel, defineSchema, denseRank, deriveRelationPathFromIntentPath, detectForeignKeys, detectManyToMany, distinct, eq, every, exists, ref as exprRef, extendDialect, extractPseudoColumnKeywords, findClosestMatch, fn, fullTextSearch, getAvailableDialects, getDialectCapabilities, getLogger, getRelationKind, getSchemaFromDb, gt, gte, inArray, inSubquery, inferForeignKey, inferRelationsFromSchema, isAliasedColumn, isAllColumns, isBatchValuesRef, isBelongsTo, isCardinalityShorthand, isColumnRef, isDistinctField, isDistinctFrom, isExpressionSpec, isGeneratedSchema, isHasMany, isKnownDialect, isManyToMany, isNotNull, isNull, isOverallSuccess, isRecursiveIncludeOptions, isRecursiveRelation, isRef, isRelationObjectDef, isRelationRef, isRelationTupleDef, isResolvedSchema, isSelfReferential, isSqlRaw, isSubqueryExpression, isTableRef, isWhereIntent, lag, lead, like, literal, lt, lte, namedArg, negotiateFeatures, neq, none, normalizeSQL, normalizeSchema, not, notExists, nqlRaw, objectToWhereIntent, op, or, outerRef, param, parseAssertionFile, parseRelationDef, parseRelationKey, pipeAfterMutationHooks, pipeAfterQueryHooks, pipeBeforeMutationHooks, pipeBeforeQueryHooks, pipeOnErrorHooks, plan, planRecursive, pluralize, rangeContainedBy, rangeContains, rangeOverlaps, rank, raw, rawExists, rawNotExists, ref$1 as ref, registerDialect, relationColumn, requiresDatabase, resetLogger, resolveQueryIndex, resolvedSchemaToGeneratedSchema, rowNumber, runAssertions, schema, schemaToModelIR, setLogger, silentLogger, singularize, some, sortByPriority, sql, star, stringAgg, subquery, supportsDDLGeneration, supportsExecution, supportsIntrospection, supportsRawSql, supportsStreaming, supportsTransactions, textScore, unary, validateAssertionBlocks, validateRecursiveInclude, validateRecursiveShape, validateTypeName, wAvg, wCount, wMax, wMin, wSum, withPriority };
package/dist/index.js CHANGED
@@ -2601,7 +2601,8 @@ function processInclude(include, sourceTable, model, state, opts, intentPath, de
2601
2601
  return;
2602
2602
  }
2603
2603
  const includePath = `${sourceTable}.${relation.name}`;
2604
- if (state.visitedIncludes.has(includePath)) {
2604
+ const isSelfReferentialRelation = relation.source === relation.target;
2605
+ if (!isSelfReferentialRelation && state.visitedIncludes.has(includePath)) {
2605
2606
  state.warnings.push({
2606
2607
  code: "CIRCULAR_INCLUDE",
2607
2608
  message: `Circular include detected: ${includePath}`,
@@ -2609,12 +2610,12 @@ function processInclude(include, sourceTable, model, state, opts, intentPath, de
2609
2610
  });
2610
2611
  return;
2611
2612
  }
2612
- state.visitedIncludes.add(includePath);
2613
+ if (!isSelfReferentialRelation) state.visitedIncludes.add(includePath);
2613
2614
  const relationPath = `${sourceTable}.${relation.name}`;
2614
2615
  const paths = state.relationAccessCounts.get(relationPath) ?? [];
2615
2616
  paths.push(intentPath);
2616
2617
  state.relationAccessCounts.set(relationPath, paths);
2617
- const isRecursiveInclude = (!!include.recursive || !!relation.recursive) && relation.source === relation.target;
2618
+ const isRecursiveInclude = (!!include.recursive || !!relation.recursive) && isSelfReferentialRelation;
2618
2619
  let includeStrategy;
2619
2620
  if (isRecursiveInclude) {
2620
2621
  if (opts.dialectCapabilities?.supportsRecursiveCTE === false) {
@@ -2749,7 +2750,7 @@ function processInclude(include, sourceTable, model, state, opts, intentPath, de
2749
2750
  }
2750
2751
  }
2751
2752
  }
2752
- state.visitedIncludes.delete(includePath);
2753
+ if (!isSelfReferentialRelation) state.visitedIncludes.delete(includePath);
2753
2754
  }
2754
2755
  function disambiguateRelation(relationName, sourceTable, model, state, opts, _intentPath, viaHint) {
2755
2756
  const directRelation = model.getRelation(`${sourceTable}.${relationName}`);
@@ -4105,6 +4106,12 @@ function coalesce(fields, as) {
4105
4106
  };
4106
4107
  }
4107
4108
  function raw(sqlFragment, as) {
4109
+ if (typeof as !== "string" || as.trim().length === 0) {
4110
+ throw new InvalidOperationError(
4111
+ "raw",
4112
+ "raw() requires an alias as second argument, e.g. raw('COUNT(*)', 'count')"
4113
+ );
4114
+ }
4108
4115
  validateIdentifier(as, "column");
4109
4116
  return {
4110
4117
  __expr: true,
@@ -6126,53 +6133,131 @@ function negotiateFeatures(model, capabilities, behavior = "warning", checkers =
6126
6133
  }
6127
6134
 
6128
6135
  // src/dx/nql.ts
6129
- import { compile as nqlCompile } from "@dbsp/nql";
6130
- function toNqlLiteral(value, index) {
6131
- if (value === null) {
6132
- return "null";
6136
+ import {
6137
+ NqlLexer,
6138
+ compile as nqlCompile
6139
+ } from "@dbsp/nql";
6140
+ import { NQL_INTERNAL_COMPILER_OPTIONS } from "@dbsp/types/internal";
6141
+ var NQL_RAW_FRAGMENT = /* @__PURE__ */ Symbol("NqlRawFragment");
6142
+ function nqlRaw(fragment) {
6143
+ if (typeof fragment !== "string") {
6144
+ throw new TypeError("nqlRaw() expects a string fragment");
6145
+ }
6146
+ const raw2 = { fragment };
6147
+ Object.defineProperty(raw2, NQL_RAW_FRAGMENT, {
6148
+ value: true,
6149
+ enumerable: false
6150
+ });
6151
+ return Object.freeze(raw2);
6152
+ }
6153
+ function isNqlRawFragment(value) {
6154
+ if (typeof value !== "object" || value === null) {
6155
+ return false;
6133
6156
  }
6134
- switch (typeof value) {
6135
- case "boolean":
6136
- return value ? "true" : "false";
6137
- case "number": {
6138
- if (!Number.isFinite(value)) {
6139
- throw new Error(
6140
- `nql\`...\`: cannot interpolate non-finite number (${value}) at position ${index}. Only finite numbers are supported. For dynamic NQL structure use the builder API (orm.select()). See issue #134.`
6141
- );
6142
- }
6143
- const s = value < 0 ? `-${Math.abs(value)}` : String(value);
6144
- if (/[eE]/.test(s)) {
6145
- throw new Error(
6146
- `nql\`...\`: number ${value} at position ${index} has no exact NQL numeric literal form (exponential notation). Convert it yourself or use the builder API (orm.select()). See issue #134.`
6147
- );
6148
- }
6149
- return s;
6157
+ if (!Object.hasOwn(value, NQL_RAW_FRAGMENT)) {
6158
+ return false;
6159
+ }
6160
+ return value[NQL_RAW_FRAGMENT] === true;
6161
+ }
6162
+ function isInsideGeneratedRange(start, end, generatedRanges) {
6163
+ return generatedRanges.some(
6164
+ (range) => start >= range.start && end <= range.end
6165
+ );
6166
+ }
6167
+ function findOverlappingGeneratedRange(start, end, generatedRanges) {
6168
+ return generatedRanges.find(
6169
+ (range) => start < range.end && end > range.start
6170
+ );
6171
+ }
6172
+ function findExactGeneratedRange(name, start, end, generatedRanges) {
6173
+ return generatedRanges.find(
6174
+ (range) => range.name === name && range.start === start && range.end === end
6175
+ );
6176
+ }
6177
+ function generatedParamLexError(name) {
6178
+ return `Generated NQL parameter :${name} was not recognized as exactly one NamedParam token after raw-fragment assembly; check adjacent nqlRaw() fragments for quotes, comments, or identifier text that can swallow the placeholder.`;
6179
+ }
6180
+ function findInternalParamSourceError(query, generatedRanges) {
6181
+ const lexResult = NqlLexer.tokenize(query);
6182
+ const generatedTokenCounts = new Map(
6183
+ generatedRanges.map((range) => [range.name, 0])
6184
+ );
6185
+ for (const token of lexResult.tokens) {
6186
+ if (token.tokenType.name !== "NamedParam") {
6187
+ continue;
6150
6188
  }
6151
- case "string": {
6152
- if (value.includes("\n") || value.includes("\r")) {
6153
- throw new Error(
6154
- `nql\`...\`: cannot interpolate a string containing a newline at position ${index}. NQL string literals do not support raw newline characters. For dynamic NQL structure use the builder API (orm.select()). See issue #134.`
6155
- );
6156
- }
6157
- const escaped = value.replaceAll("'", "''");
6158
- return `'${escaped}'`;
6189
+ const name = token.image.slice(1);
6190
+ if (!name.startsWith("__p")) {
6191
+ continue;
6159
6192
  }
6160
- default: {
6161
- const typeName = value === void 0 ? "undefined" : typeof value;
6162
- throw new Error(
6163
- `nql\`...\`: cannot interpolate value of type "${typeName}" at position ${index}. Only string, number, boolean, and null are supported; for dynamic NQL fragments use the builder API (orm.select()). See issue #134.`
6193
+ const start = token.startOffset;
6194
+ const end = (token.endOffset ?? start + token.image.length - 1) + 1;
6195
+ const exactGeneratedRange = findExactGeneratedRange(
6196
+ name,
6197
+ start,
6198
+ end,
6199
+ generatedRanges
6200
+ );
6201
+ if (exactGeneratedRange) {
6202
+ generatedTokenCounts.set(
6203
+ exactGeneratedRange.name,
6204
+ (generatedTokenCounts.get(exactGeneratedRange.name) ?? 0) + 1
6164
6205
  );
6206
+ continue;
6207
+ }
6208
+ const overlappingGeneratedRange = findOverlappingGeneratedRange(
6209
+ start,
6210
+ end,
6211
+ generatedRanges
6212
+ );
6213
+ if (overlappingGeneratedRange) {
6214
+ return generatedParamLexError(overlappingGeneratedRange.name);
6165
6215
  }
6216
+ if (!isInsideGeneratedRange(start, end, generatedRanges)) {
6217
+ return `Reserved NQL parameter namespace "__p" cannot be referenced by user source (${token.image}).`;
6218
+ }
6219
+ }
6220
+ for (const range of generatedRanges) {
6221
+ if (generatedTokenCounts.get(range.name) !== 1) {
6222
+ return generatedParamLexError(range.name);
6223
+ }
6224
+ }
6225
+ return void 0;
6226
+ }
6227
+ function assembleNqlTemplate(strings, values) {
6228
+ const params = /* @__PURE__ */ Object.create(null);
6229
+ const generatedRanges = [];
6230
+ let query = strings[0] ?? "";
6231
+ let boundIndex = 0;
6232
+ for (let i = 0; i < values.length; i++) {
6233
+ const value = values[i];
6234
+ if (isNqlRawFragment(value)) {
6235
+ query += value.fragment;
6236
+ } else {
6237
+ const name = `__p${boundIndex++}`;
6238
+ const placeholder = `:${name}`;
6239
+ const start = query.length;
6240
+ query += placeholder;
6241
+ generatedRanges.push({ name, start, end: start + placeholder.length });
6242
+ params[name] = value;
6243
+ }
6244
+ query += strings[i + 1] ?? "";
6166
6245
  }
6246
+ return {
6247
+ query,
6248
+ params,
6249
+ hasBoundParams: boundIndex > 0,
6250
+ sourceError: findInternalParamSourceError(query, generatedRanges)
6251
+ };
6167
6252
  }
6168
6253
  function createNqlTag(schemaDefinition, model, adapter, schemaName) {
6169
6254
  return function nql(strings, ...values) {
6170
- let query = strings[0] ?? "";
6171
- for (let i = 0; i < values.length; i++) {
6172
- query += toNqlLiteral(values[i], i) + (strings[i + 1] ?? "");
6173
- }
6255
+ const assembled = assembleNqlTemplate(strings, values);
6174
6256
  return new NqlBuilderImpl(
6175
- query,
6257
+ assembled.query,
6258
+ assembled.params,
6259
+ assembled.hasBoundParams,
6260
+ assembled.sourceError,
6176
6261
  schemaDefinition,
6177
6262
  model,
6178
6263
  adapter,
@@ -6183,13 +6268,19 @@ function createNqlTag(schemaDefinition, model, adapter, schemaName) {
6183
6268
  var NqlBuilderImpl = class {
6184
6269
  _intent;
6185
6270
  query;
6271
+ params;
6272
+ hasBoundParams;
6273
+ sourceError;
6186
6274
  schemaDefinition;
6187
6275
  model;
6188
6276
  // biome-ignore lint/correctness/noUnusedPrivateClassMembers: Reserved for future schema-scoping support
6189
6277
  _schemaName;
6190
6278
  adapter;
6191
- constructor(query, schemaDefinition, model, adapter, schemaName) {
6279
+ constructor(query, params, hasBoundParams, sourceError, schemaDefinition, model, adapter, schemaName) {
6192
6280
  this.query = query;
6281
+ this.params = params;
6282
+ this.hasBoundParams = hasBoundParams;
6283
+ this.sourceError = sourceError;
6193
6284
  this.schemaDefinition = schemaDefinition;
6194
6285
  this.model = model;
6195
6286
  this.adapter = adapter;
@@ -6199,7 +6290,14 @@ var NqlBuilderImpl = class {
6199
6290
  if (this._intent) {
6200
6291
  return this._intent;
6201
6292
  }
6202
- const compilerOptions = extractPseudoColumnKeywords(this.model);
6293
+ if (this.sourceError !== void 0) {
6294
+ throw new Error(`NQL compilation failed: ${this.sourceError}`);
6295
+ }
6296
+ const compilerOptions = {
6297
+ ...extractPseudoColumnKeywords(this.model) ?? {},
6298
+ params: this.params,
6299
+ [NQL_INTERNAL_COMPILER_OPTIONS]: { allowInternalParams: true }
6300
+ };
6203
6301
  const result = nqlCompile(
6204
6302
  this.query,
6205
6303
  this.schemaDefinition,
@@ -6208,7 +6306,8 @@ var NqlBuilderImpl = class {
6208
6306
  );
6209
6307
  if (!result.success) {
6210
6308
  const errors = result.errors?.map((e) => e.message).join(", ") ?? "Unknown error";
6211
- throw new Error(`NQL compilation failed: ${errors}`);
6309
+ const rawHint = this.hasBoundParams ? " If an interpolation was intended as NQL structure, wrap a trusted fragment with nqlRaw()." : "";
6310
+ throw new Error(`NQL compilation failed: ${errors}${rawHint}`);
6212
6311
  }
6213
6312
  if (result.ast?.mutation && !result.ast?.query) {
6214
6313
  throw new Error(
@@ -6224,12 +6323,15 @@ var NqlBuilderImpl = class {
6224
6323
  toIntentIR() {
6225
6324
  return this.compile();
6226
6325
  }
6227
- plan() {
6326
+ planInternal() {
6228
6327
  const intent = this.compile();
6229
6328
  return plan(intent, this.model);
6230
6329
  }
6330
+ plan() {
6331
+ return this.planInternal();
6332
+ }
6231
6333
  dump(meta) {
6232
- const planReport = this.plan();
6334
+ const planReport = this.planInternal();
6233
6335
  if (!this.adapter) {
6234
6336
  return {
6235
6337
  plan: planReport,
@@ -6272,7 +6374,7 @@ var NqlBuilderImpl = class {
6272
6374
  "Cannot execute query: no adapter configured. Pass an adapter to createOrm() or use .toIntentIR() / .plan() for debugging."
6273
6375
  );
6274
6376
  }
6275
- const planReport = this.plan();
6377
+ const planReport = this.planInternal();
6276
6378
  const compiled = this.adapter.compile(planReport);
6277
6379
  return this.adapter.execute(compiled);
6278
6380
  }
@@ -6661,8 +6763,152 @@ function hydrateJsonAggIncludes(results, planReport) {
6661
6763
  }
6662
6764
  }
6663
6765
 
6766
+ // src/dx/relation-paths.ts
6767
+ function nonEmptyString(value) {
6768
+ return typeof value === "string" && value.length > 0 ? value : void 0;
6769
+ }
6770
+ function includeNode(value) {
6771
+ return value !== null && typeof value === "object" ? value : void 0;
6772
+ }
6773
+ function parseIntentPathIndexes(intentPath) {
6774
+ const indexes = [];
6775
+ const indexPattern = /include\[(\d+)\]/g;
6776
+ let execResult = indexPattern.exec(intentPath);
6777
+ while (execResult !== null) {
6778
+ const rawIndex = execResult[1];
6779
+ if (rawIndex === void 0) break;
6780
+ indexes.push(parseInt(rawIndex, 10));
6781
+ execResult = indexPattern.exec(intentPath);
6782
+ }
6783
+ return indexes;
6784
+ }
6785
+ function deriveRelationPathFromIntentPath(includes, intentPath, fallbackRelation) {
6786
+ if (includes && intentPath) {
6787
+ let current = includes;
6788
+ const path = [];
6789
+ for (const index of parseIntentPathIndexes(intentPath)) {
6790
+ const item = includeNode(current[index]);
6791
+ if (!item) break;
6792
+ const segment = nonEmptyString(item.via) ?? nonEmptyString(item.relation);
6793
+ if (!segment) break;
6794
+ path.push(segment);
6795
+ current = Array.isArray(item.include) ? item.include : [];
6796
+ }
6797
+ if (path.length > 0) return path.join(".");
6798
+ }
6799
+ return nonEmptyString(fallbackRelation);
6800
+ }
6801
+ function countDistinctRelationPathsByName(usages) {
6802
+ const pathsByRelationName = /* @__PURE__ */ new Map();
6803
+ for (const usage of usages) {
6804
+ const relationName = nonEmptyString(usage.relationName);
6805
+ if (!relationName) continue;
6806
+ const relationPath = nonEmptyString(usage.relationPath) ?? relationName;
6807
+ const paths = pathsByRelationName.get(relationName) ?? /* @__PURE__ */ new Set();
6808
+ paths.add(relationPath);
6809
+ pathsByRelationName.set(relationName, paths);
6810
+ }
6811
+ const counts = /* @__PURE__ */ new Map();
6812
+ for (const [relationName, paths] of pathsByRelationName) {
6813
+ counts.set(relationName, paths.size);
6814
+ }
6815
+ return counts;
6816
+ }
6817
+
6664
6818
  // src/dx/result-hydrator.ts
6665
6819
  var COMPOSITE_KEY_SEP = "\0";
6820
+ function stringProp(source, key) {
6821
+ const value = source?.[key];
6822
+ return typeof value === "string" && value.length > 0 ? value : void 0;
6823
+ }
6824
+ function resolveJoinIncludePath(planReport, decision) {
6825
+ const context = decision.context;
6826
+ const decisionRecord = decision;
6827
+ const contextRecord = context;
6828
+ const explicitRelationPath = stringProp(decisionRecord, "relationPath") ?? stringProp(contextRecord, "relationPath");
6829
+ if (explicitRelationPath) return explicitRelationPath;
6830
+ const fallbackRelation = stringProp(contextRecord, "relation") ?? stringProp(contextRecord, "includeAlias") ?? stringProp(decisionRecord, "relationName");
6831
+ const intentPath = stringProp(contextRecord, "intentPath");
6832
+ return deriveRelationPathFromIntentPath(
6833
+ Array.isArray(planReport.intent?.include) ? planReport.intent.include : void 0,
6834
+ intentPath,
6835
+ fallbackRelation
6836
+ );
6837
+ }
6838
+ function lastRelationSegment(path) {
6839
+ const segments = path.split(".");
6840
+ return segments[segments.length - 1] ?? path;
6841
+ }
6842
+ function resolveJoinRelationName(decision, relationPath) {
6843
+ const context = decision.context;
6844
+ const decisionRecord = decision;
6845
+ return stringProp(context, "relation") ?? stringProp(context, "includeAlias") ?? stringProp(decisionRecord, "relationName") ?? lastRelationSegment(relationPath);
6846
+ }
6847
+ function collectJoinHydrationInfos(planReport) {
6848
+ const candidates = [];
6849
+ for (const decision of planReport.decisions) {
6850
+ if (decision.type !== "include-strategy" || decision.choice !== "join") {
6851
+ continue;
6852
+ }
6853
+ const relationPath = resolveJoinIncludePath(planReport, decision);
6854
+ if (!relationPath) continue;
6855
+ const relationName = resolveJoinRelationName(decision, relationPath);
6856
+ const decisionRecord = decision;
6857
+ const contextRecord = decision.context;
6858
+ const hydrationPrefix = stringProp(decisionRecord, "hydrationPrefix") ?? stringProp(contextRecord, "hydrationPrefix");
6859
+ candidates.push({
6860
+ relationName,
6861
+ relationPath,
6862
+ ...hydrationPrefix && { hydrationPrefix }
6863
+ });
6864
+ }
6865
+ const pathCountsByRelationName = countDistinctRelationPathsByName(candidates);
6866
+ const infosByPath = /* @__PURE__ */ new Map();
6867
+ for (const candidate of candidates) {
6868
+ if (infosByPath.has(candidate.relationPath)) continue;
6869
+ const usesFullPath = (pathCountsByRelationName.get(candidate.relationName) ?? 0) > 1;
6870
+ const keyPrefix = candidate.hydrationPrefix ?? (usesFullPath ? candidate.relationPath : candidate.relationName);
6871
+ if (!keyPrefix) continue;
6872
+ infosByPath.set(candidate.relationPath, {
6873
+ relationPath: candidate.relationPath,
6874
+ segments: candidate.relationPath.split("."),
6875
+ keyPrefix,
6876
+ keyPrefixWithDot: `${keyPrefix}.`
6877
+ });
6878
+ }
6879
+ return [...infosByPath.values()];
6880
+ }
6881
+ function findLongestJoinInfo(key, infos) {
6882
+ let best;
6883
+ for (const info of infos) {
6884
+ if (!key.startsWith(info.keyPrefixWithDot)) continue;
6885
+ if (!best || info.keyPrefixWithDot.length > best.keyPrefixWithDot.length) {
6886
+ best = info;
6887
+ }
6888
+ }
6889
+ return best;
6890
+ }
6891
+ function assignNestedValue(target, path, value) {
6892
+ let cursor = target;
6893
+ for (let i = 0; i < path.length - 1; i++) {
6894
+ const segment = path[i];
6895
+ if (!segment) return;
6896
+ const existing = cursor[segment];
6897
+ if (existing === null) {
6898
+ return;
6899
+ } else if (typeof existing !== "object" || Array.isArray(existing)) {
6900
+ cursor[segment] = {};
6901
+ }
6902
+ cursor = cursor[segment];
6903
+ }
6904
+ const leaf = path[path.length - 1];
6905
+ if (leaf) cursor[leaf] = value;
6906
+ }
6907
+ function assignColumnValue(target, columnPath, value) {
6908
+ const segments = columnPath.split(".").filter(Boolean);
6909
+ if (segments.length === 0) return;
6910
+ assignNestedValue(target, segments, value);
6911
+ }
6666
6912
  var ResultHydrator = class {
6667
6913
  model;
6668
6914
  from;
@@ -6739,51 +6985,42 @@ var ResultHydrator = class {
6739
6985
  * E2E-004: JOIN strategy for to-one relations returns columns like "author.id", "author.name".
6740
6986
  */
6741
6987
  hydrateJoinIncludes(results, planReport) {
6742
- const joinDecisions = planReport.decisions.filter(
6743
- (d) => d.type === "include-strategy" && d.choice === "join"
6988
+ const joinInfos = collectJoinHydrationInfos(planReport);
6989
+ if (joinInfos.length === 0) return;
6990
+ const infosByDepth = [...joinInfos].sort(
6991
+ (a, b) => a.segments.length - b.segments.length
6744
6992
  );
6745
- if (joinDecisions.length === 0) {
6746
- return;
6747
- }
6748
- const joinInfos = joinDecisions.map((d) => d.context?.relation).filter((r) => typeof r === "string").map((relation) => ({ relation, prefix: `${relation}.` }));
6749
- if (joinInfos.length === 0) {
6750
- return;
6751
- }
6752
- const joinInfosWithCache = joinInfos.map((info) => ({
6753
- ...info,
6754
- cachedKeys: null
6755
- }));
6756
- if (results.length > 0) {
6757
- const firstRow = results.find(
6758
- (r) => typeof r === "object" && r !== null
6759
- );
6760
- if (firstRow) {
6761
- const firstRecord = firstRow;
6762
- const allKeys = Object.keys(firstRecord);
6763
- for (const info of joinInfosWithCache) {
6764
- info.cachedKeys = allKeys.filter((k) => k.startsWith(info.prefix));
6765
- }
6766
- }
6767
- }
6768
6993
  for (const row of results) {
6769
6994
  if (typeof row !== "object" || row === null) {
6770
6995
  continue;
6771
6996
  }
6772
6997
  const record2 = row;
6773
- for (const info of joinInfosWithCache) {
6774
- const keys = info.cachedKeys && info.cachedKeys.length > 0 ? info.cachedKeys : Object.keys(record2).filter((k) => k.startsWith(info.prefix));
6775
- if (keys.length === 0) continue;
6776
- const nestedObj = {};
6777
- let allNull = true;
6778
- for (const key of keys) {
6779
- const val = record2[key];
6780
- if (val !== void 0) {
6781
- nestedObj[key.slice(info.prefix.length)] = val;
6782
- delete record2[key];
6783
- if (val !== null) allNull = false;
6784
- }
6998
+ const valuesByPath = /* @__PURE__ */ new Map();
6999
+ for (const key of Object.keys(record2)) {
7000
+ const info = findLongestJoinInfo(key, joinInfos);
7001
+ if (!info) continue;
7002
+ const value = record2[key];
7003
+ let entry = valuesByPath.get(info.relationPath);
7004
+ if (!entry) {
7005
+ entry = { values: {}, allNull: true };
7006
+ valuesByPath.set(info.relationPath, entry);
6785
7007
  }
6786
- record2[info.relation] = allNull ? null : nestedObj;
7008
+ assignColumnValue(
7009
+ entry.values,
7010
+ key.slice(info.keyPrefixWithDot.length),
7011
+ value
7012
+ );
7013
+ if (value !== null && value !== void 0) entry.allNull = false;
7014
+ delete record2[key];
7015
+ }
7016
+ for (const info of infosByDepth) {
7017
+ const entry = valuesByPath.get(info.relationPath);
7018
+ if (!entry) continue;
7019
+ assignNestedValue(
7020
+ record2,
7021
+ info.segments,
7022
+ entry.allNull ? null : entry.values
7023
+ );
6787
7024
  }
6788
7025
  }
6789
7026
  }
@@ -11474,6 +11711,7 @@ export {
11474
11711
  composeBeforeMutationHooks,
11475
11712
  composeBeforeQueryHooks,
11476
11713
  composeOnErrorHooks,
11714
+ countDistinctRelationPathsByName,
11477
11715
  createDialectCapabilities,
11478
11716
  createHookManager,
11479
11717
  createNqlTag,
@@ -11486,6 +11724,7 @@ export {
11486
11724
  defineModel,
11487
11725
  defineSchema,
11488
11726
  denseRank,
11727
+ deriveRelationPathFromIntentPath,
11489
11728
  detectForeignKeys,
11490
11729
  detectManyToMany,
11491
11730
  distinct,
@@ -11588,6 +11827,7 @@ export {
11588
11827
  normalizeSchema,
11589
11828
  not,
11590
11829
  notExists,
11830
+ nqlRaw,
11591
11831
  objectToWhereIntent,
11592
11832
  op,
11593
11833
  or,