@quereus/quereus 0.7.2 → 0.7.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +3 -3
- package/dist/src/common/constants.js.map +0 -1
- package/dist/src/common/datatype.js.map +0 -1
- package/dist/src/common/errors.js.map +0 -1
- package/dist/src/common/json-types.js.map +0 -1
- package/dist/src/common/logger.js.map +0 -1
- package/dist/src/common/type-inference.js.map +0 -1
- package/dist/src/common/types.js.map +0 -1
- package/dist/src/core/database-options.js.map +0 -1
- package/dist/src/core/database.js.map +0 -1
- package/dist/src/core/param.js.map +0 -1
- package/dist/src/core/statement.js.map +0 -1
- package/dist/src/func/builtins/aggregate.js.map +0 -1
- package/dist/src/func/builtins/builtin-window-functions.js.map +0 -1
- package/dist/src/func/builtins/conversion.js.map +0 -1
- package/dist/src/func/builtins/datetime.js.map +0 -1
- package/dist/src/func/builtins/explain.js.map +0 -1
- package/dist/src/func/builtins/generation.js.map +0 -1
- package/dist/src/func/builtins/index.js.map +0 -1
- package/dist/src/func/builtins/json-helpers.js.map +0 -1
- package/dist/src/func/builtins/json-tvf.js.map +0 -1
- package/dist/src/func/builtins/json.js.map +0 -1
- package/dist/src/func/builtins/scalar.js.map +0 -1
- package/dist/src/func/builtins/schema.js.map +0 -1
- package/dist/src/func/builtins/string.js.map +0 -1
- package/dist/src/func/builtins/timespan.js.map +0 -1
- package/dist/src/func/context.js.map +0 -1
- package/dist/src/func/registration.js.map +0 -1
- package/dist/src/index.js.map +0 -1
- package/dist/src/parser/ast.js.map +0 -1
- package/dist/src/parser/index.js.map +0 -1
- package/dist/src/parser/lexer.js.map +0 -1
- package/dist/src/parser/parser.js.map +0 -1
- package/dist/src/parser/utils.js.map +0 -1
- package/dist/src/parser/visitor.js.map +0 -1
- package/dist/src/planner/analysis/binding-collector.js.map +0 -1
- package/dist/src/planner/analysis/const-evaluator.js.map +0 -1
- package/dist/src/planner/analysis/const-pass.js.map +0 -1
- package/dist/src/planner/analysis/constraint-extractor.js.map +0 -1
- package/dist/src/planner/analysis/predicate-normalizer.js.map +0 -1
- package/dist/src/planner/building/alter-table.js.map +0 -1
- package/dist/src/planner/building/block.js.map +0 -1
- package/dist/src/planner/building/constraint-builder.js.map +0 -1
- package/dist/src/planner/building/create-assertion.js.map +0 -1
- package/dist/src/planner/building/create-view.js.map +0 -1
- package/dist/src/planner/building/ddl.js.map +0 -1
- package/dist/src/planner/building/declare-schema.js.map +0 -1
- package/dist/src/planner/building/delete.js.map +0 -1
- package/dist/src/planner/building/drop-assertion.js.map +0 -1
- package/dist/src/planner/building/drop-table.js.map +0 -1
- package/dist/src/planner/building/drop-view.js.map +0 -1
- package/dist/src/planner/building/expression.js.map +0 -1
- package/dist/src/planner/building/function-call.js.map +0 -1
- package/dist/src/planner/building/insert.js.map +0 -1
- package/dist/src/planner/building/pragma.js.map +0 -1
- package/dist/src/planner/building/schema-resolution.js.map +0 -1
- package/dist/src/planner/building/select-aggregates.js.map +0 -1
- package/dist/src/planner/building/select-compound.js.map +0 -1
- package/dist/src/planner/building/select-context.js.map +0 -1
- package/dist/src/planner/building/select-modifiers.js.map +0 -1
- package/dist/src/planner/building/select-projections.js.map +0 -1
- package/dist/src/planner/building/select-window.js.map +0 -1
- package/dist/src/planner/building/select.js.map +0 -1
- package/dist/src/planner/building/table-function.js.map +0 -1
- package/dist/src/planner/building/table.js.map +0 -1
- package/dist/src/planner/building/transaction.js.map +0 -1
- package/dist/src/planner/building/update.js.map +0 -1
- package/dist/src/planner/building/with.js.map +0 -1
- package/dist/src/planner/cache/correlation-detector.js.map +0 -1
- package/dist/src/planner/cache/materialization-advisory.js.map +0 -1
- package/dist/src/planner/cache/reference-graph.js.map +0 -1
- package/dist/src/planner/cost/index.js.map +0 -1
- package/dist/src/planner/debug/logger-utils.js.map +0 -1
- package/dist/src/planner/debug.js.map +0 -1
- package/dist/src/planner/framework/characteristics.js.map +0 -1
- package/dist/src/planner/framework/context.js.map +0 -1
- package/dist/src/planner/framework/pass.js.map +0 -1
- package/dist/src/planner/framework/physical-utils.js.map +0 -1
- package/dist/src/planner/framework/registry.js.map +0 -1
- package/dist/src/planner/framework/trace.js.map +0 -1
- package/dist/src/planner/nodes/add-constraint-node.js.map +0 -1
- package/dist/src/planner/nodes/aggregate-function.js.map +0 -1
- package/dist/src/planner/nodes/aggregate-node.js.map +0 -1
- package/dist/src/planner/nodes/array-index-node.js.map +0 -1
- package/dist/src/planner/nodes/block.js.map +0 -1
- package/dist/src/planner/nodes/cache-node.js.map +0 -1
- package/dist/src/planner/nodes/constraint-check-node.js.map +0 -1
- package/dist/src/planner/nodes/create-assertion-node.js.map +0 -1
- package/dist/src/planner/nodes/create-index-node.js.map +0 -1
- package/dist/src/planner/nodes/create-table-node.js.map +0 -1
- package/dist/src/planner/nodes/create-view-node.js.map +0 -1
- package/dist/src/planner/nodes/cte-node.js.map +0 -1
- package/dist/src/planner/nodes/cte-reference-node.js.map +0 -1
- package/dist/src/planner/nodes/declarative-schema.js.map +0 -1
- package/dist/src/planner/nodes/delete-node.js.map +0 -1
- package/dist/src/planner/nodes/distinct-node.js.map +0 -1
- package/dist/src/planner/nodes/dml-executor-node.js.map +0 -1
- package/dist/src/planner/nodes/drop-assertion-node.js.map +0 -1
- package/dist/src/planner/nodes/drop-table-node.js.map +0 -1
- package/dist/src/planner/nodes/drop-view-node.js.map +0 -1
- package/dist/src/planner/nodes/filter.js.map +0 -1
- package/dist/src/planner/nodes/function.js.map +0 -1
- package/dist/src/planner/nodes/insert-node.js.map +0 -1
- package/dist/src/planner/nodes/internal-recursive-cte-ref-node.js.map +0 -1
- package/dist/src/planner/nodes/join-node.js.map +0 -1
- package/dist/src/planner/nodes/limit-offset.js.map +0 -1
- package/dist/src/planner/nodes/plan-node-type.js.map +0 -1
- package/dist/src/planner/nodes/plan-node.js.map +0 -1
- package/dist/src/planner/nodes/pragma.js.map +0 -1
- package/dist/src/planner/nodes/project-node.js.map +0 -1
- package/dist/src/planner/nodes/recursive-cte-node.js.map +0 -1
- package/dist/src/planner/nodes/reference.js.map +0 -1
- package/dist/src/planner/nodes/remote-query-node.js.map +0 -1
- package/dist/src/planner/nodes/retrieve-node.js.map +0 -1
- package/dist/src/planner/nodes/returning-node.js.map +0 -1
- package/dist/src/planner/nodes/scalar.js.map +0 -1
- package/dist/src/planner/nodes/sequencing-node.js.map +0 -1
- package/dist/src/planner/nodes/set-operation-node.js.map +0 -1
- package/dist/src/planner/nodes/single-row.js.map +0 -1
- package/dist/src/planner/nodes/sink-node.js.map +0 -1
- package/dist/src/planner/nodes/sort.js.map +0 -1
- package/dist/src/planner/nodes/stream-aggregate.js.map +0 -1
- package/dist/src/planner/nodes/subquery.js.map +0 -1
- package/dist/src/planner/nodes/table-access-nodes.js.map +0 -1
- package/dist/src/planner/nodes/table-function-call.js.map +0 -1
- package/dist/src/planner/nodes/transaction-node.js.map +0 -1
- package/dist/src/planner/nodes/update-node.js.map +0 -1
- package/dist/src/planner/nodes/values-node.js.map +0 -1
- package/dist/src/planner/nodes/view-reference-node.js.map +0 -1
- package/dist/src/planner/nodes/window-function.js.map +0 -1
- package/dist/src/planner/nodes/window-node.js.map +0 -1
- package/dist/src/planner/optimizer-tuning.js.map +0 -1
- package/dist/src/planner/optimizer.js.map +0 -1
- package/dist/src/planner/planning-context.js.map +0 -1
- package/dist/src/planner/resolve.js.map +0 -1
- package/dist/src/planner/rules/access/rule-select-access-path.js.map +0 -1
- package/dist/src/planner/rules/aggregate/rule-aggregate-streaming.js.map +0 -1
- package/dist/src/planner/rules/cache/rule-cte-optimization.js.map +0 -1
- package/dist/src/planner/rules/cache/rule-materialization-advisory.js.map +0 -1
- package/dist/src/planner/rules/cache/rule-mutating-subquery-cache.js.map +0 -1
- package/dist/src/planner/rules/join/rule-join-greedy-commute.js.map +0 -1
- package/dist/src/planner/rules/join/rule-join-key-inference.js.map +0 -1
- package/dist/src/planner/rules/join/rule-quickpick-enumeration.js.map +0 -1
- package/dist/src/planner/rules/predicate/rule-predicate-pushdown.js.map +0 -1
- package/dist/src/planner/rules/retrieve/rule-grow-retrieve.js.map +0 -1
- package/dist/src/planner/scopes/aliased.js.map +0 -1
- package/dist/src/planner/scopes/base.js.map +0 -1
- package/dist/src/planner/scopes/empty.js.map +0 -1
- package/dist/src/planner/scopes/global.js.map +0 -1
- package/dist/src/planner/scopes/multi.js.map +0 -1
- package/dist/src/planner/scopes/param.js.map +0 -1
- package/dist/src/planner/scopes/registered.js.map +0 -1
- package/dist/src/planner/scopes/scope.js.map +0 -1
- package/dist/src/planner/stats/basic-estimates.js.map +0 -1
- package/dist/src/planner/stats/index.js.map +0 -1
- package/dist/src/planner/type-utils.js.map +0 -1
- package/dist/src/planner/util/key-utils.js.map +0 -1
- package/dist/src/planner/validation/determinism-validator.js.map +0 -1
- package/dist/src/planner/validation/plan-validator.js.map +0 -1
- package/dist/src/runtime/async-util.js.map +0 -1
- package/dist/src/runtime/cache/shared-cache.js.map +0 -1
- package/dist/src/runtime/context-helpers.js.map +0 -1
- package/dist/src/runtime/deferred-constraint-queue.js.map +0 -1
- package/dist/src/runtime/emission-context.js.map +0 -1
- package/dist/src/runtime/emit/add-constraint.js.map +0 -1
- package/dist/src/runtime/emit/aggregate.js.map +0 -1
- package/dist/src/runtime/emit/array-index.js.map +0 -1
- package/dist/src/runtime/emit/between.js.map +0 -1
- package/dist/src/runtime/emit/binary.js.map +0 -1
- package/dist/src/runtime/emit/block.js.map +0 -1
- package/dist/src/runtime/emit/cache.js.map +0 -1
- package/dist/src/runtime/emit/case.js.map +0 -1
- package/dist/src/runtime/emit/cast.js.map +0 -1
- package/dist/src/runtime/emit/collate.js.map +0 -1
- package/dist/src/runtime/emit/column-reference.js.map +0 -1
- package/dist/src/runtime/emit/constraint-check.js.map +0 -1
- package/dist/src/runtime/emit/create-assertion.js.map +0 -1
- package/dist/src/runtime/emit/create-index.js.map +0 -1
- package/dist/src/runtime/emit/create-table.js.map +0 -1
- package/dist/src/runtime/emit/create-view.js.map +0 -1
- package/dist/src/runtime/emit/cte-reference.js.map +0 -1
- package/dist/src/runtime/emit/cte.js.map +0 -1
- package/dist/src/runtime/emit/delete.js.map +0 -1
- package/dist/src/runtime/emit/distinct.js.map +0 -1
- package/dist/src/runtime/emit/dml-executor.js.map +0 -1
- package/dist/src/runtime/emit/drop-assertion.js.map +0 -1
- package/dist/src/runtime/emit/drop-table.js.map +0 -1
- package/dist/src/runtime/emit/drop-view.js.map +0 -1
- package/dist/src/runtime/emit/filter.js.map +0 -1
- package/dist/src/runtime/emit/insert.js.map +0 -1
- package/dist/src/runtime/emit/internal-recursive-cte-ref.js.map +0 -1
- package/dist/src/runtime/emit/join.js.map +0 -1
- package/dist/src/runtime/emit/limit-offset.js.map +0 -1
- package/dist/src/runtime/emit/literal.js.map +0 -1
- package/dist/src/runtime/emit/parameter.js.map +0 -1
- package/dist/src/runtime/emit/pragma.js.map +0 -1
- package/dist/src/runtime/emit/project.js.map +0 -1
- package/dist/src/runtime/emit/recursive-cte.js.map +0 -1
- package/dist/src/runtime/emit/remote-query.js.map +0 -1
- package/dist/src/runtime/emit/retrieve.js.map +0 -1
- package/dist/src/runtime/emit/returning.js.map +0 -1
- package/dist/src/runtime/emit/scalar-function.js.map +0 -1
- package/dist/src/runtime/emit/scan.js.map +0 -1
- package/dist/src/runtime/emit/schema-declarative.js.map +0 -1
- package/dist/src/runtime/emit/sequencing.js.map +0 -1
- package/dist/src/runtime/emit/set-operation.js.map +0 -1
- package/dist/src/runtime/emit/sink.js.map +0 -1
- package/dist/src/runtime/emit/sort.js.map +0 -1
- package/dist/src/runtime/emit/subquery.js.map +0 -1
- package/dist/src/runtime/emit/table-valued-function.js.map +0 -1
- package/dist/src/runtime/emit/temporal-arithmetic.js.map +0 -1
- package/dist/src/runtime/emit/transaction.js.map +0 -1
- package/dist/src/runtime/emit/unary.js.map +0 -1
- package/dist/src/runtime/emit/update.js.map +0 -1
- package/dist/src/runtime/emit/values.js.map +0 -1
- package/dist/src/runtime/emit/window-function.js.map +0 -1
- package/dist/src/runtime/emit/window.js.map +0 -1
- package/dist/src/runtime/emitters.js.map +0 -1
- package/dist/src/runtime/register.js.map +0 -1
- package/dist/src/runtime/scheduler.js.map +0 -1
- package/dist/src/runtime/types.js.map +0 -1
- package/dist/src/runtime/utils.js.map +0 -1
- package/dist/src/schema/assertion.js.map +0 -1
- package/dist/src/schema/catalog.js.map +0 -1
- package/dist/src/schema/change-events.js.map +0 -1
- package/dist/src/schema/column.js.map +0 -1
- package/dist/src/schema/declared-schema-manager.js.map +0 -1
- package/dist/src/schema/function.js.map +0 -1
- package/dist/src/schema/manager.js.map +0 -1
- package/dist/src/schema/schema-differ.js.map +0 -1
- package/dist/src/schema/schema-hasher.js.map +0 -1
- package/dist/src/schema/schema.js.map +0 -1
- package/dist/src/schema/table.js.map +0 -1
- package/dist/src/schema/view.js.map +0 -1
- package/dist/src/schema/window-function.js.map +0 -1
- package/dist/src/types/builtin-types.js.map +0 -1
- package/dist/src/types/index.js.map +0 -1
- package/dist/src/types/json-type.js.map +0 -1
- package/dist/src/types/logical-type.js.map +0 -1
- package/dist/src/types/plugin-interface.js.map +0 -1
- package/dist/src/types/registry.js.map +0 -1
- package/dist/src/types/temporal-types.js.map +0 -1
- package/dist/src/types/validation.js.map +0 -1
- package/dist/src/util/affinity.js.map +0 -1
- package/dist/src/util/ast-stringify.js.map +0 -1
- package/dist/src/util/cached.js.map +0 -1
- package/dist/src/util/coercion.js.map +0 -1
- package/dist/src/util/comparison.js.map +0 -1
- package/dist/src/util/environment.js.map +0 -1
- package/dist/src/util/hash.js.map +0 -1
- package/dist/src/util/latches.js.map +0 -1
- package/dist/src/util/mutation-statement.js.map +0 -1
- package/dist/src/util/patterns.js.map +0 -1
- package/dist/src/util/plan-formatter.js.map +0 -1
- package/dist/src/util/plugin-helper.js.map +0 -1
- package/dist/src/util/row-descriptor.js.map +0 -1
- package/dist/src/util/serialization.js.map +0 -1
- package/dist/src/util/sql-literal.js.map +0 -1
- package/dist/src/util/working-table-iterable.js.map +0 -1
- package/dist/src/vtab/best-access-plan.js.map +0 -1
- package/dist/src/vtab/connection.js.map +0 -1
- package/dist/src/vtab/filter-info.js.map +0 -1
- package/dist/src/vtab/index-info.js.map +0 -1
- package/dist/src/vtab/manifest.js.map +0 -1
- package/dist/src/vtab/memory/connection.js.map +0 -1
- package/dist/src/vtab/memory/index.js.map +0 -1
- package/dist/src/vtab/memory/layer/base-cursor.js.map +0 -1
- package/dist/src/vtab/memory/layer/base.js.map +0 -1
- package/dist/src/vtab/memory/layer/connection.js.map +0 -1
- package/dist/src/vtab/memory/layer/interface.js.map +0 -1
- package/dist/src/vtab/memory/layer/manager.js.map +0 -1
- package/dist/src/vtab/memory/layer/safe-iterate.js.map +0 -1
- package/dist/src/vtab/memory/layer/scan-plan.js.map +0 -1
- package/dist/src/vtab/memory/layer/transaction-cursor.js.map +0 -1
- package/dist/src/vtab/memory/layer/transaction.js.map +0 -1
- package/dist/src/vtab/memory/module.js.map +0 -1
- package/dist/src/vtab/memory/table.js.map +0 -1
- package/dist/src/vtab/memory/types.js.map +0 -1
- package/dist/src/vtab/memory/utils/logging.js.map +0 -1
- package/dist/src/vtab/memory/utils/primary-key.js.map +0 -1
- package/dist/src/vtab/module.js.map +0 -1
- package/dist/src/vtab/table.js.map +0 -1
- package/src/common/constants.ts +0 -60
- package/src/common/datatype.ts +0 -85
- package/src/common/errors.ts +0 -189
- package/src/common/json-types.ts +0 -16
- package/src/common/logger.ts +0 -97
- package/src/common/type-inference.ts +0 -39
- package/src/common/types.ts +0 -151
- package/src/core/database-options.ts +0 -258
- package/src/core/database.ts +0 -1461
- package/src/core/param.ts +0 -56
- package/src/core/statement.ts +0 -528
- package/src/func/builtins/aggregate.ts +0 -269
- package/src/func/builtins/builtin-window-functions.ts +0 -166
- package/src/func/builtins/conversion.ts +0 -226
- package/src/func/builtins/datetime.ts +0 -500
- package/src/func/builtins/explain.ts +0 -779
- package/src/func/builtins/generation.ts +0 -43
- package/src/func/builtins/index.ts +0 -167
- package/src/func/builtins/json-helpers.ts +0 -237
- package/src/func/builtins/json-tvf.ts +0 -224
- package/src/func/builtins/json.ts +0 -588
- package/src/func/builtins/scalar.ts +0 -423
- package/src/func/builtins/schema.ts +0 -213
- package/src/func/builtins/string.ts +0 -306
- package/src/func/builtins/timespan.ts +0 -179
- package/src/func/context.ts +0 -258
- package/src/func/registration.ts +0 -201
- package/src/index.ts +0 -172
- package/src/parser/ast.ts +0 -581
- package/src/parser/index.ts +0 -65
- package/src/parser/lexer.ts +0 -806
- package/src/parser/parser.ts +0 -3352
- package/src/parser/utils.ts +0 -10
- package/src/parser/visitor.ts +0 -188
- package/src/planner/analysis/README.md +0 -93
- package/src/planner/analysis/binding-collector.ts +0 -83
- package/src/planner/analysis/const-evaluator.ts +0 -63
- package/src/planner/analysis/const-pass.ts +0 -282
- package/src/planner/analysis/constraint-extractor.ts +0 -764
- package/src/planner/analysis/predicate-normalizer.ts +0 -237
- package/src/planner/building/alter-table.ts +0 -49
- package/src/planner/building/block.ts +0 -93
- package/src/planner/building/constraint-builder.ts +0 -178
- package/src/planner/building/create-assertion.ts +0 -7
- package/src/planner/building/create-view.ts +0 -29
- package/src/planner/building/ddl.ts +0 -24
- package/src/planner/building/declare-schema.ts +0 -22
- package/src/planner/building/delete.ts +0 -218
- package/src/planner/building/drop-assertion.ts +0 -11
- package/src/planner/building/drop-table.ts +0 -13
- package/src/planner/building/drop-view.ts +0 -19
- package/src/planner/building/expression.ts +0 -205
- package/src/planner/building/function-call.ts +0 -129
- package/src/planner/building/insert.ts +0 -435
- package/src/planner/building/pragma.ts +0 -34
- package/src/planner/building/schema-resolution.ts +0 -176
- package/src/planner/building/select-aggregates.ts +0 -318
- package/src/planner/building/select-compound.ts +0 -119
- package/src/planner/building/select-context.ts +0 -85
- package/src/planner/building/select-modifiers.ts +0 -236
- package/src/planner/building/select-projections.ts +0 -177
- package/src/planner/building/select-window.ts +0 -259
- package/src/planner/building/select.ts +0 -567
- package/src/planner/building/table-function.ts +0 -49
- package/src/planner/building/table.ts +0 -40
- package/src/planner/building/transaction.ts +0 -23
- package/src/planner/building/update.ts +0 -331
- package/src/planner/building/with.ts +0 -180
- package/src/planner/cache/correlation-detector.ts +0 -83
- package/src/planner/cache/materialization-advisory.ts +0 -265
- package/src/planner/cache/reference-graph.ts +0 -196
- package/src/planner/cost/index.ts +0 -169
- package/src/planner/debug/logger-utils.ts +0 -68
- package/src/planner/debug.ts +0 -480
- package/src/planner/framework/README.md +0 -132
- package/src/planner/framework/characteristics.ts +0 -503
- package/src/planner/framework/context.ts +0 -239
- package/src/planner/framework/pass.ts +0 -354
- package/src/planner/framework/physical-utils.ts +0 -210
- package/src/planner/framework/registry.ts +0 -261
- package/src/planner/framework/trace.ts +0 -259
- package/src/planner/nodes/add-constraint-node.ts +0 -62
- package/src/planner/nodes/aggregate-function.ts +0 -155
- package/src/planner/nodes/aggregate-node.ts +0 -267
- package/src/planner/nodes/array-index-node.ts +0 -50
- package/src/planner/nodes/block.ts +0 -80
- package/src/planner/nodes/cache-node.ts +0 -103
- package/src/planner/nodes/constraint-check-node.ts +0 -138
- package/src/planner/nodes/create-assertion-node.ts +0 -51
- package/src/planner/nodes/create-index-node.ts +0 -41
- package/src/planner/nodes/create-table-node.ts +0 -35
- package/src/planner/nodes/create-view-node.ts +0 -44
- package/src/planner/nodes/cte-node.ts +0 -168
- package/src/planner/nodes/cte-reference-node.ts +0 -125
- package/src/planner/nodes/declarative-schema.ts +0 -221
- package/src/planner/nodes/delete-node.ts +0 -102
- package/src/planner/nodes/distinct-node.ts +0 -107
- package/src/planner/nodes/dml-executor-node.ts +0 -104
- package/src/planner/nodes/drop-assertion-node.ts +0 -50
- package/src/planner/nodes/drop-table-node.ts +0 -36
- package/src/planner/nodes/drop-view-node.ts +0 -37
- package/src/planner/nodes/filter.ts +0 -144
- package/src/planner/nodes/function.ts +0 -98
- package/src/planner/nodes/insert-node.ts +0 -126
- package/src/planner/nodes/internal-recursive-cte-ref-node.ts +0 -61
- package/src/planner/nodes/join-node.ts +0 -336
- package/src/planner/nodes/limit-offset.ts +0 -144
- package/src/planner/nodes/plan-node-type.ts +0 -95
- package/src/planner/nodes/plan-node.ts +0 -503
- package/src/planner/nodes/pragma.ts +0 -98
- package/src/planner/nodes/project-node.ts +0 -337
- package/src/planner/nodes/recursive-cte-node.ts +0 -158
- package/src/planner/nodes/reference.ts +0 -334
- package/src/planner/nodes/remote-query-node.ts +0 -73
- package/src/planner/nodes/retrieve-node.ts +0 -86
- package/src/planner/nodes/returning-node.ts +0 -269
- package/src/planner/nodes/scalar.ts +0 -772
- package/src/planner/nodes/sequencing-node.ts +0 -113
- package/src/planner/nodes/set-operation-node.ts +0 -87
- package/src/planner/nodes/single-row.ts +0 -85
- package/src/planner/nodes/sink-node.ts +0 -61
- package/src/planner/nodes/sort.ts +0 -166
- package/src/planner/nodes/stream-aggregate.ts +0 -293
- package/src/planner/nodes/subquery.ts +0 -268
- package/src/planner/nodes/table-access-nodes.ts +0 -323
- package/src/planner/nodes/table-function-call.ts +0 -134
- package/src/planner/nodes/transaction-node.ts +0 -55
- package/src/planner/nodes/update-node.ts +0 -138
- package/src/planner/nodes/values-node.ts +0 -244
- package/src/planner/nodes/view-reference-node.ts +0 -97
- package/src/planner/nodes/window-function.ts +0 -73
- package/src/planner/nodes/window-node.ts +0 -199
- package/src/planner/optimizer-tuning.ts +0 -105
- package/src/planner/optimizer.ts +0 -332
- package/src/planner/planning-context.ts +0 -190
- package/src/planner/resolve.ts +0 -101
- package/src/planner/rules/README.md +0 -96
- package/src/planner/rules/access/rule-select-access-path.ts +0 -399
- package/src/planner/rules/aggregate/rule-aggregate-streaming.ts +0 -162
- package/src/planner/rules/cache/rule-cte-optimization.ts +0 -79
- package/src/planner/rules/cache/rule-materialization-advisory.ts +0 -77
- package/src/planner/rules/cache/rule-mutating-subquery-cache.ts +0 -104
- package/src/planner/rules/join/rule-join-greedy-commute.ts +0 -48
- package/src/planner/rules/join/rule-join-key-inference.ts +0 -35
- package/src/planner/rules/join/rule-quickpick-enumeration.ts +0 -267
- package/src/planner/rules/predicate/rule-predicate-pushdown.ts +0 -144
- package/src/planner/rules/retrieve/rule-grow-retrieve.ts +0 -337
- package/src/planner/scopes/aliased.ts +0 -50
- package/src/planner/scopes/base.ts +0 -10
- package/src/planner/scopes/empty.ts +0 -12
- package/src/planner/scopes/global.ts +0 -73
- package/src/planner/scopes/multi.ts +0 -40
- package/src/planner/scopes/param.ts +0 -95
- package/src/planner/scopes/registered.ts +0 -67
- package/src/planner/scopes/scope.ts +0 -16
- package/src/planner/stats/basic-estimates.ts +0 -107
- package/src/planner/stats/index.ts +0 -158
- package/src/planner/type-utils.ts +0 -87
- package/src/planner/util/key-utils.ts +0 -46
- package/src/planner/validation/determinism-validator.ts +0 -104
- package/src/planner/validation/plan-validator.ts +0 -335
- package/src/runtime/async-util.ts +0 -283
- package/src/runtime/cache/shared-cache.ts +0 -169
- package/src/runtime/context-helpers.ts +0 -191
- package/src/runtime/deferred-constraint-queue.ts +0 -196
- package/src/runtime/emission-context.ts +0 -319
- package/src/runtime/emit/add-constraint.ts +0 -78
- package/src/runtime/emit/aggregate.ts +0 -581
- package/src/runtime/emit/array-index.ts +0 -25
- package/src/runtime/emit/between.ts +0 -51
- package/src/runtime/emit/binary.ts +0 -357
- package/src/runtime/emit/block.ts +0 -23
- package/src/runtime/emit/cache.ts +0 -64
- package/src/runtime/emit/case.ts +0 -87
- package/src/runtime/emit/cast.ts +0 -151
- package/src/runtime/emit/collate.ts +0 -9
- package/src/runtime/emit/column-reference.ts +0 -17
- package/src/runtime/emit/constraint-check.ts +0 -290
- package/src/runtime/emit/create-assertion.ts +0 -82
- package/src/runtime/emit/create-index.ts +0 -15
- package/src/runtime/emit/create-table.ts +0 -15
- package/src/runtime/emit/create-view.ts +0 -52
- package/src/runtime/emit/cte-reference.ts +0 -38
- package/src/runtime/emit/cte.ts +0 -39
- package/src/runtime/emit/delete.ts +0 -24
- package/src/runtime/emit/distinct.ts +0 -40
- package/src/runtime/emit/dml-executor.ts +0 -198
- package/src/runtime/emit/drop-assertion.ts +0 -45
- package/src/runtime/emit/drop-table.ts +0 -27
- package/src/runtime/emit/drop-view.ts +0 -49
- package/src/runtime/emit/filter.ts +0 -30
- package/src/runtime/emit/insert.ts +0 -42
- package/src/runtime/emit/internal-recursive-cte-ref.ts +0 -37
- package/src/runtime/emit/join.ts +0 -148
- package/src/runtime/emit/limit-offset.ts +0 -73
- package/src/runtime/emit/literal.ts +0 -17
- package/src/runtime/emit/parameter.ts +0 -59
- package/src/runtime/emit/pragma.ts +0 -56
- package/src/runtime/emit/project.ts +0 -46
- package/src/runtime/emit/recursive-cte.ts +0 -111
- package/src/runtime/emit/remote-query.ts +0 -47
- package/src/runtime/emit/retrieve.ts +0 -15
- package/src/runtime/emit/returning.ts +0 -41
- package/src/runtime/emit/scalar-function.ts +0 -69
- package/src/runtime/emit/scan.ts +0 -106
- package/src/runtime/emit/schema-declarative.ts +0 -215
- package/src/runtime/emit/sequencing.ts +0 -24
- package/src/runtime/emit/set-operation.ts +0 -141
- package/src/runtime/emit/sink.ts +0 -27
- package/src/runtime/emit/sort.ts +0 -75
- package/src/runtime/emit/subquery.ts +0 -203
- package/src/runtime/emit/table-valued-function.ts +0 -106
- package/src/runtime/emit/temporal-arithmetic.ts +0 -302
- package/src/runtime/emit/transaction.ts +0 -205
- package/src/runtime/emit/unary.ts +0 -101
- package/src/runtime/emit/update.ts +0 -66
- package/src/runtime/emit/values.ts +0 -66
- package/src/runtime/emit/window-function.ts +0 -42
- package/src/runtime/emit/window.ts +0 -458
- package/src/runtime/emitters.ts +0 -183
- package/src/runtime/register.ts +0 -150
- package/src/runtime/scheduler.ts +0 -488
- package/src/runtime/types.ts +0 -242
- package/src/runtime/utils.ts +0 -177
- package/src/schema/assertion.ts +0 -21
- package/src/schema/catalog.ts +0 -269
- package/src/schema/change-events.ts +0 -80
- package/src/schema/column.ts +0 -51
- package/src/schema/declared-schema-manager.ts +0 -82
- package/src/schema/function.ts +0 -188
- package/src/schema/manager.ts +0 -1034
- package/src/schema/schema-differ.ts +0 -214
- package/src/schema/schema-hasher.ts +0 -26
- package/src/schema/schema.ts +0 -222
- package/src/schema/table.ts +0 -409
- package/src/schema/view.ts +0 -19
- package/src/schema/window-function.ts +0 -56
- package/src/types/builtin-types.ts +0 -350
- package/src/types/index.ts +0 -17
- package/src/types/json-type.ts +0 -152
- package/src/types/logical-type.ts +0 -91
- package/src/types/plugin-interface.ts +0 -10
- package/src/types/registry.ts +0 -204
- package/src/types/temporal-types.ts +0 -290
- package/src/types/validation.ts +0 -120
- package/src/util/affinity.ts +0 -151
- package/src/util/ast-stringify.ts +0 -887
- package/src/util/cached.ts +0 -25
- package/src/util/coercion.ts +0 -113
- package/src/util/comparison.ts +0 -510
- package/src/util/environment.ts +0 -52
- package/src/util/hash.ts +0 -90
- package/src/util/latches.ts +0 -47
- package/src/util/mutation-statement.ts +0 -135
- package/src/util/patterns.ts +0 -56
- package/src/util/plan-formatter.ts +0 -48
- package/src/util/plugin-helper.ts +0 -110
- package/src/util/row-descriptor.ts +0 -105
- package/src/util/serialization.ts +0 -47
- package/src/util/sql-literal.ts +0 -22
- package/src/util/working-table-iterable.ts +0 -38
- package/src/vtab/best-access-plan.ts +0 -244
- package/src/vtab/connection.ts +0 -36
- package/src/vtab/filter-info.ts +0 -23
- package/src/vtab/index-info.ts +0 -84
- package/src/vtab/manifest.ts +0 -86
- package/src/vtab/memory/connection.ts +0 -73
- package/src/vtab/memory/index.ts +0 -191
- package/src/vtab/memory/layer/base-cursor.ts +0 -124
- package/src/vtab/memory/layer/base.ts +0 -275
- package/src/vtab/memory/layer/connection.ts +0 -203
- package/src/vtab/memory/layer/interface.ts +0 -47
- package/src/vtab/memory/layer/manager.ts +0 -909
- package/src/vtab/memory/layer/safe-iterate.ts +0 -49
- package/src/vtab/memory/layer/scan-plan.ts +0 -84
- package/src/vtab/memory/layer/transaction-cursor.ts +0 -162
- package/src/vtab/memory/layer/transaction.ts +0 -229
- package/src/vtab/memory/module.ts +0 -667
- package/src/vtab/memory/table.ts +0 -251
- package/src/vtab/memory/types.ts +0 -23
- package/src/vtab/memory/utils/logging.ts +0 -36
- package/src/vtab/memory/utils/primary-key.ts +0 -163
- package/src/vtab/module.ts +0 -162
- package/src/vtab/table.ts +0 -177
package/src/parser/parser.ts
DELETED
|
@@ -1,3352 +0,0 @@
|
|
|
1
|
-
import { createLogger } from '../common/logger.js'; // Import logger
|
|
2
|
-
import { Lexer, type Token, TokenType } from './lexer.js';
|
|
3
|
-
import * as AST from './ast.js';
|
|
4
|
-
import { ConflictResolution } from '../common/constants.js';
|
|
5
|
-
import type { RowOp, SqlValue } from '../common/types.js';
|
|
6
|
-
import { quereusError } from '../common/errors.js';
|
|
7
|
-
import { StatusCode } from '../common/types.js';
|
|
8
|
-
import { getSyncLiteral } from './utils.js';
|
|
9
|
-
|
|
10
|
-
const log = createLogger('parser:parser'); // Create logger instance
|
|
11
|
-
const errorLog = log.extend('error');
|
|
12
|
-
|
|
13
|
-
export class ParseError extends Error {
|
|
14
|
-
token: Token;
|
|
15
|
-
|
|
16
|
-
constructor(token: Token, message: string) {
|
|
17
|
-
super(message);
|
|
18
|
-
this.token = token;
|
|
19
|
-
this.name = 'ParseError';
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
// Helper function to create the location object
|
|
24
|
-
function _createLoc(startToken: Token, endToken: Token): AST.AstNode['loc'] {
|
|
25
|
-
return {
|
|
26
|
-
start: {
|
|
27
|
-
line: startToken.startLine,
|
|
28
|
-
column: startToken.startColumn,
|
|
29
|
-
offset: startToken.startOffset,
|
|
30
|
-
},
|
|
31
|
-
end: {
|
|
32
|
-
line: endToken.endLine,
|
|
33
|
-
column: endToken.endColumn,
|
|
34
|
-
offset: endToken.endOffset,
|
|
35
|
-
},
|
|
36
|
-
};
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export class Parser {
|
|
40
|
-
private tokens: Token[] = [];
|
|
41
|
-
private current = 0;
|
|
42
|
-
// Counter for positional parameters
|
|
43
|
-
private parameterPosition = 1;
|
|
44
|
-
// Track opening parentheses for accurate error locations
|
|
45
|
-
private parenStack: Token[] = [];
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Initialize the parser with tokens from a SQL string
|
|
49
|
-
* @param sql SQL string to parse
|
|
50
|
-
* @returns this parser instance for chaining
|
|
51
|
-
*/
|
|
52
|
-
initialize(sql: string): Parser {
|
|
53
|
-
const lexer = new Lexer(sql);
|
|
54
|
-
this.tokens = lexer.scanTokens();
|
|
55
|
-
this.current = 0;
|
|
56
|
-
this.parameterPosition = 1; // Reset parameter counter
|
|
57
|
-
this.parenStack = [];
|
|
58
|
-
|
|
59
|
-
// Check for errors from lexer
|
|
60
|
-
const errorToken = this.tokens.find(t => t.type === TokenType.ERROR);
|
|
61
|
-
if (errorToken) {
|
|
62
|
-
quereusError(
|
|
63
|
-
`Lexer error: ${errorToken.lexeme}`,
|
|
64
|
-
StatusCode.ERROR,
|
|
65
|
-
undefined,
|
|
66
|
-
{
|
|
67
|
-
loc: {
|
|
68
|
-
start: {
|
|
69
|
-
line: errorToken.startLine,
|
|
70
|
-
column: errorToken.startColumn,
|
|
71
|
-
},
|
|
72
|
-
end: {
|
|
73
|
-
line: errorToken.endLine,
|
|
74
|
-
column: errorToken.endColumn,
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
return this;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* Parse SQL text into an array of ASTs
|
|
86
|
-
*/
|
|
87
|
-
parseAll(sql: string): AST.Statement[] {
|
|
88
|
-
this.initialize(sql);
|
|
89
|
-
const statements: AST.Statement[] = [];
|
|
90
|
-
|
|
91
|
-
while (!this.isAtEnd()) {
|
|
92
|
-
try {
|
|
93
|
-
const stmt = this.statement();
|
|
94
|
-
statements.push(stmt as AST.Statement); // Cast needed as statement() returns AstNode
|
|
95
|
-
|
|
96
|
-
// Consume optional semicolon at the end of the statement
|
|
97
|
-
this.match(TokenType.SEMICOLON);
|
|
98
|
-
|
|
99
|
-
} catch (e) {
|
|
100
|
-
// error() method now throws QuereusError directly with location info
|
|
101
|
-
if (e instanceof Error && e.name === 'QuereusError') {
|
|
102
|
-
throw e;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// Handle unexpected non-QuereusError exceptions
|
|
106
|
-
errorLog("Unhandled parser error: %O", e);
|
|
107
|
-
quereusError(
|
|
108
|
-
`Parser error: ${e instanceof Error ? e.message : e}`,
|
|
109
|
-
StatusCode.ERROR,
|
|
110
|
-
e instanceof Error ? e : undefined
|
|
111
|
-
);
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
// Report any unterminated parenthesis at EOF with pointer to opening location
|
|
116
|
-
if (this.parenStack.length > 0) {
|
|
117
|
-
const openToken = this.parenStack[this.parenStack.length - 1];
|
|
118
|
-
quereusError(
|
|
119
|
-
`Unterminated '(' opened at line ${openToken.startLine}, column ${openToken.startColumn}. Expected ')' before end of input.`,
|
|
120
|
-
StatusCode.ERROR,
|
|
121
|
-
undefined,
|
|
122
|
-
{
|
|
123
|
-
loc: {
|
|
124
|
-
start: { line: openToken.startLine, column: openToken.startColumn },
|
|
125
|
-
end: { line: this.peek().endLine, column: this.peek().endColumn },
|
|
126
|
-
},
|
|
127
|
-
}
|
|
128
|
-
);
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// If we consumed all tokens and didn't parse any statements (e.g., empty input or only comments/whitespace),
|
|
132
|
-
// return an empty array instead of throwing an error.
|
|
133
|
-
return statements;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
/**
|
|
137
|
-
* Parse SQL text into a single AST node.
|
|
138
|
-
* Use parseAll instead for potentially multi-statement strings.
|
|
139
|
-
* Throws error if more than one statement is found after the first.
|
|
140
|
-
*/
|
|
141
|
-
parse(sql: string): AST.Statement {
|
|
142
|
-
const statements = this.parseAll(sql);
|
|
143
|
-
if (statements.length === 0) {
|
|
144
|
-
// Handle case of empty input or input with only comments/whitespace
|
|
145
|
-
// Depending on desired behavior, could return null, undefined, or throw.
|
|
146
|
-
// Throwing seems reasonable as prepare/eval expect a statement.
|
|
147
|
-
quereusError("No SQL statement found to parse.", StatusCode.ERROR);
|
|
148
|
-
}
|
|
149
|
-
if (statements.length > 1) {
|
|
150
|
-
// Find the token that starts the second statement for better error location
|
|
151
|
-
const secondStatementStartToken = statements[1]?.loc?.start;
|
|
152
|
-
const errToken = this.tokens.find(t => t.startOffset === secondStatementStartToken?.offset) ?? this.peek();
|
|
153
|
-
this.error(errToken, "Provided SQL string contains multiple statements. Use exec() for multi-statement execution.");
|
|
154
|
-
}
|
|
155
|
-
return statements[0];
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
/**
|
|
159
|
-
* Attempts to parse a WITH clause if present.
|
|
160
|
-
* @returns The WithClause AST node or undefined if no WITH clause is found.
|
|
161
|
-
*/
|
|
162
|
-
private tryParseWithClause(): AST.WithClause | undefined {
|
|
163
|
-
if (!this.check(TokenType.WITH)) {
|
|
164
|
-
return undefined;
|
|
165
|
-
}
|
|
166
|
-
const startToken = this.advance(); // Consume WITH
|
|
167
|
-
|
|
168
|
-
const recursive = this.match(TokenType.RECURSIVE);
|
|
169
|
-
|
|
170
|
-
const ctes: AST.CommonTableExpr[] = [];
|
|
171
|
-
do {
|
|
172
|
-
ctes.push(this.commonTableExpression());
|
|
173
|
-
} while (this.match(TokenType.COMMA));
|
|
174
|
-
|
|
175
|
-
// Parse optional OPTION clause
|
|
176
|
-
let options: AST.WithClauseOptions | undefined;
|
|
177
|
-
if (this.matchKeyword('OPTION')) {
|
|
178
|
-
this.consume(TokenType.LPAREN, "Expected '(' after OPTION.");
|
|
179
|
-
|
|
180
|
-
// Parse MAXRECURSION option
|
|
181
|
-
if (this.matchKeyword('MAXRECURSION')) {
|
|
182
|
-
if (!this.check(TokenType.INTEGER)) {
|
|
183
|
-
this.error(this.peek(), "Expected integer value after MAXRECURSION.");
|
|
184
|
-
}
|
|
185
|
-
const maxRecursionToken = this.advance();
|
|
186
|
-
const maxRecursion = maxRecursionToken.literal as number;
|
|
187
|
-
|
|
188
|
-
if (maxRecursion < 0) {
|
|
189
|
-
this.error(maxRecursionToken, "MAXRECURSION value must be non-negative.");
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
options = { maxRecursion };
|
|
193
|
-
} else {
|
|
194
|
-
throw this.error(this.peek(), "Expected MAXRECURSION in OPTION clause.");
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
this.consume(TokenType.RPAREN, "Expected ')' after OPTION clause.");
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
const endToken = this.previous(); // Last token of the WITH clause
|
|
201
|
-
|
|
202
|
-
return { type: 'with', recursive, ctes, options, loc: _createLoc(startToken, endToken) };
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
/**
|
|
206
|
-
* Parses a single Common Table Expression (CTE).
|
|
207
|
-
* cte_name [(col1, col2, ...)] AS (query)
|
|
208
|
-
*/
|
|
209
|
-
private commonTableExpression(): AST.CommonTableExpr {
|
|
210
|
-
const startToken = this.peek(); // Peek before consuming name
|
|
211
|
-
const name = this.consumeIdentifier(['key', 'action', 'set', 'default', 'check', 'unique', 'like'], "Expected CTE name.");
|
|
212
|
-
let endToken = this.previous(); // End token initially is the name
|
|
213
|
-
|
|
214
|
-
let columns: string[] | undefined;
|
|
215
|
-
if (this.match(TokenType.LPAREN)) {
|
|
216
|
-
columns = [];
|
|
217
|
-
if (!this.check(TokenType.RPAREN)) {
|
|
218
|
-
do {
|
|
219
|
-
columns.push(this.consumeIdentifier(['key', 'action', 'set', 'default', 'check', 'unique', 'like'], "Expected column name in CTE definition."));
|
|
220
|
-
} while (this.match(TokenType.COMMA) && !this.check(TokenType.RPAREN));
|
|
221
|
-
}
|
|
222
|
-
endToken = this.consume(TokenType.RPAREN, "Expected ')' after CTE column list.");
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
this.consume(TokenType.AS, "Expected 'AS' after CTE name.");
|
|
226
|
-
|
|
227
|
-
let materializationHint: AST.CommonTableExpr['materializationHint'];
|
|
228
|
-
if (this.matchKeyword('MATERIALIZED')) {
|
|
229
|
-
materializationHint = 'materialized';
|
|
230
|
-
} else if (this.matchKeyword('NOT')) {
|
|
231
|
-
this.consumeKeyword('MATERIALIZED', "Expected 'MATERIALIZED' after 'NOT'.");
|
|
232
|
-
materializationHint = 'not_materialized';
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
this.consume(TokenType.LPAREN, "Expected '(' before CTE query.");
|
|
236
|
-
|
|
237
|
-
// Parse the CTE query (can be SELECT, VALUES (via SELECT), INSERT, UPDATE, DELETE)
|
|
238
|
-
const queryStartToken = this.peek();
|
|
239
|
-
let query: AST.SelectStmt | AST.InsertStmt | AST.UpdateStmt | AST.DeleteStmt;
|
|
240
|
-
if (this.check(TokenType.SELECT)) {
|
|
241
|
-
this.advance(); // Consume SELECT token
|
|
242
|
-
query = this.selectStatement(queryStartToken); // Pass start token
|
|
243
|
-
} else if (this.check(TokenType.INSERT)) {
|
|
244
|
-
this.advance(); // Consume INSERT token
|
|
245
|
-
query = this.insertStatement(queryStartToken);
|
|
246
|
-
} else if (this.check(TokenType.UPDATE)) {
|
|
247
|
-
this.advance(); // Consume UPDATE token
|
|
248
|
-
query = this.updateStatement(queryStartToken);
|
|
249
|
-
} else if (this.check(TokenType.DELETE)) {
|
|
250
|
-
this.advance(); // Consume DELETE token
|
|
251
|
-
query = this.deleteStatement(queryStartToken);
|
|
252
|
-
}
|
|
253
|
-
// TODO: Add support for VALUES directly if needed (though VALUES is usually part of SELECT)
|
|
254
|
-
else {
|
|
255
|
-
throw this.error(this.peek(), "Expected SELECT, INSERT, UPDATE, or DELETE statement for CTE query.");
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
endToken = this.consume(TokenType.RPAREN, "Expected ')' after CTE query."); // Capture ')' as end token
|
|
259
|
-
|
|
260
|
-
return { type: 'commonTableExpr', name, columns, query, materializationHint, loc: _createLoc(startToken, endToken) };
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
/**
|
|
264
|
-
* Parse a single SQL statement
|
|
265
|
-
*/
|
|
266
|
-
private statement(): AST.AstNode {
|
|
267
|
-
// Check for WITH clause first
|
|
268
|
-
let withClause: AST.WithClause | undefined;
|
|
269
|
-
if (this.check(TokenType.WITH)) {
|
|
270
|
-
withClause = this.tryParseWithClause();
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
const startToken = this.peek();
|
|
274
|
-
// --- Check for specific keywords first ---
|
|
275
|
-
const currentKeyword = startToken.lexeme.toUpperCase();
|
|
276
|
-
let stmt: AST.AstNode;
|
|
277
|
-
|
|
278
|
-
switch (currentKeyword) {
|
|
279
|
-
case 'SELECT': this.advance(); stmt = this.selectStatement(startToken, withClause); break;
|
|
280
|
-
case 'INSERT': this.advance(); stmt = this.insertStatement(startToken, withClause); break;
|
|
281
|
-
case 'UPDATE': this.advance(); stmt = this.updateStatement(startToken, withClause); break;
|
|
282
|
-
case 'DELETE': this.advance(); stmt = this.deleteStatement(startToken, withClause); break;
|
|
283
|
-
case 'VALUES': this.advance(); stmt = this.valuesStatement(startToken); break;
|
|
284
|
-
case 'CREATE': this.advance(); stmt = this.createStatement(startToken, withClause); break;
|
|
285
|
-
case 'DROP': this.advance(); stmt = this.dropStatement(startToken, withClause); break;
|
|
286
|
-
case 'ALTER': this.advance(); stmt = this.alterTableStatement(startToken, withClause); break;
|
|
287
|
-
case 'BEGIN': this.advance(); stmt = this.beginStatement(startToken, withClause); break;
|
|
288
|
-
case 'COMMIT': this.advance(); stmt = this.commitStatement(startToken, withClause); break;
|
|
289
|
-
case 'ROLLBACK': this.advance(); stmt = this.rollbackStatement(startToken, withClause); break;
|
|
290
|
-
case 'SAVEPOINT': this.advance(); stmt = this.savepointStatement(startToken, withClause); break;
|
|
291
|
-
case 'RELEASE': this.advance(); stmt = this.releaseStatement(startToken, withClause); break;
|
|
292
|
-
// TODO: Replace pragmas with build-in functions
|
|
293
|
-
case 'PRAGMA': this.advance(); stmt = this.pragmaStatement(startToken, withClause); break;
|
|
294
|
-
case 'DECLARE': this.advance(); stmt = this.declareSchemaStatement(startToken); break;
|
|
295
|
-
case 'DIFF': this.advance(); stmt = this.diffSchemaStatement(startToken); break;
|
|
296
|
-
case 'APPLY': this.advance(); stmt = this.applySchemaStatement(startToken); break;
|
|
297
|
-
case 'EXPLAIN': this.advance(); stmt = this.explainSchemaStatement(startToken); break;
|
|
298
|
-
// --- Add default case ---
|
|
299
|
-
default:
|
|
300
|
-
// If it wasn't a recognized keyword starting the statement
|
|
301
|
-
throw this.error(startToken, `Expected statement type (SELECT, INSERT, UPDATE, DELETE, VALUES, CREATE, etc.), got '${startToken.lexeme}'.`);
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
// Attach WITH clause if present and supported
|
|
305
|
-
if (withClause && this.statementSupportsWithClause(stmt)) {
|
|
306
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
307
|
-
(stmt as any).withClause = withClause;
|
|
308
|
-
if (withClause.loc && stmt.loc) {
|
|
309
|
-
stmt.loc.start = withClause.loc.start;
|
|
310
|
-
}
|
|
311
|
-
} else if (withClause) {
|
|
312
|
-
throw this.error(this.previous(), `WITH clause cannot be used with ${stmt.type} statement.`);
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
return stmt;
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
/**
|
|
319
|
-
* Parse an INSERT statement
|
|
320
|
-
* @returns AST for the INSERT statement
|
|
321
|
-
*/
|
|
322
|
-
insertStatement(startToken: Token, withClause?: AST.WithClause): AST.InsertStmt {
|
|
323
|
-
// INTO keyword is optional in SQLite
|
|
324
|
-
this.matchKeyword('INTO'); // Handle missing keyword gracefully
|
|
325
|
-
|
|
326
|
-
// Parse the table reference
|
|
327
|
-
const table = this.tableIdentifier();
|
|
328
|
-
const contextualKeywords = ['key', 'action', 'set', 'default', 'check', 'unique', 'references', 'on', 'cascade', 'restrict', 'like'];
|
|
329
|
-
|
|
330
|
-
// Parse column list if provided
|
|
331
|
-
let columns: string[] | undefined;
|
|
332
|
-
if (this.match(TokenType.LPAREN)) {
|
|
333
|
-
columns = [];
|
|
334
|
-
do {
|
|
335
|
-
if (!this.checkIdentifierLike(contextualKeywords)) {
|
|
336
|
-
throw this.error(this.peek(), "Expected column name.");
|
|
337
|
-
}
|
|
338
|
-
columns.push(this.getIdentifierValue(this.advance()));
|
|
339
|
-
} while (this.match(TokenType.COMMA));
|
|
340
|
-
|
|
341
|
-
this.consume(TokenType.RPAREN, "Expected ')' after column list.");
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
// Parse mutation context assignments if present (after column list, before VALUES/SELECT)
|
|
345
|
-
let contextValues: AST.ContextAssignment[] | undefined;
|
|
346
|
-
if (this.matchKeyword('WITH')) {
|
|
347
|
-
if (this.matchKeyword('CONTEXT')) {
|
|
348
|
-
contextValues = this.parseContextAssignments();
|
|
349
|
-
} else {
|
|
350
|
-
// Not a WITH CONTEXT clause, backtrack
|
|
351
|
-
this.current--;
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
// Parse VALUES clause
|
|
356
|
-
let values: AST.Expression[][] | undefined;
|
|
357
|
-
let select: AST.SelectStmt | undefined;
|
|
358
|
-
let lastConsumedToken = this.previous(); // After columns or table id
|
|
359
|
-
|
|
360
|
-
if (this.match(TokenType.VALUES)) {
|
|
361
|
-
values = [];
|
|
362
|
-
do {
|
|
363
|
-
this.consume(TokenType.LPAREN, "Expected '(' before values.");
|
|
364
|
-
const valueList: AST.Expression[] = [];
|
|
365
|
-
|
|
366
|
-
if (!this.check(TokenType.RPAREN)) { // Check for empty value list
|
|
367
|
-
do {
|
|
368
|
-
valueList.push(this.expression());
|
|
369
|
-
} while (this.match(TokenType.COMMA));
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
this.consume(TokenType.RPAREN, "Expected ')' after values.");
|
|
373
|
-
values.push(valueList);
|
|
374
|
-
lastConsumedToken = this.previous(); // Update after closing paren of value list
|
|
375
|
-
} while (this.match(TokenType.COMMA));
|
|
376
|
-
} else if (this.check(TokenType.SELECT)) { // If current token is SELECT
|
|
377
|
-
// Handle INSERT ... SELECT
|
|
378
|
-
// Consume the SELECT token, as selectStatement expects to start parsing after it.
|
|
379
|
-
// selectKeywordToken will be the actual 'SELECT' token object, used for location.
|
|
380
|
-
const selectKeywordToken = this.advance(); // Consume 'SELECT'
|
|
381
|
-
// Pass the withClause so the embedded SELECT can (via the planner) resolve CTEs defined for the INSERT.
|
|
382
|
-
select = this.selectStatement(selectKeywordToken, withClause);
|
|
383
|
-
lastConsumedToken = this.previous(); // After SELECT statement is parsed
|
|
384
|
-
} else {
|
|
385
|
-
throw this.error(this.peek(), "Expected VALUES or SELECT after INSERT.");
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
// Parse RETURNING clause if present
|
|
389
|
-
let returning: AST.ResultColumn[] | undefined;
|
|
390
|
-
if (this.matchKeyword('RETURNING')) {
|
|
391
|
-
returning = this.columnList();
|
|
392
|
-
lastConsumedToken = this.previous(); // Update after RETURNING clause
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
return {
|
|
396
|
-
type: 'insert',
|
|
397
|
-
table,
|
|
398
|
-
columns,
|
|
399
|
-
values,
|
|
400
|
-
select,
|
|
401
|
-
returning,
|
|
402
|
-
contextValues,
|
|
403
|
-
loc: _createLoc(startToken, lastConsumedToken),
|
|
404
|
-
};
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
/**
|
|
408
|
-
* Parse a SELECT statement
|
|
409
|
-
* @param startToken The 'SELECT' token or start token of a sub-query
|
|
410
|
-
* @param withClause The WITH clause context for CTE access
|
|
411
|
-
* @param isCompoundSubquery If true, don't parse ORDER BY/LIMIT as they belong to the outer compound
|
|
412
|
-
* @returns AST for the SELECT statement
|
|
413
|
-
*/
|
|
414
|
-
selectStatement(startToken?: Token, withClause?: AST.WithClause, isCompoundSubquery: boolean = false): AST.SelectStmt {
|
|
415
|
-
const start = startToken ?? this.previous(); // Use provided or the keyword token
|
|
416
|
-
let lastConsumedToken = start; // Initialize lastConsumed
|
|
417
|
-
|
|
418
|
-
const distinct = this.matchKeyword('DISTINCT');
|
|
419
|
-
if (distinct) lastConsumedToken = this.previous();
|
|
420
|
-
const all = !distinct && this.matchKeyword('ALL');
|
|
421
|
-
if (all) lastConsumedToken = this.previous();
|
|
422
|
-
|
|
423
|
-
// Parse column list
|
|
424
|
-
const columns = this.columnList();
|
|
425
|
-
if (columns.length > 0) lastConsumedToken = this.previous(); // Update after last column element
|
|
426
|
-
|
|
427
|
-
// Parse FROM clause if present
|
|
428
|
-
let from: AST.FromClause[] | undefined;
|
|
429
|
-
if (this.match(TokenType.FROM)) {
|
|
430
|
-
from = this.tableSourceList(withClause);
|
|
431
|
-
if (from.length > 0) lastConsumedToken = this.previous(); // After last source/join
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
// Parse WHERE clause if present
|
|
435
|
-
let where: AST.Expression | undefined;
|
|
436
|
-
if (this.match(TokenType.WHERE)) {
|
|
437
|
-
where = this.expression();
|
|
438
|
-
lastConsumedToken = this.previous(); // After where expression
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
// Parse GROUP BY clause if present
|
|
442
|
-
let groupBy: AST.Expression[] | undefined;
|
|
443
|
-
if (this.match(TokenType.GROUP) && this.consume(TokenType.BY, "Expected 'BY' after 'GROUP'.")) {
|
|
444
|
-
groupBy = [];
|
|
445
|
-
do {
|
|
446
|
-
groupBy.push(this.expression());
|
|
447
|
-
} while (this.match(TokenType.COMMA));
|
|
448
|
-
lastConsumedToken = this.previous(); // After last group by expression
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
// Parse HAVING clause if present
|
|
452
|
-
let having: AST.Expression | undefined;
|
|
453
|
-
if (this.match(TokenType.HAVING)) {
|
|
454
|
-
having = this.expression();
|
|
455
|
-
lastConsumedToken = this.previous(); // After having expression
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
// Check for compound set operations (UNION / INTERSECT / EXCEPT) BEFORE ORDER BY/LIMIT
|
|
459
|
-
let compound: { op: 'union' | 'unionAll' | 'intersect' | 'except' | 'diff'; select: AST.SelectStmt } | undefined;
|
|
460
|
-
if (this.match(TokenType.UNION, TokenType.INTERSECT, TokenType.EXCEPT, TokenType.DIFF)) {
|
|
461
|
-
const tok = this.previous();
|
|
462
|
-
let op: 'union' | 'unionAll' | 'intersect' | 'except' | 'diff';
|
|
463
|
-
if (tok.type === TokenType.UNION) {
|
|
464
|
-
if (this.match(TokenType.ALL)) {
|
|
465
|
-
op = 'unionAll';
|
|
466
|
-
} else {
|
|
467
|
-
op = 'union';
|
|
468
|
-
}
|
|
469
|
-
} else if (tok.type === TokenType.INTERSECT) {
|
|
470
|
-
op = 'intersect';
|
|
471
|
-
} else if (tok.type === TokenType.EXCEPT) {
|
|
472
|
-
op = 'except';
|
|
473
|
-
} else {
|
|
474
|
-
op = 'diff';
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
let rightSelect: AST.SelectStmt;
|
|
478
|
-
|
|
479
|
-
// Handle parenthesized subquery after set operation
|
|
480
|
-
if (this.match(TokenType.LPAREN)) {
|
|
481
|
-
const selectToken = this.consume(TokenType.SELECT, "Expected 'SELECT' in parenthesized set operation.");
|
|
482
|
-
rightSelect = this.selectStatement(selectToken, withClause, true); // Pass true to indicate compound subquery
|
|
483
|
-
this.consume(TokenType.RPAREN, "Expected ')' after parenthesized set operation.");
|
|
484
|
-
} else {
|
|
485
|
-
// Handle direct SELECT statement
|
|
486
|
-
const selectStartToken = this.peek();
|
|
487
|
-
if (this.match(TokenType.SELECT)) {
|
|
488
|
-
rightSelect = this.selectStatement(selectStartToken, withClause, true); // Pass true to indicate compound subquery
|
|
489
|
-
} else {
|
|
490
|
-
throw this.error(this.peek(), "Expected 'SELECT' or '(' after set operation keyword.");
|
|
491
|
-
}
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
lastConsumedToken = this.previous();
|
|
495
|
-
compound = { op, select: rightSelect };
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
// Parse ORDER BY clause if present (applies to final result after compound operations)
|
|
499
|
-
// Skip if this is a compound subquery as ORDER BY belongs to the outer compound
|
|
500
|
-
let orderBy: AST.OrderByClause[] | undefined;
|
|
501
|
-
if (!isCompoundSubquery && this.match(TokenType.ORDER) && this.consume(TokenType.BY, "Expected 'BY' after 'ORDER'.")) {
|
|
502
|
-
orderBy = [];
|
|
503
|
-
do {
|
|
504
|
-
const expr = this.expression();
|
|
505
|
-
const direction = this.match(TokenType.DESC) ? 'desc' :
|
|
506
|
-
(this.match(TokenType.ASC) ? 'asc' : 'asc'); // Default to ASC
|
|
507
|
-
|
|
508
|
-
// Handle NULLS FIRST/LAST
|
|
509
|
-
let nulls: 'first' | 'last' | undefined;
|
|
510
|
-
if (this.matchKeyword('NULLS')) {
|
|
511
|
-
if (this.matchKeyword('FIRST')) {
|
|
512
|
-
nulls = 'first';
|
|
513
|
-
} else if (this.matchKeyword('LAST')) {
|
|
514
|
-
nulls = 'last';
|
|
515
|
-
} else {
|
|
516
|
-
throw this.error(this.peek(), "Expected 'FIRST' or 'LAST' after 'NULLS'.");
|
|
517
|
-
}
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
const orderClause: AST.OrderByClause = { expr, direction };
|
|
521
|
-
if (nulls) {
|
|
522
|
-
orderClause.nulls = nulls;
|
|
523
|
-
}
|
|
524
|
-
orderBy.push(orderClause);
|
|
525
|
-
} while (this.match(TokenType.COMMA));
|
|
526
|
-
lastConsumedToken = this.previous(); // After last order by clause
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
// Parse LIMIT clause if present (applies to final result after compound operations)
|
|
530
|
-
// Skip if this is a compound subquery as LIMIT belongs to the outer compound
|
|
531
|
-
let limit: AST.Expression | undefined;
|
|
532
|
-
let offset: AST.Expression | undefined;
|
|
533
|
-
if (!isCompoundSubquery && this.match(TokenType.LIMIT)) {
|
|
534
|
-
limit = this.expression();
|
|
535
|
-
lastConsumedToken = this.previous(); // After limit expression
|
|
536
|
-
|
|
537
|
-
// LIMIT x OFFSET y syntax
|
|
538
|
-
if (this.match(TokenType.OFFSET)) {
|
|
539
|
-
offset = this.expression();
|
|
540
|
-
lastConsumedToken = this.previous(); // After offset expression
|
|
541
|
-
}
|
|
542
|
-
// LIMIT x, y syntax (x is offset, y is limit)
|
|
543
|
-
else if (this.match(TokenType.COMMA)) {
|
|
544
|
-
offset = limit;
|
|
545
|
-
limit = this.expression();
|
|
546
|
-
lastConsumedToken = this.previous(); // After second limit expression
|
|
547
|
-
}
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
return {
|
|
551
|
-
type: 'select',
|
|
552
|
-
columns,
|
|
553
|
-
from,
|
|
554
|
-
where,
|
|
555
|
-
groupBy,
|
|
556
|
-
having,
|
|
557
|
-
orderBy,
|
|
558
|
-
limit,
|
|
559
|
-
offset,
|
|
560
|
-
distinct,
|
|
561
|
-
all,
|
|
562
|
-
compound,
|
|
563
|
-
loc: _createLoc(start, lastConsumedToken),
|
|
564
|
-
};
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
/**
|
|
568
|
-
* Parse a comma-separated list of result columns for SELECT
|
|
569
|
-
*/
|
|
570
|
-
private columnList(): AST.ResultColumn[] {
|
|
571
|
-
const columns: AST.ResultColumn[] = [];
|
|
572
|
-
const contextualKeywords = ['key', 'action', 'set', 'default', 'check', 'unique', 'references', 'on', 'cascade', 'restrict', 'like'];
|
|
573
|
-
|
|
574
|
-
do {
|
|
575
|
-
log(`columnList: Loop start. Current token: ${this.peek().lexeme} (${this.peek().type})`); // DEBUG
|
|
576
|
-
// Handle wildcard: * or table.*
|
|
577
|
-
if (this.match(TokenType.ASTERISK)) {
|
|
578
|
-
columns.push({ type: 'all' });
|
|
579
|
-
}
|
|
580
|
-
// Handle table.* syntax
|
|
581
|
-
else if (this.checkIdentifierLike(contextualKeywords) && this.checkNext(1, TokenType.DOT) &&
|
|
582
|
-
this.checkNext(2, TokenType.ASTERISK)) {
|
|
583
|
-
const table = this.consumeIdentifier(contextualKeywords, "Expected table name before '.*'.");
|
|
584
|
-
this.advance(); // consume DOT
|
|
585
|
-
this.advance(); // consume ASTERISK
|
|
586
|
-
columns.push({ type: 'all', table });
|
|
587
|
-
}
|
|
588
|
-
// Handle regular column expression
|
|
589
|
-
else {
|
|
590
|
-
log(`columnList: Parsing expression...`); // DEBUG
|
|
591
|
-
const expr = this.expression();
|
|
592
|
-
log(`columnList: Parsed expression. Current token: ${this.peek().lexeme} (${this.peek().type})`); // DEBUG
|
|
593
|
-
let alias: string | undefined;
|
|
594
|
-
|
|
595
|
-
// Handle AS alias or just alias
|
|
596
|
-
if (this.match(TokenType.AS)) {
|
|
597
|
-
if (this.checkIdentifierLike(contextualKeywords) || this.check(TokenType.STRING)) {
|
|
598
|
-
const aliasToken = this.advance();
|
|
599
|
-
// For STRING tokens, use literal; for identifiers, use getIdentifierValue
|
|
600
|
-
alias = aliasToken.type === TokenType.STRING
|
|
601
|
-
? aliasToken.literal
|
|
602
|
-
: this.getIdentifierValue(aliasToken);
|
|
603
|
-
} else {
|
|
604
|
-
throw this.error(this.peek(), "Expected identifier or string after 'AS'.");
|
|
605
|
-
}
|
|
606
|
-
}
|
|
607
|
-
// Implicit alias (no AS keyword)
|
|
608
|
-
else if (this.checkIdentifierLike([]) &&
|
|
609
|
-
!this.checkNext(1, TokenType.LPAREN) &&
|
|
610
|
-
!this.checkNext(1, TokenType.DOT) &&
|
|
611
|
-
!this.checkNext(1, TokenType.COMMA) &&
|
|
612
|
-
!this.isEndOfClause()) {
|
|
613
|
-
const aliasToken = this.advance();
|
|
614
|
-
alias = this.getIdentifierValue(aliasToken);
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
columns.push({ type: 'column', expr, alias });
|
|
618
|
-
}
|
|
619
|
-
log(`columnList: Checking for comma. Current token: ${this.peek().lexeme} (${this.peek().type})`); // DEBUG
|
|
620
|
-
} while (this.match(TokenType.COMMA));
|
|
621
|
-
|
|
622
|
-
log(`columnList: Loop ended. Current token: ${this.peek().lexeme} (${this.peek().type})`); // DEBUG
|
|
623
|
-
return columns;
|
|
624
|
-
}
|
|
625
|
-
|
|
626
|
-
/**
|
|
627
|
-
* Parse a table identifier (possibly schema-qualified)
|
|
628
|
-
*/
|
|
629
|
-
private tableIdentifier(): AST.IdentifierExpr {
|
|
630
|
-
const startToken = this.peek();
|
|
631
|
-
let schema: string | undefined;
|
|
632
|
-
let name: string;
|
|
633
|
-
let endToken = startToken;
|
|
634
|
-
const contextualKeywords = ['key', 'action', 'set', 'default', 'check', 'unique', 'references', 'on', 'cascade', 'restrict', 'like', 'temp', 'temporary'];
|
|
635
|
-
|
|
636
|
-
// Check for schema.table pattern
|
|
637
|
-
if (this.checkIdentifierLike(contextualKeywords) && this.checkNext(1, TokenType.DOT)) {
|
|
638
|
-
schema = this.consumeIdentifier(contextualKeywords, "Expected schema name.");
|
|
639
|
-
this.advance(); // Consume DOT
|
|
640
|
-
name = this.consumeIdentifier(contextualKeywords, "Expected table name after schema.");
|
|
641
|
-
endToken = this.previous();
|
|
642
|
-
} else if (this.checkIdentifierLike(contextualKeywords)) {
|
|
643
|
-
name = this.consumeIdentifier(contextualKeywords, "Expected table name.");
|
|
644
|
-
endToken = this.previous();
|
|
645
|
-
} else {
|
|
646
|
-
throw this.error(this.peek(), "Expected table name.");
|
|
647
|
-
}
|
|
648
|
-
|
|
649
|
-
return {
|
|
650
|
-
type: 'identifier',
|
|
651
|
-
name,
|
|
652
|
-
schema,
|
|
653
|
-
loc: _createLoc(startToken, endToken),
|
|
654
|
-
};
|
|
655
|
-
}
|
|
656
|
-
|
|
657
|
-
/**
|
|
658
|
-
* Parse a comma-separated list of table sources (FROM clause)
|
|
659
|
-
*/
|
|
660
|
-
private tableSourceList(withClause?: AST.WithClause): AST.FromClause[] {
|
|
661
|
-
const sources: AST.FromClause[] = [];
|
|
662
|
-
|
|
663
|
-
do {
|
|
664
|
-
// Get the base table source
|
|
665
|
-
let source: AST.FromClause = this.tableSource(withClause);
|
|
666
|
-
|
|
667
|
-
// Look for JOINs
|
|
668
|
-
while (this.isJoinToken()) {
|
|
669
|
-
source = this.joinClause(source, withClause);
|
|
670
|
-
}
|
|
671
|
-
|
|
672
|
-
sources.push(source);
|
|
673
|
-
} while (this.match(TokenType.COMMA));
|
|
674
|
-
|
|
675
|
-
return sources;
|
|
676
|
-
}
|
|
677
|
-
|
|
678
|
-
/**
|
|
679
|
-
* Parse a single table source, which can now be a table name, table-valued function call, or subquery
|
|
680
|
-
*/
|
|
681
|
-
private tableSource(withClause?: AST.WithClause): AST.FromClause {
|
|
682
|
-
const startToken = this.peek();
|
|
683
|
-
const contextualKeywords = ['key', 'action', 'set', 'default', 'check', 'unique', 'references', 'on', 'cascade', 'restrict', 'like'];
|
|
684
|
-
|
|
685
|
-
// Check for subquery: ( SELECT ... or ( VALUES ... or ( INSERT/UPDATE/DELETE ...
|
|
686
|
-
if (this.check(TokenType.LPAREN)) {
|
|
687
|
-
// Look ahead to see if this is a subquery
|
|
688
|
-
const lookahead = this.current + 1;
|
|
689
|
-
if (lookahead < this.tokens.length) {
|
|
690
|
-
const nextTokenType = this.tokens[lookahead].type;
|
|
691
|
-
if (nextTokenType === TokenType.SELECT || nextTokenType === TokenType.VALUES) {
|
|
692
|
-
return this.subquerySource(startToken, withClause);
|
|
693
|
-
} else if (nextTokenType === TokenType.INSERT || nextTokenType === TokenType.UPDATE || nextTokenType === TokenType.DELETE) {
|
|
694
|
-
return this.mutatingSubquerySource(startToken, withClause);
|
|
695
|
-
}
|
|
696
|
-
}
|
|
697
|
-
}
|
|
698
|
-
|
|
699
|
-
// Check for function call syntax: IDENTIFIER (
|
|
700
|
-
if (this.checkIdentifierLike(contextualKeywords) && this.checkNext(1, TokenType.LPAREN)) {
|
|
701
|
-
return this.functionSource(startToken);
|
|
702
|
-
}
|
|
703
|
-
// Otherwise, assume it's a standard table source
|
|
704
|
-
else {
|
|
705
|
-
return this.standardTableSource(startToken);
|
|
706
|
-
}
|
|
707
|
-
}
|
|
708
|
-
|
|
709
|
-
/** Parses a subquery source: (SELECT ...) AS alias */
|
|
710
|
-
private subquerySource(startToken: Token, withClause?: AST.WithClause): AST.SubquerySource {
|
|
711
|
-
this.consume(TokenType.LPAREN, "Expected '(' before subquery.");
|
|
712
|
-
|
|
713
|
-
let subquery: AST.SelectStmt | AST.ValuesStmt;
|
|
714
|
-
|
|
715
|
-
if (this.check(TokenType.SELECT)) {
|
|
716
|
-
// Consume the SELECT token and pass it as startToken to selectStatement
|
|
717
|
-
const selectToken = this.advance();
|
|
718
|
-
subquery = this.selectStatement(selectToken, withClause);
|
|
719
|
-
} else if (this.check(TokenType.VALUES)) {
|
|
720
|
-
// Handle VALUES subquery
|
|
721
|
-
const valuesToken = this.advance();
|
|
722
|
-
subquery = this.valuesStatement(valuesToken);
|
|
723
|
-
} else {
|
|
724
|
-
throw this.error(this.peek(), "Expected 'SELECT' or 'VALUES' in subquery.");
|
|
725
|
-
}
|
|
726
|
-
|
|
727
|
-
this.consume(TokenType.RPAREN, "Expected ')' after subquery.");
|
|
728
|
-
|
|
729
|
-
// Parse optional alias for subquery
|
|
730
|
-
let alias: string;
|
|
731
|
-
let columns: string[] | undefined;
|
|
732
|
-
|
|
733
|
-
if (this.match(TokenType.AS)) {
|
|
734
|
-
if (!this.checkIdentifierLike([])) {
|
|
735
|
-
throw this.error(this.peek(), "Expected alias after 'AS'.");
|
|
736
|
-
}
|
|
737
|
-
alias = this.getIdentifierValue(this.advance());
|
|
738
|
-
} else if (this.checkIdentifierLike([]) &&
|
|
739
|
-
!this.checkNext(1, TokenType.DOT) &&
|
|
740
|
-
!this.checkNext(1, TokenType.COMMA) &&
|
|
741
|
-
!this.isJoinToken() &&
|
|
742
|
-
!this.isEndOfClause()) {
|
|
743
|
-
alias = this.getIdentifierValue(this.advance());
|
|
744
|
-
} else {
|
|
745
|
-
// Generate a default alias if none provided
|
|
746
|
-
alias = `subquery_${startToken.startOffset}`;
|
|
747
|
-
}
|
|
748
|
-
|
|
749
|
-
// Parse optional column list after alias: AS alias(col1, col2, ...)
|
|
750
|
-
if (this.match(TokenType.LPAREN)) {
|
|
751
|
-
columns = [];
|
|
752
|
-
const contextualKeywords = ['key', 'action', 'set', 'default', 'check', 'unique', 'references', 'on', 'cascade', 'restrict', 'like'];
|
|
753
|
-
|
|
754
|
-
if (!this.check(TokenType.RPAREN)) {
|
|
755
|
-
do {
|
|
756
|
-
columns.push(this.consumeIdentifier(contextualKeywords, "Expected column name in alias column list."));
|
|
757
|
-
} while (this.match(TokenType.COMMA) && !this.check(TokenType.RPAREN));
|
|
758
|
-
}
|
|
759
|
-
this.consume(TokenType.RPAREN, "Expected ')' after alias column list.");
|
|
760
|
-
}
|
|
761
|
-
|
|
762
|
-
const endToken = this.previous();
|
|
763
|
-
return {
|
|
764
|
-
type: 'subquerySource',
|
|
765
|
-
subquery,
|
|
766
|
-
alias,
|
|
767
|
-
columns,
|
|
768
|
-
loc: _createLoc(startToken, endToken),
|
|
769
|
-
};
|
|
770
|
-
}
|
|
771
|
-
|
|
772
|
-
/** Parses a mutating subquery source: (INSERT/UPDATE/DELETE ... RETURNING ...) AS alias */
|
|
773
|
-
private mutatingSubquerySource(startToken: Token, withClause?: AST.WithClause): AST.MutatingSubquerySource {
|
|
774
|
-
this.consume(TokenType.LPAREN, "Expected '(' before mutating subquery.");
|
|
775
|
-
|
|
776
|
-
let stmt: AST.InsertStmt | AST.UpdateStmt | AST.DeleteStmt;
|
|
777
|
-
|
|
778
|
-
if (this.check(TokenType.INSERT)) {
|
|
779
|
-
const insertToken = this.advance();
|
|
780
|
-
stmt = this.insertStatement(insertToken, withClause);
|
|
781
|
-
} else if (this.check(TokenType.UPDATE)) {
|
|
782
|
-
const updateToken = this.advance();
|
|
783
|
-
stmt = this.updateStatement(updateToken, withClause);
|
|
784
|
-
} else if (this.check(TokenType.DELETE)) {
|
|
785
|
-
const deleteToken = this.advance();
|
|
786
|
-
stmt = this.deleteStatement(deleteToken, withClause);
|
|
787
|
-
} else {
|
|
788
|
-
throw this.error(this.peek(), "Expected 'INSERT', 'UPDATE', or 'DELETE' in mutating subquery.");
|
|
789
|
-
}
|
|
790
|
-
|
|
791
|
-
// Validate that the statement has a RETURNING clause
|
|
792
|
-
if (!stmt.returning || stmt.returning.length === 0) {
|
|
793
|
-
throw this.error(this.previous(), "Mutating subqueries must have a RETURNING clause to be used as table sources.");
|
|
794
|
-
}
|
|
795
|
-
|
|
796
|
-
this.consume(TokenType.RPAREN, "Expected ')' after mutating subquery.");
|
|
797
|
-
|
|
798
|
-
// Parse optional alias for mutating subquery
|
|
799
|
-
let alias: string;
|
|
800
|
-
let columns: string[] | undefined;
|
|
801
|
-
|
|
802
|
-
if (this.match(TokenType.AS)) {
|
|
803
|
-
if (!this.checkIdentifierLike([])) {
|
|
804
|
-
throw this.error(this.peek(), "Expected alias after 'AS'.");
|
|
805
|
-
}
|
|
806
|
-
alias = this.getIdentifierValue(this.advance());
|
|
807
|
-
} else if (this.checkIdentifierLike([]) &&
|
|
808
|
-
!this.checkNext(1, TokenType.DOT) &&
|
|
809
|
-
!this.checkNext(1, TokenType.COMMA) &&
|
|
810
|
-
!this.isJoinToken() &&
|
|
811
|
-
!this.isEndOfClause()) {
|
|
812
|
-
alias = this.getIdentifierValue(this.advance());
|
|
813
|
-
} else {
|
|
814
|
-
// Generate a default alias if none provided
|
|
815
|
-
alias = `mutating_subquery_${startToken.startOffset}`;
|
|
816
|
-
}
|
|
817
|
-
|
|
818
|
-
// Parse optional column list after alias: AS alias(col1, col2, ...)
|
|
819
|
-
if (this.match(TokenType.LPAREN)) {
|
|
820
|
-
columns = [];
|
|
821
|
-
const contextualKeywords = ['key', 'action', 'set', 'default', 'check', 'unique', 'references', 'on', 'cascade', 'restrict', 'like'];
|
|
822
|
-
|
|
823
|
-
if (!this.check(TokenType.RPAREN)) {
|
|
824
|
-
do {
|
|
825
|
-
columns.push(this.consumeIdentifier(contextualKeywords, "Expected column name in alias column list."));
|
|
826
|
-
} while (this.match(TokenType.COMMA) && !this.check(TokenType.RPAREN));
|
|
827
|
-
}
|
|
828
|
-
this.consume(TokenType.RPAREN, "Expected ')' after alias column list.");
|
|
829
|
-
}
|
|
830
|
-
|
|
831
|
-
const endToken = this.previous();
|
|
832
|
-
return {
|
|
833
|
-
type: 'mutatingSubquerySource',
|
|
834
|
-
stmt,
|
|
835
|
-
alias,
|
|
836
|
-
columns,
|
|
837
|
-
loc: _createLoc(startToken, endToken),
|
|
838
|
-
};
|
|
839
|
-
}
|
|
840
|
-
|
|
841
|
-
/** Parses a standard table source (schema.table or table) */
|
|
842
|
-
private standardTableSource(startToken: Token): AST.TableSource {
|
|
843
|
-
const contextualKeywords = ['key', 'action', 'set', 'default', 'check', 'unique', 'references', 'on', 'cascade', 'restrict', 'like'];
|
|
844
|
-
|
|
845
|
-
// Parse table name (potentially schema-qualified)
|
|
846
|
-
const table = this.tableIdentifier();
|
|
847
|
-
let endToken = this.previous(); // Initialize endToken after parsing table identifier
|
|
848
|
-
|
|
849
|
-
// Parse optional alias
|
|
850
|
-
let alias: string | undefined;
|
|
851
|
-
if (this.match(TokenType.AS)) {
|
|
852
|
-
if (!this.checkIdentifierLike(contextualKeywords)) {
|
|
853
|
-
throw this.error(this.peek(), "Expected alias after 'AS'.");
|
|
854
|
-
}
|
|
855
|
-
const aliasToken = this.advance();
|
|
856
|
-
alias = this.getIdentifierValue(aliasToken);
|
|
857
|
-
endToken = aliasToken;
|
|
858
|
-
} else if (this.checkIdentifierLike([]) &&
|
|
859
|
-
!this.checkNext(1, TokenType.DOT) &&
|
|
860
|
-
!this.checkNext(1, TokenType.COMMA) &&
|
|
861
|
-
!this.isJoinToken() &&
|
|
862
|
-
!this.isEndOfClause()) {
|
|
863
|
-
const aliasToken = this.advance();
|
|
864
|
-
alias = this.getIdentifierValue(aliasToken);
|
|
865
|
-
endToken = aliasToken;
|
|
866
|
-
}
|
|
867
|
-
|
|
868
|
-
return {
|
|
869
|
-
type: 'table',
|
|
870
|
-
table,
|
|
871
|
-
alias,
|
|
872
|
-
loc: _createLoc(startToken, endToken),
|
|
873
|
-
};
|
|
874
|
-
}
|
|
875
|
-
|
|
876
|
-
/** Parses a table-valued function source: name(arg1, ...) [AS alias] */
|
|
877
|
-
private functionSource(startToken: Token): AST.FunctionSource {
|
|
878
|
-
const contextualKeywords = ['key', 'action', 'set', 'default', 'check', 'unique', 'references', 'on', 'cascade', 'restrict', 'like'];
|
|
879
|
-
|
|
880
|
-
const name = this.tableIdentifier(); // name has its own loc
|
|
881
|
-
let endToken = this.previous(); // Initialize endToken after parsing function identifier
|
|
882
|
-
|
|
883
|
-
this.consume(TokenType.LPAREN, "Expected '(' after table function name.");
|
|
884
|
-
|
|
885
|
-
const args: AST.Expression[] = [];
|
|
886
|
-
if (!this.check(TokenType.RPAREN)) {
|
|
887
|
-
// Handle DISTINCT inside function calls like COUNT(DISTINCT col)
|
|
888
|
-
const distinct = this.matchKeyword('DISTINCT');
|
|
889
|
-
// Handle * argument AFTER checking for distinct
|
|
890
|
-
if (this.match(TokenType.ASTERISK)) {
|
|
891
|
-
// Do not add '*' as an argument to the list for aggregates like COUNT(*)
|
|
892
|
-
if (args.length > 0 || distinct) {
|
|
893
|
-
// '*' is only valid as the *only* argument, potentially after DISTINCT
|
|
894
|
-
// e.g. COUNT(*), COUNT(DISTINCT *) - though DISTINCT * might not be standard SQL?
|
|
895
|
-
// For now, disallow '*' if other args exist.
|
|
896
|
-
throw this.error(this.previous(), "'*' cannot be used with other arguments in function call.");
|
|
897
|
-
}
|
|
898
|
-
// If we parsed '*', the args list remains empty.
|
|
899
|
-
} else {
|
|
900
|
-
// Parse regular arguments if '*' wasn't found
|
|
901
|
-
do {
|
|
902
|
-
args.push(this.expression());
|
|
903
|
-
} while (this.match(TokenType.COMMA));
|
|
904
|
-
}
|
|
905
|
-
}
|
|
906
|
-
|
|
907
|
-
endToken = this.consume(TokenType.RPAREN, "Expected ')' after table function arguments.");
|
|
908
|
-
|
|
909
|
-
// Parse optional alias (same logic as for standard tables)
|
|
910
|
-
let alias: string | undefined;
|
|
911
|
-
let columns: string[] | undefined;
|
|
912
|
-
if (this.match(TokenType.AS)) {
|
|
913
|
-
if (!this.checkIdentifierLike(contextualKeywords)) {
|
|
914
|
-
throw this.error(this.peek(), "Expected alias after 'AS'.");
|
|
915
|
-
}
|
|
916
|
-
const aliasToken = this.advance();
|
|
917
|
-
alias = this.getIdentifierValue(aliasToken);
|
|
918
|
-
endToken = aliasToken;
|
|
919
|
-
} else if (this.checkIdentifierLike([]) &&
|
|
920
|
-
!this.checkNext(1, TokenType.DOT) &&
|
|
921
|
-
!this.checkNext(1, TokenType.COMMA) &&
|
|
922
|
-
!this.isJoinToken() &&
|
|
923
|
-
!this.isEndOfClause()) {
|
|
924
|
-
const aliasToken = this.advance();
|
|
925
|
-
alias = this.getIdentifierValue(aliasToken);
|
|
926
|
-
endToken = aliasToken;
|
|
927
|
-
}
|
|
928
|
-
|
|
929
|
-
// Optional column list after alias: alias(col1, col2, ...)
|
|
930
|
-
if (alias && this.match(TokenType.LPAREN)) {
|
|
931
|
-
columns = [];
|
|
932
|
-
const colKeywords = ['key', 'action', 'set', 'default', 'check', 'unique', 'references', 'on', 'cascade', 'restrict', 'like'];
|
|
933
|
-
if (!this.check(TokenType.RPAREN)) {
|
|
934
|
-
do {
|
|
935
|
-
columns.push(this.consumeIdentifier(colKeywords, "Expected column name in alias column list."));
|
|
936
|
-
} while (this.match(TokenType.COMMA) && !this.check(TokenType.RPAREN));
|
|
937
|
-
}
|
|
938
|
-
endToken = this.consume(TokenType.RPAREN, "Expected ')' after alias column list.");
|
|
939
|
-
}
|
|
940
|
-
|
|
941
|
-
return {
|
|
942
|
-
type: 'functionSource',
|
|
943
|
-
name,
|
|
944
|
-
args,
|
|
945
|
-
alias,
|
|
946
|
-
columns,
|
|
947
|
-
loc: _createLoc(startToken, endToken),
|
|
948
|
-
};
|
|
949
|
-
}
|
|
950
|
-
|
|
951
|
-
/**
|
|
952
|
-
* Parse a JOIN clause
|
|
953
|
-
*/
|
|
954
|
-
private joinClause(left: AST.FromClause, withClause?: AST.WithClause): AST.JoinClause {
|
|
955
|
-
const joinStartToken = this.peek(); // Capture token before parsing JOIN type
|
|
956
|
-
|
|
957
|
-
// Determine join type
|
|
958
|
-
let joinType: 'inner' | 'left' | 'right' | 'full' | 'cross' = 'inner';
|
|
959
|
-
|
|
960
|
-
if (this.match(TokenType.LEFT)) {
|
|
961
|
-
this.match(TokenType.OUTER); // optional
|
|
962
|
-
joinType = 'left';
|
|
963
|
-
} else if (this.match(TokenType.RIGHT)) {
|
|
964
|
-
this.match(TokenType.OUTER); // optional
|
|
965
|
-
joinType = 'right';
|
|
966
|
-
} else if (this.match(TokenType.FULL)) {
|
|
967
|
-
this.match(TokenType.OUTER); // optional
|
|
968
|
-
joinType = 'full';
|
|
969
|
-
} else if (this.match(TokenType.CROSS)) {
|
|
970
|
-
joinType = 'cross';
|
|
971
|
-
} else if (this.match(TokenType.INNER)) {
|
|
972
|
-
joinType = 'inner';
|
|
973
|
-
}
|
|
974
|
-
|
|
975
|
-
// Consume JOIN token
|
|
976
|
-
this.consume(TokenType.JOIN, "Expected 'JOIN'.");
|
|
977
|
-
|
|
978
|
-
// Optional LATERAL before right side
|
|
979
|
-
const _isLateral = this.match(TokenType.LATERAL);
|
|
980
|
-
// Parse right side of join
|
|
981
|
-
const right = this.tableSource(withClause);
|
|
982
|
-
|
|
983
|
-
// Parse join condition
|
|
984
|
-
let condition: AST.Expression | undefined;
|
|
985
|
-
let columns: string[] | undefined;
|
|
986
|
-
let endToken = this.previous(); // End token is end of right source initially
|
|
987
|
-
|
|
988
|
-
if (this.match(TokenType.ON)) {
|
|
989
|
-
condition = this.expression();
|
|
990
|
-
endToken = this.previous(); // End token is end of ON expression
|
|
991
|
-
} else if (this.match(TokenType.USING)) {
|
|
992
|
-
this.consume(TokenType.LPAREN, "Expected '(' after 'USING'.");
|
|
993
|
-
columns = [];
|
|
994
|
-
const contextualKeywords = ['key', 'action', 'set', 'default', 'check', 'unique', 'references', 'on', 'cascade', 'restrict', 'like'];
|
|
995
|
-
|
|
996
|
-
do {
|
|
997
|
-
columns.push(this.consumeIdentifier(contextualKeywords, "Expected column name."));
|
|
998
|
-
} while (this.match(TokenType.COMMA));
|
|
999
|
-
|
|
1000
|
-
endToken = this.consume(TokenType.RPAREN, "Expected ')' after columns.");
|
|
1001
|
-
} else if (joinType !== 'cross') {
|
|
1002
|
-
throw this.error(this.peek(), "Expected 'ON' or 'USING' after JOIN.");
|
|
1003
|
-
}
|
|
1004
|
-
|
|
1005
|
-
return {
|
|
1006
|
-
type: 'join',
|
|
1007
|
-
joinType,
|
|
1008
|
-
left,
|
|
1009
|
-
right,
|
|
1010
|
-
condition,
|
|
1011
|
-
columns,
|
|
1012
|
-
loc: _createLoc(joinStartToken, endToken),
|
|
1013
|
-
};
|
|
1014
|
-
}
|
|
1015
|
-
|
|
1016
|
-
/**
|
|
1017
|
-
* Parse an expression
|
|
1018
|
-
*/
|
|
1019
|
-
private expression(): AST.Expression {
|
|
1020
|
-
return this.logicalXorOr();
|
|
1021
|
-
}
|
|
1022
|
-
|
|
1023
|
-
/**
|
|
1024
|
-
* Parse logical OR and XOR expressions (lowest precedence)
|
|
1025
|
-
*/
|
|
1026
|
-
private logicalXorOr(): AST.Expression {
|
|
1027
|
-
let expr = this.logicalAnd();
|
|
1028
|
-
const startToken = expr.loc ? this.tokens.find(t => t.startOffset === expr.loc!.start.offset) ?? this.peek() : this.peek(); // Get start token of left expr
|
|
1029
|
-
|
|
1030
|
-
while (this.match(TokenType.OR, TokenType.XOR)) { // Added XOR
|
|
1031
|
-
const operator = this.previous().type === TokenType.XOR ? 'XOR' : 'OR'; // Determine operator
|
|
1032
|
-
const right = this.logicalAnd();
|
|
1033
|
-
const endToken = this.previous(); // End token is end of right expr
|
|
1034
|
-
expr = {
|
|
1035
|
-
type: 'binary',
|
|
1036
|
-
operator,
|
|
1037
|
-
left: expr,
|
|
1038
|
-
right,
|
|
1039
|
-
loc: _createLoc(startToken, endToken),
|
|
1040
|
-
};
|
|
1041
|
-
}
|
|
1042
|
-
|
|
1043
|
-
return expr;
|
|
1044
|
-
}
|
|
1045
|
-
|
|
1046
|
-
/**
|
|
1047
|
-
* Parse logical AND expression
|
|
1048
|
-
*/
|
|
1049
|
-
private logicalAnd(): AST.Expression {
|
|
1050
|
-
let expr = this.isNull();
|
|
1051
|
-
const startToken = expr.loc ? this.tokens.find(t => t.startOffset === expr.loc!.start.offset) ?? this.peek() : this.peek(); // Get start token of left expr
|
|
1052
|
-
|
|
1053
|
-
while (this.match(TokenType.AND)) {
|
|
1054
|
-
const operator = 'AND';
|
|
1055
|
-
const right = this.isNull();
|
|
1056
|
-
const endToken = this.previous(); // End token is end of right expr
|
|
1057
|
-
expr = {
|
|
1058
|
-
type: 'binary',
|
|
1059
|
-
operator,
|
|
1060
|
-
left: expr,
|
|
1061
|
-
right,
|
|
1062
|
-
loc: _createLoc(startToken, endToken),
|
|
1063
|
-
};
|
|
1064
|
-
}
|
|
1065
|
-
|
|
1066
|
-
return expr;
|
|
1067
|
-
}
|
|
1068
|
-
|
|
1069
|
-
/**
|
|
1070
|
-
* Parse IS NULL / IS NOT NULL expressions
|
|
1071
|
-
*/
|
|
1072
|
-
private isNull(): AST.Expression {
|
|
1073
|
-
const expr = this.equality();
|
|
1074
|
-
const startToken = expr.loc ? this.tokens.find(t => t.startOffset === expr.loc!.start.offset) ?? this.peek() : this.peek(); // Get start token of left expr
|
|
1075
|
-
|
|
1076
|
-
if (this.match(TokenType.IS)) {
|
|
1077
|
-
let isNot = false;
|
|
1078
|
-
if (this.match(TokenType.NOT)) {
|
|
1079
|
-
isNot = true;
|
|
1080
|
-
}
|
|
1081
|
-
if (this.match(TokenType.NULL)) {
|
|
1082
|
-
const endToken = this.previous(); // End token is NULL
|
|
1083
|
-
const operator = isNot ? 'IS NOT NULL' : 'IS NULL';
|
|
1084
|
-
// Represent IS NULL / IS NOT NULL as UnaryExpr for simplicity
|
|
1085
|
-
return { type: 'unary', operator, expr, loc: _createLoc(startToken, endToken) };
|
|
1086
|
-
}
|
|
1087
|
-
// If it was IS or IS NOT but not followed by NULL, maybe it's IS TRUE/FALSE/DISTINCT FROM?
|
|
1088
|
-
// For now, assume standard comparison if NULL doesn't follow IS [NOT]
|
|
1089
|
-
// We need to "unread" the IS and optional NOT token if we didn't match NULL.
|
|
1090
|
-
// Backtrack current position.
|
|
1091
|
-
if (isNot) this.current--; // Backtrack NOT
|
|
1092
|
-
this.current--; // Backtrack IS
|
|
1093
|
-
}
|
|
1094
|
-
|
|
1095
|
-
return expr;
|
|
1096
|
-
}
|
|
1097
|
-
|
|
1098
|
-
/**
|
|
1099
|
-
* Parse equality expression
|
|
1100
|
-
*/
|
|
1101
|
-
private equality(): AST.Expression {
|
|
1102
|
-
let expr = this.comparison();
|
|
1103
|
-
const startToken = expr.loc ? this.tokens.find(t => t.startOffset === expr.loc!.start.offset) ?? this.peek() : this.peek(); // Get start token of left expr
|
|
1104
|
-
|
|
1105
|
-
while (this.match(TokenType.EQUAL, TokenType.EQUAL_EQUAL, TokenType.NOT_EQUAL)) {
|
|
1106
|
-
let operator: string;
|
|
1107
|
-
switch (this.previous().type) {
|
|
1108
|
-
case TokenType.NOT_EQUAL: operator = '!='; break;
|
|
1109
|
-
case TokenType.EQUAL_EQUAL: operator = '=='; break;
|
|
1110
|
-
default: operator = '='; break;
|
|
1111
|
-
}
|
|
1112
|
-
const right = this.comparison();
|
|
1113
|
-
const endToken = this.previous(); // End token is end of right expr
|
|
1114
|
-
expr = {
|
|
1115
|
-
type: 'binary',
|
|
1116
|
-
operator,
|
|
1117
|
-
left: expr,
|
|
1118
|
-
right,
|
|
1119
|
-
loc: _createLoc(startToken, endToken),
|
|
1120
|
-
};
|
|
1121
|
-
}
|
|
1122
|
-
|
|
1123
|
-
return expr;
|
|
1124
|
-
}
|
|
1125
|
-
|
|
1126
|
-
/**
|
|
1127
|
-
* Parse comparison expression
|
|
1128
|
-
*/
|
|
1129
|
-
private comparison(): AST.Expression {
|
|
1130
|
-
let expr = this.term();
|
|
1131
|
-
const startToken = expr.loc ? this.tokens.find(t => t.startOffset === expr.loc!.start.offset) ?? this.peek() : this.peek(); // Get start token of left expr
|
|
1132
|
-
|
|
1133
|
-
while (this.match(
|
|
1134
|
-
TokenType.LESS, TokenType.LESS_EQUAL,
|
|
1135
|
-
TokenType.GREATER, TokenType.GREATER_EQUAL,
|
|
1136
|
-
TokenType.BETWEEN, TokenType.IN, TokenType.NOT,
|
|
1137
|
-
TokenType.LIKE
|
|
1138
|
-
)) {
|
|
1139
|
-
const operatorToken = this.previous();
|
|
1140
|
-
|
|
1141
|
-
// Handle NOT IN, NOT BETWEEN, and NOT LIKE
|
|
1142
|
-
if (operatorToken.type === TokenType.NOT) {
|
|
1143
|
-
const notStartToken = operatorToken;
|
|
1144
|
-
if (this.match(TokenType.IN)) {
|
|
1145
|
-
// NOT IN
|
|
1146
|
-
this.consume(TokenType.LPAREN, "Expected '(' after NOT IN.");
|
|
1147
|
-
|
|
1148
|
-
if (this.check(TokenType.SELECT)) {
|
|
1149
|
-
// NOT IN subquery: expr NOT IN (SELECT ...)
|
|
1150
|
-
const selectToken = this.advance(); // Consume SELECT
|
|
1151
|
-
const subquery = this.selectStatement(selectToken);
|
|
1152
|
-
const endToken = this.consume(TokenType.RPAREN, "Expected ')' after NOT IN subquery.");
|
|
1153
|
-
|
|
1154
|
-
// Create an IN expression with subquery, then wrap in NOT
|
|
1155
|
-
const inExpr: AST.InExpr = {
|
|
1156
|
-
type: 'in',
|
|
1157
|
-
expr,
|
|
1158
|
-
subquery,
|
|
1159
|
-
loc: _createLoc(startToken, endToken),
|
|
1160
|
-
};
|
|
1161
|
-
|
|
1162
|
-
expr = {
|
|
1163
|
-
type: 'unary',
|
|
1164
|
-
operator: 'NOT',
|
|
1165
|
-
expr: inExpr,
|
|
1166
|
-
loc: _createLoc(notStartToken, endToken),
|
|
1167
|
-
};
|
|
1168
|
-
} else {
|
|
1169
|
-
// NOT IN value list: expr NOT IN (value1, value2, ...)
|
|
1170
|
-
const values: AST.Expression[] = [];
|
|
1171
|
-
if (!this.check(TokenType.RPAREN)) {
|
|
1172
|
-
do {
|
|
1173
|
-
values.push(this.expression());
|
|
1174
|
-
} while (this.match(TokenType.COMMA));
|
|
1175
|
-
}
|
|
1176
|
-
const endToken = this.consume(TokenType.RPAREN, "Expected ')' after NOT IN values.");
|
|
1177
|
-
|
|
1178
|
-
// Create an IN expression with value list, then wrap in NOT
|
|
1179
|
-
const inExpr: AST.InExpr = {
|
|
1180
|
-
type: 'in',
|
|
1181
|
-
expr,
|
|
1182
|
-
values,
|
|
1183
|
-
loc: _createLoc(startToken, endToken),
|
|
1184
|
-
};
|
|
1185
|
-
|
|
1186
|
-
expr = {
|
|
1187
|
-
type: 'unary',
|
|
1188
|
-
operator: 'NOT',
|
|
1189
|
-
expr: inExpr,
|
|
1190
|
-
loc: _createLoc(notStartToken, endToken),
|
|
1191
|
-
};
|
|
1192
|
-
}
|
|
1193
|
-
} else if (this.match(TokenType.BETWEEN)) {
|
|
1194
|
-
// NOT BETWEEN
|
|
1195
|
-
const lower = this.term();
|
|
1196
|
-
this.consume(TokenType.AND, "Expected 'AND' after NOT BETWEEN lower bound.");
|
|
1197
|
-
const upper = this.term();
|
|
1198
|
-
const endToken = this.previous(); // End token is end of upper expr
|
|
1199
|
-
|
|
1200
|
-
// Create the NOT BETWEEN expression as a dedicated node type
|
|
1201
|
-
expr = {
|
|
1202
|
-
type: 'between',
|
|
1203
|
-
expr,
|
|
1204
|
-
lower,
|
|
1205
|
-
upper,
|
|
1206
|
-
not: true,
|
|
1207
|
-
loc: _createLoc(notStartToken, endToken),
|
|
1208
|
-
};
|
|
1209
|
-
} else if (this.match(TokenType.LIKE)) {
|
|
1210
|
-
// NOT LIKE
|
|
1211
|
-
const pattern = this.term();
|
|
1212
|
-
const endToken = this.previous(); // End token is end of pattern expr
|
|
1213
|
-
|
|
1214
|
-
// Create the LIKE expression as a binary expression, then wrap in NOT
|
|
1215
|
-
const likeExpr: AST.BinaryExpr = {
|
|
1216
|
-
type: 'binary',
|
|
1217
|
-
operator: 'LIKE',
|
|
1218
|
-
left: expr,
|
|
1219
|
-
right: pattern,
|
|
1220
|
-
loc: _createLoc(startToken, endToken),
|
|
1221
|
-
};
|
|
1222
|
-
|
|
1223
|
-
expr = {
|
|
1224
|
-
type: 'unary',
|
|
1225
|
-
operator: 'NOT',
|
|
1226
|
-
expr: likeExpr,
|
|
1227
|
-
loc: _createLoc(notStartToken, endToken),
|
|
1228
|
-
};
|
|
1229
|
-
} else {
|
|
1230
|
-
// Put back the NOT token and break out of the loop
|
|
1231
|
-
this.current--;
|
|
1232
|
-
break;
|
|
1233
|
-
}
|
|
1234
|
-
} else if (operatorToken.type === TokenType.LIKE) {
|
|
1235
|
-
// Parse LIKE expression: expr LIKE pattern
|
|
1236
|
-
const pattern = this.term();
|
|
1237
|
-
const endToken = this.previous(); // End token is end of pattern expr
|
|
1238
|
-
|
|
1239
|
-
// Create the LIKE expression as a binary expression
|
|
1240
|
-
expr = {
|
|
1241
|
-
type: 'binary',
|
|
1242
|
-
operator: 'LIKE',
|
|
1243
|
-
left: expr,
|
|
1244
|
-
right: pattern,
|
|
1245
|
-
loc: _createLoc(startToken, endToken),
|
|
1246
|
-
};
|
|
1247
|
-
} else if (operatorToken.type === TokenType.BETWEEN) {
|
|
1248
|
-
// Parse BETWEEN expression: expr BETWEEN low AND high
|
|
1249
|
-
const lower = this.term();
|
|
1250
|
-
this.consume(TokenType.AND, "Expected 'AND' after BETWEEN lower bound.");
|
|
1251
|
-
const upper = this.term();
|
|
1252
|
-
const endToken = this.previous(); // End token is end of upper expr
|
|
1253
|
-
|
|
1254
|
-
// Create the BETWEEN expression as a dedicated node type
|
|
1255
|
-
expr = {
|
|
1256
|
-
type: 'between',
|
|
1257
|
-
expr,
|
|
1258
|
-
lower,
|
|
1259
|
-
upper,
|
|
1260
|
-
loc: _createLoc(startToken, endToken),
|
|
1261
|
-
};
|
|
1262
|
-
} else if (operatorToken.type === TokenType.IN) {
|
|
1263
|
-
// Parse IN expression: expr IN (value1, value2, ...) or expr IN (subquery)
|
|
1264
|
-
this.consume(TokenType.LPAREN, "Expected '(' after IN.");
|
|
1265
|
-
|
|
1266
|
-
// Check if this is a subquery or value list
|
|
1267
|
-
if (this.check(TokenType.SELECT)) {
|
|
1268
|
-
// IN subquery: expr IN (SELECT ...)
|
|
1269
|
-
const selectToken = this.advance(); // Consume SELECT
|
|
1270
|
-
const subquery = this.selectStatement(selectToken);
|
|
1271
|
-
const endToken = this.consume(TokenType.RPAREN, "Expected ')' after IN subquery.");
|
|
1272
|
-
|
|
1273
|
-
// Create an IN expression with subquery
|
|
1274
|
-
expr = {
|
|
1275
|
-
type: 'in',
|
|
1276
|
-
expr,
|
|
1277
|
-
subquery,
|
|
1278
|
-
loc: _createLoc(startToken, endToken),
|
|
1279
|
-
};
|
|
1280
|
-
} else {
|
|
1281
|
-
// IN value list: expr IN (value1, value2, ...)
|
|
1282
|
-
const values: AST.Expression[] = [];
|
|
1283
|
-
if (!this.check(TokenType.RPAREN)) {
|
|
1284
|
-
do {
|
|
1285
|
-
values.push(this.expression());
|
|
1286
|
-
} while (this.match(TokenType.COMMA));
|
|
1287
|
-
}
|
|
1288
|
-
const endToken = this.consume(TokenType.RPAREN, "Expected ')' after IN values.");
|
|
1289
|
-
|
|
1290
|
-
// Create an IN expression with value list
|
|
1291
|
-
expr = {
|
|
1292
|
-
type: 'in',
|
|
1293
|
-
expr,
|
|
1294
|
-
values,
|
|
1295
|
-
loc: _createLoc(startToken, endToken),
|
|
1296
|
-
};
|
|
1297
|
-
}
|
|
1298
|
-
} else {
|
|
1299
|
-
// Handle other comparison operators
|
|
1300
|
-
let operator: string;
|
|
1301
|
-
switch (operatorToken.type) {
|
|
1302
|
-
case TokenType.LESS: operator = '<'; break;
|
|
1303
|
-
case TokenType.LESS_EQUAL: operator = '<='; break;
|
|
1304
|
-
case TokenType.GREATER: operator = '>'; break;
|
|
1305
|
-
case TokenType.GREATER_EQUAL: operator = '>='; break;
|
|
1306
|
-
default: operator = '?';
|
|
1307
|
-
}
|
|
1308
|
-
|
|
1309
|
-
const right = this.term();
|
|
1310
|
-
const endToken = this.previous(); // End token is end of right expr
|
|
1311
|
-
expr = {
|
|
1312
|
-
type: 'binary',
|
|
1313
|
-
operator,
|
|
1314
|
-
left: expr,
|
|
1315
|
-
right,
|
|
1316
|
-
loc: _createLoc(startToken, endToken),
|
|
1317
|
-
};
|
|
1318
|
-
}
|
|
1319
|
-
}
|
|
1320
|
-
|
|
1321
|
-
return expr;
|
|
1322
|
-
}
|
|
1323
|
-
|
|
1324
|
-
/**
|
|
1325
|
-
* Parse addition and subtraction
|
|
1326
|
-
*/
|
|
1327
|
-
private term(): AST.Expression {
|
|
1328
|
-
let expr = this.factor();
|
|
1329
|
-
const startToken = expr.loc ? this.tokens.find(t => t.startOffset === expr.loc!.start.offset) ?? this.peek() : this.peek(); // Get start token of left expr
|
|
1330
|
-
|
|
1331
|
-
while (this.match(TokenType.PLUS, TokenType.MINUS)) {
|
|
1332
|
-
const operator = this.previous().type === TokenType.PLUS ? '+' : '-';
|
|
1333
|
-
const right = this.factor();
|
|
1334
|
-
const endToken = this.previous(); // End token is end of right expr
|
|
1335
|
-
expr = {
|
|
1336
|
-
type: 'binary',
|
|
1337
|
-
operator,
|
|
1338
|
-
left: expr,
|
|
1339
|
-
right,
|
|
1340
|
-
loc: _createLoc(startToken, endToken),
|
|
1341
|
-
};
|
|
1342
|
-
}
|
|
1343
|
-
|
|
1344
|
-
return expr;
|
|
1345
|
-
}
|
|
1346
|
-
|
|
1347
|
-
/**
|
|
1348
|
-
* Parse multiplication and division
|
|
1349
|
-
*/
|
|
1350
|
-
private factor(): AST.Expression {
|
|
1351
|
-
// First, handle unary operators
|
|
1352
|
-
if (this.match(TokenType.MINUS, TokenType.PLUS, TokenType.TILDE, TokenType.NOT)) { // Added NOT
|
|
1353
|
-
const operatorToken = this.previous();
|
|
1354
|
-
const operatorStartToken = operatorToken; // Start token is the operator itself
|
|
1355
|
-
const operator = operatorToken.lexeme;
|
|
1356
|
-
// Unary operator applies to the result of the *next* precedence level (concatenation)
|
|
1357
|
-
const right = this.concatenation(); // Should call concatenation (higher precedence than factor)
|
|
1358
|
-
const endToken = this.previous(); // End token is end of the operand
|
|
1359
|
-
return { type: 'unary', operator, expr: right, loc: _createLoc(operatorStartToken, endToken) };
|
|
1360
|
-
}
|
|
1361
|
-
|
|
1362
|
-
let expr = this.concatenation(); // Factor operands have higher precedence (concatenation)
|
|
1363
|
-
const startToken = expr.loc ? this.tokens.find(t => t.startOffset === expr.loc!.start.offset) ?? this.peek() : this.peek(); // Get start token of left expr
|
|
1364
|
-
|
|
1365
|
-
while (this.match(TokenType.ASTERISK, TokenType.SLASH, TokenType.PERCENT)) {
|
|
1366
|
-
const operatorToken = this.previous();
|
|
1367
|
-
const operator = operatorToken.lexeme;
|
|
1368
|
-
const right = this.concatenation(); // Factor operands have higher precedence (concatenation)
|
|
1369
|
-
const endToken = this.previous(); // End token is end of right expr
|
|
1370
|
-
expr = { type: 'binary', operator, left: expr, right, loc: _createLoc(startToken, endToken) };
|
|
1371
|
-
}
|
|
1372
|
-
|
|
1373
|
-
return expr;
|
|
1374
|
-
}
|
|
1375
|
-
|
|
1376
|
-
/**
|
|
1377
|
-
* Parse concatenation expression (||)
|
|
1378
|
-
*/
|
|
1379
|
-
private concatenation(): AST.Expression {
|
|
1380
|
-
let expr = this.collateExpression(); // Concatenation operands have higher precedence (collate)
|
|
1381
|
-
const startToken = expr.loc ? this.tokens.find(t => t.startOffset === expr.loc!.start.offset) ?? this.peek() : this.peek(); // Get start token of left expr
|
|
1382
|
-
|
|
1383
|
-
while (this.match(TokenType.PIPE_PIPE)) {
|
|
1384
|
-
const operator = '||';
|
|
1385
|
-
const right = this.collateExpression(); // Concatenation operands have higher precedence (collate)
|
|
1386
|
-
const endToken = this.previous(); // End token is end of right expr
|
|
1387
|
-
expr = {
|
|
1388
|
-
type: 'binary',
|
|
1389
|
-
operator,
|
|
1390
|
-
left: expr,
|
|
1391
|
-
right,
|
|
1392
|
-
loc: _createLoc(startToken, endToken),
|
|
1393
|
-
};
|
|
1394
|
-
}
|
|
1395
|
-
|
|
1396
|
-
return expr;
|
|
1397
|
-
}
|
|
1398
|
-
|
|
1399
|
-
/**
|
|
1400
|
-
* Parse COLLATE expression
|
|
1401
|
-
*/
|
|
1402
|
-
private collateExpression(): AST.Expression {
|
|
1403
|
-
const expr = this.primary(); // Parse primary expression first
|
|
1404
|
-
|
|
1405
|
-
if (this.matchKeyword('COLLATE')) {
|
|
1406
|
-
const collationToken = this.consume(TokenType.IDENTIFIER, "Expected collation name after COLLATE.");
|
|
1407
|
-
const collation = collationToken.lexeme;
|
|
1408
|
-
// Use the start of the original expression and end of collation name for location
|
|
1409
|
-
const startLocToken = expr.loc?.start ? this.tokens.find(t => t.startOffset === expr.loc!.start.offset) ?? this.peek() : this.peek();
|
|
1410
|
-
return { type: 'collate', expr, collation, loc: _createLoc(startLocToken, collationToken) };
|
|
1411
|
-
}
|
|
1412
|
-
|
|
1413
|
-
return expr;
|
|
1414
|
-
}
|
|
1415
|
-
|
|
1416
|
-
/**
|
|
1417
|
-
* Parse primary expressions (literals, identifiers, etc.)
|
|
1418
|
-
*/
|
|
1419
|
-
private primary(): AST.Expression {
|
|
1420
|
-
const startToken = this.peek();
|
|
1421
|
-
|
|
1422
|
-
// Case expression
|
|
1423
|
-
if (this.matchKeyword('CASE')) {
|
|
1424
|
-
return this.parseCaseExpression(startToken);
|
|
1425
|
-
}
|
|
1426
|
-
|
|
1427
|
-
// CAST expression: CAST(expr AS type)
|
|
1428
|
-
if (this.peekKeyword('CAST') && this.checkNext(1, TokenType.LPAREN)) {
|
|
1429
|
-
const castToken = this.advance(); // Consume CAST
|
|
1430
|
-
this.consume(TokenType.LPAREN, "Expected '(' after CAST.");
|
|
1431
|
-
const expr = this.expression();
|
|
1432
|
-
this.consumeKeyword('AS', "Expected 'AS' in CAST expression.");
|
|
1433
|
-
// Allow type names that might be keywords (e.g., TEXT, INTEGER, REAL, BLOB)
|
|
1434
|
-
// or multi-word type names if supported (e.g., "VARCHAR(255)") - for now, simple identifier
|
|
1435
|
-
if (!this.check(TokenType.IDENTIFIER) &&
|
|
1436
|
-
!this.isTypeNameKeyword(this.peek().lexeme.toUpperCase())) {
|
|
1437
|
-
throw this.error(this.peek(), "Expected type name after 'AS' in CAST expression.");
|
|
1438
|
-
}
|
|
1439
|
-
const typeToken = this.advance(); // Consume type name
|
|
1440
|
-
const targetType = typeToken.lexeme;
|
|
1441
|
-
const endToken = this.consume(TokenType.RPAREN, "Expected ')' after CAST expression type.");
|
|
1442
|
-
return { type: 'cast', expr, targetType, loc: _createLoc(castToken, endToken) };
|
|
1443
|
-
}
|
|
1444
|
-
|
|
1445
|
-
// EXISTS expression: EXISTS(SELECT ...)
|
|
1446
|
-
if (this.match(TokenType.EXISTS)) {
|
|
1447
|
-
const existsToken = this.previous();
|
|
1448
|
-
this.consume(TokenType.LPAREN, "Expected '(' after EXISTS.");
|
|
1449
|
-
const selectToken = this.consume(TokenType.SELECT, "Expected 'SELECT' in EXISTS subquery.");
|
|
1450
|
-
const subquery = this.selectStatement(selectToken);
|
|
1451
|
-
const endToken = this.consume(TokenType.RPAREN, "Expected ')' after EXISTS subquery.");
|
|
1452
|
-
return {
|
|
1453
|
-
type: 'exists',
|
|
1454
|
-
subquery,
|
|
1455
|
-
loc: _createLoc(existsToken, endToken)
|
|
1456
|
-
};
|
|
1457
|
-
}
|
|
1458
|
-
|
|
1459
|
-
// Literals
|
|
1460
|
-
if (this.match(TokenType.INTEGER, TokenType.FLOAT, TokenType.STRING, TokenType.NULL, TokenType.TRUE, TokenType.FALSE, TokenType.BLOB)) {
|
|
1461
|
-
const token = this.previous();
|
|
1462
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1463
|
-
let value: any;
|
|
1464
|
-
let lexeme: string | undefined = undefined;
|
|
1465
|
-
|
|
1466
|
-
if (token.type === TokenType.NULL) {
|
|
1467
|
-
value = null;
|
|
1468
|
-
lexeme = token.lexeme; // Store original case (NULL vs null)
|
|
1469
|
-
} else if (token.type === TokenType.TRUE) {
|
|
1470
|
-
value = true;
|
|
1471
|
-
lexeme = token.lexeme; // Store original case (TRUE vs true)
|
|
1472
|
-
} else if (token.type === TokenType.FALSE) {
|
|
1473
|
-
value = false;
|
|
1474
|
-
lexeme = token.lexeme; // Store original case (FALSE vs false)
|
|
1475
|
-
} else if (token.type === TokenType.FLOAT) {
|
|
1476
|
-
// For FLOAT, parse the literal (which is the original string)
|
|
1477
|
-
value = parseFloat(token.literal as string);
|
|
1478
|
-
lexeme = token.literal as string; // Store original string as lexeme
|
|
1479
|
-
} else if (token.type === TokenType.INTEGER) {
|
|
1480
|
-
value = token.literal; // Already number or BigInt
|
|
1481
|
-
if (token.lexeme !== String(value)) { // Store lexeme only if different
|
|
1482
|
-
lexeme = token.lexeme;
|
|
1483
|
-
}
|
|
1484
|
-
} else {
|
|
1485
|
-
value = token.literal; // STRING, BLOB
|
|
1486
|
-
}
|
|
1487
|
-
|
|
1488
|
-
const node: AST.LiteralExpr = { type: 'literal', value, loc: _createLoc(startToken, token) };
|
|
1489
|
-
if (lexeme !== undefined) {
|
|
1490
|
-
node.lexeme = lexeme;
|
|
1491
|
-
}
|
|
1492
|
-
return node;
|
|
1493
|
-
}
|
|
1494
|
-
|
|
1495
|
-
// Parameter expressions (?, :name, $name)
|
|
1496
|
-
if (this.match(TokenType.QUESTION)) {
|
|
1497
|
-
const token = this.previous();
|
|
1498
|
-
return { type: 'parameter', index: this.parameterPosition++, loc: _createLoc(startToken, token) };
|
|
1499
|
-
}
|
|
1500
|
-
|
|
1501
|
-
if (this.match(TokenType.COLON, TokenType.DOLLAR)) {
|
|
1502
|
-
// Named parameter (can be identifier like :name or integer like :1)
|
|
1503
|
-
if (!this.check(TokenType.IDENTIFIER) && !this.check(TokenType.INTEGER)) {
|
|
1504
|
-
throw this.error(this.peek(), "Expected identifier or number after parameter prefix.");
|
|
1505
|
-
}
|
|
1506
|
-
const nameToken = this.advance();
|
|
1507
|
-
return { type: 'parameter', name: nameToken.lexeme, loc: _createLoc(startToken, nameToken) };
|
|
1508
|
-
}
|
|
1509
|
-
|
|
1510
|
-
// Function call (with optional window function support)
|
|
1511
|
-
if (this.checkIdentifierLike(['key', 'action', 'set', 'default', 'check', 'unique', 'references', 'on', 'cascade', 'restrict', 'like', 'replace']) && this.checkNext(1, TokenType.LPAREN)) {
|
|
1512
|
-
const name = this.consumeIdentifier(['key', 'action', 'set', 'default', 'check', 'unique', 'references', 'on', 'cascade', 'restrict', 'like', 'replace'], "Expected function name.");
|
|
1513
|
-
|
|
1514
|
-
this.consume(TokenType.LPAREN, "Expected '(' after function name.");
|
|
1515
|
-
|
|
1516
|
-
const args: AST.Expression[] = [];
|
|
1517
|
-
let distinct = false;
|
|
1518
|
-
if (!this.check(TokenType.RPAREN)) {
|
|
1519
|
-
// Handle DISTINCT inside function calls like COUNT(DISTINCT col)
|
|
1520
|
-
distinct = this.matchKeyword('DISTINCT');
|
|
1521
|
-
// Handle * argument AFTER checking for distinct
|
|
1522
|
-
if (this.match(TokenType.ASTERISK)) {
|
|
1523
|
-
// Do not add '*' as an argument to the list for aggregates like COUNT(*)
|
|
1524
|
-
if (args.length > 0 || distinct) {
|
|
1525
|
-
// '*' is only valid as the *only* argument, potentially after DISTINCT
|
|
1526
|
-
// e.g. COUNT(*), COUNT(DISTINCT *) - though DISTINCT * might not be standard SQL?
|
|
1527
|
-
// For now, disallow '*' if other args exist.
|
|
1528
|
-
throw this.error(this.previous(), "'*' cannot be used with other arguments in function call.");
|
|
1529
|
-
}
|
|
1530
|
-
// If we parsed '*', the args list remains empty.
|
|
1531
|
-
} else {
|
|
1532
|
-
// Parse regular arguments if '*' wasn't found
|
|
1533
|
-
do {
|
|
1534
|
-
args.push(this.expression());
|
|
1535
|
-
} while (this.match(TokenType.COMMA));
|
|
1536
|
-
}
|
|
1537
|
-
}
|
|
1538
|
-
|
|
1539
|
-
const endToken = this.consume(TokenType.RPAREN, "Expected ')' after function arguments.");
|
|
1540
|
-
|
|
1541
|
-
const funcExpr: AST.FunctionExpr = {
|
|
1542
|
-
type: 'function',
|
|
1543
|
-
name,
|
|
1544
|
-
args,
|
|
1545
|
-
loc: _createLoc(startToken, endToken)
|
|
1546
|
-
};
|
|
1547
|
-
|
|
1548
|
-
// Add distinct field if it was parsed
|
|
1549
|
-
if (distinct) {
|
|
1550
|
-
funcExpr.distinct = true;
|
|
1551
|
-
}
|
|
1552
|
-
|
|
1553
|
-
// Check for OVER clause (window function)
|
|
1554
|
-
if (this.matchKeyword('OVER')) {
|
|
1555
|
-
const window = this.parseWindowSpecification();
|
|
1556
|
-
const overEndToken = this.previous();
|
|
1557
|
-
return {
|
|
1558
|
-
type: 'windowFunction',
|
|
1559
|
-
function: funcExpr,
|
|
1560
|
-
window,
|
|
1561
|
-
loc: _createLoc(startToken, overEndToken)
|
|
1562
|
-
};
|
|
1563
|
-
}
|
|
1564
|
-
|
|
1565
|
-
return funcExpr;
|
|
1566
|
-
}
|
|
1567
|
-
|
|
1568
|
-
// Column/identifier expressions
|
|
1569
|
-
const contextualKeywords = ['key', 'action', 'set', 'default', 'check', 'unique', 'references', 'on', 'cascade', 'restrict', 'like'];
|
|
1570
|
-
if (this.checkIdentifierLike(contextualKeywords)) {
|
|
1571
|
-
// Schema.table.column
|
|
1572
|
-
if (this.checkNext(1, TokenType.DOT) && this.checkIdentifierLikeAt(2, contextualKeywords) &&
|
|
1573
|
-
this.checkNext(3, TokenType.DOT) && this.checkIdentifierLikeAt(4, contextualKeywords)) {
|
|
1574
|
-
const schema = this.consumeIdentifier(contextualKeywords, "Expected schema name.");
|
|
1575
|
-
this.advance(); // Consume DOT
|
|
1576
|
-
const table = this.consumeIdentifier(contextualKeywords, "Expected table name.");
|
|
1577
|
-
this.advance(); // Consume DOT
|
|
1578
|
-
const name = this.consumeIdentifier(contextualKeywords, "Expected column name.");
|
|
1579
|
-
const nameToken = this.previous();
|
|
1580
|
-
|
|
1581
|
-
return {
|
|
1582
|
-
type: 'column',
|
|
1583
|
-
name,
|
|
1584
|
-
table,
|
|
1585
|
-
schema,
|
|
1586
|
-
loc: _createLoc(startToken, nameToken),
|
|
1587
|
-
};
|
|
1588
|
-
}
|
|
1589
|
-
// table.column
|
|
1590
|
-
else if (this.checkNext(1, TokenType.DOT) && this.checkIdentifierLikeAt(2, contextualKeywords)) {
|
|
1591
|
-
const table = this.consumeIdentifier(contextualKeywords, "Expected table name.");
|
|
1592
|
-
this.advance(); // Consume DOT
|
|
1593
|
-
const name = this.consumeIdentifier(contextualKeywords, "Expected column name.");
|
|
1594
|
-
const nameToken = this.previous();
|
|
1595
|
-
|
|
1596
|
-
return {
|
|
1597
|
-
type: 'column',
|
|
1598
|
-
name,
|
|
1599
|
-
table,
|
|
1600
|
-
loc: _createLoc(startToken, nameToken),
|
|
1601
|
-
};
|
|
1602
|
-
}
|
|
1603
|
-
// just column
|
|
1604
|
-
else {
|
|
1605
|
-
const name = this.consumeIdentifier(contextualKeywords, "Expected column name.");
|
|
1606
|
-
const nameToken = this.previous();
|
|
1607
|
-
|
|
1608
|
-
return {
|
|
1609
|
-
type: 'column',
|
|
1610
|
-
name,
|
|
1611
|
-
loc: _createLoc(startToken, nameToken),
|
|
1612
|
-
};
|
|
1613
|
-
}
|
|
1614
|
-
}
|
|
1615
|
-
|
|
1616
|
-
// Parenthesized expression or scalar subquery
|
|
1617
|
-
if (this.match(TokenType.LPAREN)) {
|
|
1618
|
-
// Look ahead to see if this is a scalar subquery (SELECT ...)
|
|
1619
|
-
if (this.check(TokenType.SELECT)) {
|
|
1620
|
-
const selectToken = this.consume(TokenType.SELECT, "Expected 'SELECT' in subquery.");
|
|
1621
|
-
const subquery = this.selectStatement(selectToken);
|
|
1622
|
-
this.consume(TokenType.RPAREN, "Expected ')' after subquery.");
|
|
1623
|
-
return {
|
|
1624
|
-
type: 'subquery',
|
|
1625
|
-
query: subquery,
|
|
1626
|
-
loc: _createLoc(startToken, this.previous())
|
|
1627
|
-
};
|
|
1628
|
-
} else {
|
|
1629
|
-
// Regular parenthesized expression
|
|
1630
|
-
const expr = this.expression();
|
|
1631
|
-
this.consume(TokenType.RPAREN, "Expected ')' after expression.");
|
|
1632
|
-
return expr;
|
|
1633
|
-
}
|
|
1634
|
-
}
|
|
1635
|
-
|
|
1636
|
-
throw this.error(this.peek(), "Expected expression.");
|
|
1637
|
-
}
|
|
1638
|
-
|
|
1639
|
-
/**
|
|
1640
|
-
* Parses a window specification: (PARTITION BY ... ORDER BY ... [frame])
|
|
1641
|
-
*/
|
|
1642
|
-
private parseWindowSpecification(): AST.WindowDefinition {
|
|
1643
|
-
if (this.match(TokenType.LPAREN)) {
|
|
1644
|
-
let partitionBy: AST.Expression[] | undefined;
|
|
1645
|
-
let orderBy: AST.OrderByClause[] | undefined;
|
|
1646
|
-
let frame: AST.WindowFrame | undefined;
|
|
1647
|
-
|
|
1648
|
-
if (this.matchKeyword('PARTITION')) {
|
|
1649
|
-
this.consumeKeyword('BY', "Expected 'BY' after 'PARTITION'.");
|
|
1650
|
-
partitionBy = [];
|
|
1651
|
-
do {
|
|
1652
|
-
partitionBy.push(this.expression());
|
|
1653
|
-
} while (this.match(TokenType.COMMA));
|
|
1654
|
-
}
|
|
1655
|
-
|
|
1656
|
-
if (this.matchKeyword('ORDER')) {
|
|
1657
|
-
this.consumeKeyword('BY', "Expected 'BY' after 'ORDER'.");
|
|
1658
|
-
orderBy = [];
|
|
1659
|
-
do {
|
|
1660
|
-
const expr = this.expression();
|
|
1661
|
-
const direction = this.match(TokenType.DESC) ? 'desc' : (this.match(TokenType.ASC) ? 'asc' : 'asc');
|
|
1662
|
-
|
|
1663
|
-
// Handle NULLS FIRST/LAST
|
|
1664
|
-
let nulls: 'first' | 'last' | undefined;
|
|
1665
|
-
if (this.matchKeyword('NULLS')) {
|
|
1666
|
-
if (this.matchKeyword('FIRST')) {
|
|
1667
|
-
nulls = 'first';
|
|
1668
|
-
} else if (this.matchKeyword('LAST')) {
|
|
1669
|
-
nulls = 'last';
|
|
1670
|
-
} else {
|
|
1671
|
-
throw this.error(this.peek(), "Expected 'FIRST' or 'LAST' after 'NULLS'.");
|
|
1672
|
-
}
|
|
1673
|
-
}
|
|
1674
|
-
|
|
1675
|
-
const orderClause: AST.OrderByClause = { expr, direction };
|
|
1676
|
-
if (nulls) {
|
|
1677
|
-
orderClause.nulls = nulls;
|
|
1678
|
-
}
|
|
1679
|
-
orderBy.push(orderClause);
|
|
1680
|
-
} while (this.match(TokenType.COMMA));
|
|
1681
|
-
}
|
|
1682
|
-
|
|
1683
|
-
// Frame clause (ROWS|RANGE ...)
|
|
1684
|
-
if (this.matchKeyword('ROWS') || this.matchKeyword('RANGE')) {
|
|
1685
|
-
const frameType = this.previous().lexeme.toLowerCase() as 'rows' | 'range';
|
|
1686
|
-
|
|
1687
|
-
// Handle both BETWEEN...AND and single bound syntax
|
|
1688
|
-
if (this.matchKeyword('BETWEEN')) {
|
|
1689
|
-
// ROWS BETWEEN start_bound AND end_bound
|
|
1690
|
-
const start = this.parseWindowFrameBound();
|
|
1691
|
-
this.consumeKeyword('AND', "Expected 'AND' after frame start bound.");
|
|
1692
|
-
const end = this.parseWindowFrameBound();
|
|
1693
|
-
frame = { type: frameType, start, end };
|
|
1694
|
-
} else {
|
|
1695
|
-
// ROWS start_bound (shorthand for ROWS BETWEEN start_bound AND CURRENT ROW)
|
|
1696
|
-
const start = this.parseWindowFrameBound();
|
|
1697
|
-
frame = { type: frameType, start, end: null };
|
|
1698
|
-
}
|
|
1699
|
-
}
|
|
1700
|
-
|
|
1701
|
-
this.consume(TokenType.RPAREN, "Expected ')' after window specification.");
|
|
1702
|
-
return { type: 'windowDefinition', partitionBy, orderBy, frame };
|
|
1703
|
-
} else {
|
|
1704
|
-
// Window name (not implemented)
|
|
1705
|
-
throw this.error(this.peek(), 'Window name references are not yet supported. Use explicit window specs.');
|
|
1706
|
-
}
|
|
1707
|
-
}
|
|
1708
|
-
|
|
1709
|
-
/**
|
|
1710
|
-
* Parses a window frame bound (UNBOUNDED PRECEDING, CURRENT ROW, n PRECEDING/FOLLOWING)
|
|
1711
|
-
*/
|
|
1712
|
-
private parseWindowFrameBound(): AST.WindowFrameBound {
|
|
1713
|
-
if (this.matchKeyword('UNBOUNDED')) {
|
|
1714
|
-
if (this.matchKeyword('PRECEDING')) {
|
|
1715
|
-
return { type: 'unboundedPreceding' };
|
|
1716
|
-
} else if (this.matchKeyword('FOLLOWING')) {
|
|
1717
|
-
return { type: 'unboundedFollowing' };
|
|
1718
|
-
} else {
|
|
1719
|
-
throw this.error(this.peek(), "Expected PRECEDING or FOLLOWING after UNBOUNDED.");
|
|
1720
|
-
}
|
|
1721
|
-
} else if (this.matchKeyword('CURRENT')) {
|
|
1722
|
-
this.consumeKeyword('ROW', "Expected 'ROW' after 'CURRENT'.");
|
|
1723
|
-
return { type: 'currentRow' };
|
|
1724
|
-
} else {
|
|
1725
|
-
const value = this.expression();
|
|
1726
|
-
if (this.matchKeyword('PRECEDING')) {
|
|
1727
|
-
return { type: 'preceding', value };
|
|
1728
|
-
} else if (this.matchKeyword('FOLLOWING')) {
|
|
1729
|
-
return { type: 'following', value };
|
|
1730
|
-
} else {
|
|
1731
|
-
throw this.error(this.peek(), "Expected PRECEDING or FOLLOWING after frame value.");
|
|
1732
|
-
}
|
|
1733
|
-
}
|
|
1734
|
-
}
|
|
1735
|
-
|
|
1736
|
-
// Helper methods for token management
|
|
1737
|
-
|
|
1738
|
-
private match(...types: TokenType[]): boolean {
|
|
1739
|
-
for (const type of types) {
|
|
1740
|
-
if (this.check(type)) {
|
|
1741
|
-
this.advance();
|
|
1742
|
-
return true;
|
|
1743
|
-
}
|
|
1744
|
-
}
|
|
1745
|
-
return false;
|
|
1746
|
-
}
|
|
1747
|
-
|
|
1748
|
-
private consume(type: TokenType, message: string): Token {
|
|
1749
|
-
if (this.check(type)) {
|
|
1750
|
-
return this.advance();
|
|
1751
|
-
}
|
|
1752
|
-
|
|
1753
|
-
// If a ')' was expected, point back to the matching '('
|
|
1754
|
-
if (type === TokenType.RPAREN && this.parenStack.length > 0) {
|
|
1755
|
-
const openToken = this.parenStack[this.parenStack.length - 1];
|
|
1756
|
-
const got = this.peek();
|
|
1757
|
-
quereusError(
|
|
1758
|
-
`${message} Unterminated '(' opened at line ${openToken.startLine}, column ${openToken.startColumn}. Got '${got.lexeme}'.`,
|
|
1759
|
-
StatusCode.ERROR,
|
|
1760
|
-
undefined,
|
|
1761
|
-
{
|
|
1762
|
-
loc: {
|
|
1763
|
-
start: { line: openToken.startLine, column: openToken.startColumn },
|
|
1764
|
-
end: { line: this.peek().endLine, column: this.peek().endColumn },
|
|
1765
|
-
},
|
|
1766
|
-
}
|
|
1767
|
-
);
|
|
1768
|
-
}
|
|
1769
|
-
|
|
1770
|
-
const got = this.peek();
|
|
1771
|
-
this.error(got, `${message} Got '${got.lexeme}'.`);
|
|
1772
|
-
}
|
|
1773
|
-
|
|
1774
|
-
private check(type: TokenType): boolean {
|
|
1775
|
-
if (this.isAtEnd()) return false;
|
|
1776
|
-
return this.peek().type === type;
|
|
1777
|
-
}
|
|
1778
|
-
|
|
1779
|
-
private checkNext(n: number, type: TokenType): boolean {
|
|
1780
|
-
if (this.current + n >= this.tokens.length) return false;
|
|
1781
|
-
return this.tokens[this.current + n].type === type;
|
|
1782
|
-
}
|
|
1783
|
-
|
|
1784
|
-
private advance(): Token {
|
|
1785
|
-
if (!this.isAtEnd()) this.current++;
|
|
1786
|
-
const tok = this.previous();
|
|
1787
|
-
// Maintain parenthesis balance for precise diagnostics
|
|
1788
|
-
if (tok.type === TokenType.LPAREN) {
|
|
1789
|
-
this.parenStack.push(tok);
|
|
1790
|
-
} else if (tok.type === TokenType.RPAREN) {
|
|
1791
|
-
if (this.parenStack.length === 0) {
|
|
1792
|
-
quereusError(
|
|
1793
|
-
`Unmatched ')' at line ${tok.startLine}, column ${tok.startColumn}.`,
|
|
1794
|
-
StatusCode.ERROR,
|
|
1795
|
-
undefined,
|
|
1796
|
-
{ loc: { start: { line: tok.startLine, column: tok.startColumn }, end: { line: tok.endLine, column: tok.endColumn } } }
|
|
1797
|
-
);
|
|
1798
|
-
} else {
|
|
1799
|
-
this.parenStack.pop();
|
|
1800
|
-
}
|
|
1801
|
-
}
|
|
1802
|
-
return tok;
|
|
1803
|
-
}
|
|
1804
|
-
|
|
1805
|
-
private isAtEnd(): boolean {
|
|
1806
|
-
return this.peek().type === TokenType.EOF;
|
|
1807
|
-
}
|
|
1808
|
-
|
|
1809
|
-
private peek(): Token {
|
|
1810
|
-
return this.tokens[this.current];
|
|
1811
|
-
}
|
|
1812
|
-
|
|
1813
|
-
private previous(): Token {
|
|
1814
|
-
return this.tokens[this.current - 1];
|
|
1815
|
-
}
|
|
1816
|
-
|
|
1817
|
-
private error(token: Token, message: string): never {
|
|
1818
|
-
// If we see common starter tokens for a different clause where a separator/comma or keyword was expected,
|
|
1819
|
-
// enhance the message to hint at likely fixes instead of generic parenthesis errors.
|
|
1820
|
-
const nextLex = token.lexeme?.toUpperCase?.() || token.lexeme;
|
|
1821
|
-
const hintParts: string[] = [];
|
|
1822
|
-
if (this.peekKeyword('CONSTRAINT') || this.peekKeyword('PRIMARY') || this.peekKeyword('UNIQUE') || this.peekKeyword('CHECK') || this.peekKeyword('FOREIGN')) {
|
|
1823
|
-
hintParts.push("If you're in CREATE TABLE, you might be missing a comma between elements.");
|
|
1824
|
-
}
|
|
1825
|
-
if (nextLex === 'ON' && !this.peekKeyword('JOIN')) {
|
|
1826
|
-
hintParts.push("'ON' must follow a JOIN. Use WHERE for filters in subqueries.");
|
|
1827
|
-
}
|
|
1828
|
-
const fullMessage = hintParts.length > 0 ? `${message} ${hintParts.join(' ')}` : message;
|
|
1829
|
-
quereusError(
|
|
1830
|
-
fullMessage,
|
|
1831
|
-
StatusCode.ERROR,
|
|
1832
|
-
undefined,
|
|
1833
|
-
{
|
|
1834
|
-
loc: {
|
|
1835
|
-
start: {
|
|
1836
|
-
line: token.startLine,
|
|
1837
|
-
column: token.startColumn,
|
|
1838
|
-
},
|
|
1839
|
-
end: {
|
|
1840
|
-
line: token.endLine,
|
|
1841
|
-
column: token.endColumn,
|
|
1842
|
-
}
|
|
1843
|
-
}
|
|
1844
|
-
}
|
|
1845
|
-
);
|
|
1846
|
-
}
|
|
1847
|
-
|
|
1848
|
-
private isJoinToken(): boolean {
|
|
1849
|
-
return this.check(TokenType.JOIN) ||
|
|
1850
|
-
this.check(TokenType.INNER) ||
|
|
1851
|
-
this.check(TokenType.LEFT) ||
|
|
1852
|
-
this.check(TokenType.RIGHT) ||
|
|
1853
|
-
this.check(TokenType.FULL) ||
|
|
1854
|
-
this.check(TokenType.CROSS);
|
|
1855
|
-
}
|
|
1856
|
-
|
|
1857
|
-
private isEndOfClause(): boolean {
|
|
1858
|
-
const token = this.peek().type;
|
|
1859
|
-
return token === TokenType.FROM ||
|
|
1860
|
-
token === TokenType.WHERE ||
|
|
1861
|
-
token === TokenType.GROUP ||
|
|
1862
|
-
token === TokenType.HAVING ||
|
|
1863
|
-
token === TokenType.ORDER ||
|
|
1864
|
-
token === TokenType.LIMIT ||
|
|
1865
|
-
token === TokenType.UNION || token === TokenType.DIFF || token === TokenType.INTERSECT || token === TokenType.EXCEPT ||
|
|
1866
|
-
token === TokenType.SEMICOLON ||
|
|
1867
|
-
token === TokenType.EOF;
|
|
1868
|
-
}
|
|
1869
|
-
|
|
1870
|
-
// --- Statement Parsing Stubs ---
|
|
1871
|
-
|
|
1872
|
-
/** @internal */
|
|
1873
|
-
private updateStatement(startToken: Token, _withClause?: AST.WithClause): AST.UpdateStmt {
|
|
1874
|
-
const table = this.tableIdentifier();
|
|
1875
|
-
|
|
1876
|
-
// Parse mutation context assignments if present
|
|
1877
|
-
let contextValues: AST.ContextAssignment[] | undefined;
|
|
1878
|
-
if (this.matchKeyword('WITH')) {
|
|
1879
|
-
if (this.matchKeyword('CONTEXT')) {
|
|
1880
|
-
contextValues = this.parseContextAssignments();
|
|
1881
|
-
} else {
|
|
1882
|
-
// Not a WITH CONTEXT clause, backtrack
|
|
1883
|
-
this.current--;
|
|
1884
|
-
}
|
|
1885
|
-
}
|
|
1886
|
-
|
|
1887
|
-
this.consume(TokenType.SET, "Expected 'SET' after table name in UPDATE.");
|
|
1888
|
-
const assignments: { column: string; value: AST.Expression }[] = [];
|
|
1889
|
-
do {
|
|
1890
|
-
const column = this.consumeIdentifier(['key', 'action', 'set', 'default', 'check', 'unique', 'references', 'on', 'cascade', 'restrict', 'like'], "Expected column name in SET clause.");
|
|
1891
|
-
this.consume(TokenType.EQUAL, "Expected '=' after column name in SET clause.");
|
|
1892
|
-
const value = this.expression();
|
|
1893
|
-
assignments.push({ column, value });
|
|
1894
|
-
} while (this.match(TokenType.COMMA));
|
|
1895
|
-
let where: AST.Expression | undefined;
|
|
1896
|
-
if (this.match(TokenType.WHERE)) {
|
|
1897
|
-
where = this.expression();
|
|
1898
|
-
}
|
|
1899
|
-
// Parse RETURNING clause if present
|
|
1900
|
-
let returning: AST.ResultColumn[] | undefined;
|
|
1901
|
-
if (this.matchKeyword('RETURNING')) {
|
|
1902
|
-
returning = this.columnList();
|
|
1903
|
-
}
|
|
1904
|
-
const endToken = this.previous();
|
|
1905
|
-
return { type: 'update', table, assignments, where, returning, contextValues, loc: _createLoc(startToken, endToken) };
|
|
1906
|
-
}
|
|
1907
|
-
|
|
1908
|
-
/** @internal */
|
|
1909
|
-
private deleteStatement(startToken: Token, _withClause?: AST.WithClause): AST.DeleteStmt {
|
|
1910
|
-
this.matchKeyword('FROM');
|
|
1911
|
-
const table = this.tableIdentifier();
|
|
1912
|
-
|
|
1913
|
-
// Parse mutation context assignments if present
|
|
1914
|
-
let contextValues: AST.ContextAssignment[] | undefined;
|
|
1915
|
-
if (this.matchKeyword('WITH')) {
|
|
1916
|
-
if (this.matchKeyword('CONTEXT')) {
|
|
1917
|
-
contextValues = this.parseContextAssignments();
|
|
1918
|
-
} else {
|
|
1919
|
-
// Not a WITH CONTEXT clause, backtrack
|
|
1920
|
-
this.current--;
|
|
1921
|
-
}
|
|
1922
|
-
}
|
|
1923
|
-
|
|
1924
|
-
let where: AST.Expression | undefined;
|
|
1925
|
-
if (this.match(TokenType.WHERE)) {
|
|
1926
|
-
where = this.expression();
|
|
1927
|
-
}
|
|
1928
|
-
|
|
1929
|
-
// Parse RETURNING clause if present
|
|
1930
|
-
let returning: AST.ResultColumn[] | undefined;
|
|
1931
|
-
if (this.matchKeyword('RETURNING')) {
|
|
1932
|
-
returning = this.columnList();
|
|
1933
|
-
}
|
|
1934
|
-
|
|
1935
|
-
const endToken = this.previous();
|
|
1936
|
-
return { type: 'delete', table, where, returning, contextValues, loc: _createLoc(startToken, endToken) };
|
|
1937
|
-
}
|
|
1938
|
-
|
|
1939
|
-
/** @internal */
|
|
1940
|
-
private valuesStatement(startToken: Token): AST.ValuesStmt {
|
|
1941
|
-
const values: AST.Expression[][] = [];
|
|
1942
|
-
|
|
1943
|
-
do {
|
|
1944
|
-
this.consume(TokenType.LPAREN, "Expected '(' before values.");
|
|
1945
|
-
const valueList: AST.Expression[] = [];
|
|
1946
|
-
|
|
1947
|
-
if (!this.check(TokenType.RPAREN)) { // Check for empty value list
|
|
1948
|
-
do {
|
|
1949
|
-
valueList.push(this.expression());
|
|
1950
|
-
} while (this.match(TokenType.COMMA));
|
|
1951
|
-
}
|
|
1952
|
-
|
|
1953
|
-
this.consume(TokenType.RPAREN, "Expected ')' after values.");
|
|
1954
|
-
values.push(valueList);
|
|
1955
|
-
} while (this.match(TokenType.COMMA));
|
|
1956
|
-
|
|
1957
|
-
const endToken = this.previous();
|
|
1958
|
-
return { type: 'values', values, loc: _createLoc(startToken, endToken) };
|
|
1959
|
-
}
|
|
1960
|
-
|
|
1961
|
-
/** @internal */
|
|
1962
|
-
private createStatement(startToken: Token, withClause?: AST.WithClause): AST.CreateTableStmt | AST.CreateIndexStmt | AST.CreateViewStmt | AST.CreateAssertionStmt {
|
|
1963
|
-
if (this.peekKeyword('TABLE')) {
|
|
1964
|
-
this.consumeKeyword('TABLE', "Expected 'TABLE' after CREATE.");
|
|
1965
|
-
return this.createTableStatement(startToken, withClause);
|
|
1966
|
-
} else if (this.peekKeyword('INDEX')) {
|
|
1967
|
-
this.consumeKeyword('INDEX', "Expected 'INDEX' after CREATE.");
|
|
1968
|
-
return this.createIndexStatement(startToken, false, withClause);
|
|
1969
|
-
} else if (this.peekKeyword('VIEW')) {
|
|
1970
|
-
this.consumeKeyword('VIEW', "Expected 'VIEW' after CREATE.");
|
|
1971
|
-
return this.createViewStatement(startToken, withClause);
|
|
1972
|
-
} else if (this.peekKeyword('ASSERTION')) {
|
|
1973
|
-
this.consumeKeyword('ASSERTION', "Expected 'ASSERTION' after CREATE.");
|
|
1974
|
-
return this.createAssertionStatement(startToken, withClause);
|
|
1975
|
-
} else if (this.peekKeyword('UNIQUE')) {
|
|
1976
|
-
this.consumeKeyword('UNIQUE', "Expected 'UNIQUE' after CREATE.");
|
|
1977
|
-
this.consumeKeyword('INDEX', "Expected 'INDEX' after CREATE UNIQUE.");
|
|
1978
|
-
return this.createIndexStatement(startToken, true, withClause);
|
|
1979
|
-
}
|
|
1980
|
-
throw this.error(this.peek(), "Expected TABLE, [UNIQUE] INDEX, VIEW, ASSERTION, or VIRTUAL after CREATE.");
|
|
1981
|
-
}
|
|
1982
|
-
|
|
1983
|
-
/**
|
|
1984
|
-
* Parse CREATE TABLE statement
|
|
1985
|
-
* @returns AST for CREATE TABLE
|
|
1986
|
-
*/
|
|
1987
|
-
private createTableStatement(startToken: Token, _withClause?: AST.WithClause): AST.CreateTableStmt {
|
|
1988
|
-
let isTemporary = false;
|
|
1989
|
-
if (this.peekKeyword('TEMP') || this.peekKeyword('TEMPORARY')) {
|
|
1990
|
-
isTemporary = true;
|
|
1991
|
-
this.advance();
|
|
1992
|
-
}
|
|
1993
|
-
|
|
1994
|
-
let ifNotExists = false;
|
|
1995
|
-
if (this.matchKeyword('IF')) {
|
|
1996
|
-
this.consumeKeyword('NOT', "Expected 'NOT' after 'IF'.");
|
|
1997
|
-
this.consumeKeyword('EXISTS', "Expected 'EXISTS' after 'IF NOT'.");
|
|
1998
|
-
ifNotExists = true;
|
|
1999
|
-
}
|
|
2000
|
-
|
|
2001
|
-
const table = this.tableIdentifier();
|
|
2002
|
-
|
|
2003
|
-
const columns: AST.ColumnDef[] = [];
|
|
2004
|
-
const constraints: AST.TableConstraint[] = [];
|
|
2005
|
-
|
|
2006
|
-
if (this.check(TokenType.LPAREN)) {
|
|
2007
|
-
this.consume(TokenType.LPAREN, "Expected '(' to start table definition.");
|
|
2008
|
-
do {
|
|
2009
|
-
if (this.peekKeyword('PRIMARY') || this.peekKeyword('UNIQUE') || this.peekKeyword('CHECK') || this.peekKeyword('FOREIGN') || this.peekKeyword('CONSTRAINT')) {
|
|
2010
|
-
constraints.push(this.tableConstraint());
|
|
2011
|
-
} else {
|
|
2012
|
-
columns.push(this.columnDefinition());
|
|
2013
|
-
}
|
|
2014
|
-
// Allow trailing comma before ')'
|
|
2015
|
-
} while (this.match(TokenType.COMMA) && !this.check(TokenType.RPAREN));
|
|
2016
|
-
|
|
2017
|
-
// If we didn't see a comma and the next token looks like the start of another
|
|
2018
|
-
// column or table constraint, provide a clearer error about a missing comma.
|
|
2019
|
-
if (!this.check(TokenType.RPAREN)) {
|
|
2020
|
-
const contextualKeywords = ['key', 'action', 'set', 'default', 'check', 'unique', 'references', 'on', 'cascade', 'restrict', 'like'];
|
|
2021
|
-
const nextLooksLikeAnotherItem = this.peekKeyword('PRIMARY') || this.peekKeyword('UNIQUE') || this.peekKeyword('CHECK') || this.peekKeyword('FOREIGN') || this.peekKeyword('CONSTRAINT') || this.checkIdentifierLike(contextualKeywords);
|
|
2022
|
-
if (nextLooksLikeAnotherItem) {
|
|
2023
|
-
const next = this.peek();
|
|
2024
|
-
throw this.error(next, `Expected ',' between table elements. Did you forget a comma before '${next.lexeme}'?`);
|
|
2025
|
-
}
|
|
2026
|
-
}
|
|
2027
|
-
|
|
2028
|
-
this.consume(TokenType.RPAREN, "Expected ')' after table definition.");
|
|
2029
|
-
|
|
2030
|
-
} else if (this.matchKeyword('AS')) {
|
|
2031
|
-
const token = this.previous();
|
|
2032
|
-
quereusError(
|
|
2033
|
-
'CREATE TABLE AS SELECT is not supported.',
|
|
2034
|
-
StatusCode.UNSUPPORTED,
|
|
2035
|
-
undefined,
|
|
2036
|
-
{ loc: { start: { line: token.startLine, column: token.startColumn } } }
|
|
2037
|
-
);
|
|
2038
|
-
} else {
|
|
2039
|
-
throw this.error(this.peek(), "Expected '(' or 'AS' after table name.");
|
|
2040
|
-
}
|
|
2041
|
-
|
|
2042
|
-
let moduleName: string | undefined;
|
|
2043
|
-
const moduleArgs: Record<string, SqlValue> = {};
|
|
2044
|
-
if (this.matchKeyword('USING')) {
|
|
2045
|
-
moduleName = this.consumeIdentifier("Expected module name after 'USING'.");
|
|
2046
|
-
if (this.match(TokenType.LPAREN)) {
|
|
2047
|
-
let positionalIndex = 0;
|
|
2048
|
-
if (!this.check(TokenType.RPAREN)) {
|
|
2049
|
-
do {
|
|
2050
|
-
// Check if this is a positional argument (string/number literal) or named argument (identifier=value)
|
|
2051
|
-
if (this.check(TokenType.STRING) || this.check(TokenType.INTEGER) || this.check(TokenType.FLOAT)) {
|
|
2052
|
-
// Positional argument
|
|
2053
|
-
const token = this.advance();
|
|
2054
|
-
moduleArgs[String(positionalIndex++)] = token.literal;
|
|
2055
|
-
} else if (this.check(TokenType.IDENTIFIER)) {
|
|
2056
|
-
// Could be named argument or identifier value
|
|
2057
|
-
const nameValue = this.nameValueItem('module argument');
|
|
2058
|
-
moduleArgs[nameValue.name] = nameValue.value && nameValue.value.type === 'literal'
|
|
2059
|
-
? getSyncLiteral(nameValue.value)
|
|
2060
|
-
: (nameValue.value && nameValue.value.type === 'identifier' ? nameValue.value.name : nameValue.name);
|
|
2061
|
-
} else {
|
|
2062
|
-
throw this.error(this.peek(), "Expected module argument (string, number, or name=value pair).");
|
|
2063
|
-
}
|
|
2064
|
-
} while (this.match(TokenType.COMMA));
|
|
2065
|
-
}
|
|
2066
|
-
this.consume(TokenType.RPAREN, "Expected ')' after module arguments.");
|
|
2067
|
-
}
|
|
2068
|
-
}
|
|
2069
|
-
|
|
2070
|
-
// Parse mutation context definitions if present
|
|
2071
|
-
let contextDefinitions: AST.MutationContextVar[] | undefined;
|
|
2072
|
-
if (this.matchKeyword('WITH')) {
|
|
2073
|
-
if (this.matchKeyword('CONTEXT')) {
|
|
2074
|
-
contextDefinitions = this.parseMutationContextDefinitions();
|
|
2075
|
-
} else {
|
|
2076
|
-
// Not a WITH CONTEXT clause, backtrack
|
|
2077
|
-
this.current--;
|
|
2078
|
-
}
|
|
2079
|
-
}
|
|
2080
|
-
|
|
2081
|
-
return {
|
|
2082
|
-
type: 'createTable',
|
|
2083
|
-
table,
|
|
2084
|
-
ifNotExists,
|
|
2085
|
-
columns,
|
|
2086
|
-
constraints,
|
|
2087
|
-
isTemporary,
|
|
2088
|
-
moduleName,
|
|
2089
|
-
moduleArgs,
|
|
2090
|
-
contextDefinitions,
|
|
2091
|
-
loc: _createLoc(startToken, this.previous()),
|
|
2092
|
-
};
|
|
2093
|
-
}
|
|
2094
|
-
|
|
2095
|
-
/**
|
|
2096
|
-
* Parse CREATE INDEX statement
|
|
2097
|
-
* @param isUnique Flag indicating if UNIQUE keyword was already parsed
|
|
2098
|
-
* @returns AST for CREATE INDEX
|
|
2099
|
-
*/
|
|
2100
|
-
private createIndexStatement(startToken: Token, isUnique = false, _withClause?: AST.WithClause): AST.CreateIndexStmt {
|
|
2101
|
-
if (!isUnique && this.peekKeyword('UNIQUE')) {
|
|
2102
|
-
isUnique = true;
|
|
2103
|
-
this.advance();
|
|
2104
|
-
}
|
|
2105
|
-
|
|
2106
|
-
let ifNotExists = false;
|
|
2107
|
-
if (this.matchKeyword('IF')) {
|
|
2108
|
-
this.consumeKeyword('NOT', "Expected 'NOT' after 'IF'.");
|
|
2109
|
-
this.consumeKeyword('EXISTS', "Expected 'EXISTS' after 'IF NOT'.");
|
|
2110
|
-
ifNotExists = true;
|
|
2111
|
-
}
|
|
2112
|
-
|
|
2113
|
-
const index = this.tableIdentifier();
|
|
2114
|
-
|
|
2115
|
-
this.consumeKeyword('ON', "Expected 'ON' after index name.");
|
|
2116
|
-
|
|
2117
|
-
const table = this.tableIdentifier();
|
|
2118
|
-
|
|
2119
|
-
this.consume(TokenType.LPAREN, "Expected '(' before indexed columns.");
|
|
2120
|
-
const columns = this.indexedColumnList();
|
|
2121
|
-
this.consume(TokenType.RPAREN, "Expected ')' after indexed columns.");
|
|
2122
|
-
|
|
2123
|
-
let where: AST.Expression | undefined;
|
|
2124
|
-
if (this.matchKeyword('WHERE')) {
|
|
2125
|
-
where = this.expression();
|
|
2126
|
-
}
|
|
2127
|
-
|
|
2128
|
-
return {
|
|
2129
|
-
type: 'createIndex',
|
|
2130
|
-
index,
|
|
2131
|
-
table,
|
|
2132
|
-
ifNotExists,
|
|
2133
|
-
columns,
|
|
2134
|
-
where,
|
|
2135
|
-
isUnique,
|
|
2136
|
-
loc: _createLoc(startToken, this.previous()),
|
|
2137
|
-
};
|
|
2138
|
-
}
|
|
2139
|
-
|
|
2140
|
-
/**
|
|
2141
|
-
* Parse CREATE VIEW statement
|
|
2142
|
-
* @returns AST for CREATE VIEW
|
|
2143
|
-
*/
|
|
2144
|
-
private createViewStatement(startToken: Token, withClause?: AST.WithClause): AST.CreateViewStmt {
|
|
2145
|
-
let isTemporary = false;
|
|
2146
|
-
if (this.peekKeyword('TEMP') || this.peekKeyword('TEMPORARY')) {
|
|
2147
|
-
isTemporary = true;
|
|
2148
|
-
this.advance();
|
|
2149
|
-
}
|
|
2150
|
-
|
|
2151
|
-
let ifNotExists = false;
|
|
2152
|
-
if (this.matchKeyword('IF')) {
|
|
2153
|
-
this.consumeKeyword('NOT', "Expected 'NOT' after 'IF'.");
|
|
2154
|
-
this.consumeKeyword('EXISTS', "Expected 'EXISTS' after 'IF NOT'.");
|
|
2155
|
-
ifNotExists = true;
|
|
2156
|
-
}
|
|
2157
|
-
|
|
2158
|
-
const view = this.tableIdentifier();
|
|
2159
|
-
|
|
2160
|
-
let columns: string[] | undefined;
|
|
2161
|
-
if (this.check(TokenType.LPAREN)) {
|
|
2162
|
-
this.consume(TokenType.LPAREN, "Expected '(' to start view column list.");
|
|
2163
|
-
columns = [];
|
|
2164
|
-
const contextualKeywords = ['key', 'action', 'set', 'default', 'check', 'unique', 'references', 'on', 'cascade', 'restrict', 'like'];
|
|
2165
|
-
if (!this.check(TokenType.RPAREN)) {
|
|
2166
|
-
do {
|
|
2167
|
-
columns.push(this.consumeIdentifier(contextualKeywords, "Expected column name in view column list."));
|
|
2168
|
-
} while (this.match(TokenType.COMMA) && !this.check(TokenType.RPAREN));
|
|
2169
|
-
}
|
|
2170
|
-
this.consume(TokenType.RPAREN, "Expected ')' after view column list.");
|
|
2171
|
-
}
|
|
2172
|
-
|
|
2173
|
-
this.consumeKeyword('AS', "Expected 'AS' before SELECT statement for CREATE VIEW.");
|
|
2174
|
-
|
|
2175
|
-
const selectStartToken = this.consume(TokenType.SELECT, "Expected 'SELECT' after 'AS' in CREATE VIEW.");
|
|
2176
|
-
const select = this.selectStatement(selectStartToken, withClause);
|
|
2177
|
-
|
|
2178
|
-
return {
|
|
2179
|
-
type: 'createView',
|
|
2180
|
-
view,
|
|
2181
|
-
ifNotExists,
|
|
2182
|
-
columns,
|
|
2183
|
-
select,
|
|
2184
|
-
isTemporary,
|
|
2185
|
-
loc: _createLoc(startToken, this.previous()),
|
|
2186
|
-
};
|
|
2187
|
-
}
|
|
2188
|
-
|
|
2189
|
-
/**
|
|
2190
|
-
* Parse CREATE ASSERTION statement
|
|
2191
|
-
* @returns AST for CREATE ASSERTION
|
|
2192
|
-
*/
|
|
2193
|
-
private createAssertionStatement(startToken: Token, _withClause?: AST.WithClause): AST.CreateAssertionStmt {
|
|
2194
|
-
const name = this.consumeIdentifier("Expected assertion name.");
|
|
2195
|
-
|
|
2196
|
-
this.consumeKeyword('CHECK', "Expected 'CHECK' after assertion name.");
|
|
2197
|
-
this.consume(TokenType.LPAREN, "Expected '(' after CHECK.");
|
|
2198
|
-
|
|
2199
|
-
const check = this.expression();
|
|
2200
|
-
|
|
2201
|
-
this.consume(TokenType.RPAREN, "Expected ')' after CHECK expression.");
|
|
2202
|
-
|
|
2203
|
-
return {
|
|
2204
|
-
type: 'createAssertion',
|
|
2205
|
-
name,
|
|
2206
|
-
check,
|
|
2207
|
-
loc: _createLoc(startToken, this.previous()),
|
|
2208
|
-
};
|
|
2209
|
-
}
|
|
2210
|
-
|
|
2211
|
-
/**
|
|
2212
|
-
* Parse DROP statement
|
|
2213
|
-
* @returns AST for DROP statement
|
|
2214
|
-
*/
|
|
2215
|
-
private dropStatement(startToken: Token, _withClause?: AST.WithClause): AST.DropStmt {
|
|
2216
|
-
let objectType: 'table' | 'view' | 'index' | 'trigger' | 'assertion';
|
|
2217
|
-
|
|
2218
|
-
if (this.peekKeyword('TABLE')) {
|
|
2219
|
-
this.consumeKeyword('TABLE', "Expected TABLE after DROP.");
|
|
2220
|
-
objectType = 'table';
|
|
2221
|
-
} else if (this.peekKeyword('VIEW')) {
|
|
2222
|
-
this.consumeKeyword('VIEW', "Expected VIEW after DROP.");
|
|
2223
|
-
objectType = 'view';
|
|
2224
|
-
} else if (this.peekKeyword('INDEX')) {
|
|
2225
|
-
this.consumeKeyword('INDEX', "Expected INDEX after DROP.");
|
|
2226
|
-
objectType = 'index';
|
|
2227
|
-
} else if (this.peekKeyword('ASSERTION')) {
|
|
2228
|
-
this.consumeKeyword('ASSERTION', "Expected ASSERTION after DROP.");
|
|
2229
|
-
objectType = 'assertion';
|
|
2230
|
-
} else {
|
|
2231
|
-
throw this.error(this.peek(), "Expected TABLE, VIEW, INDEX, or ASSERTION after DROP.");
|
|
2232
|
-
}
|
|
2233
|
-
|
|
2234
|
-
let ifExists = false;
|
|
2235
|
-
if (this.matchKeyword('IF')) {
|
|
2236
|
-
this.consumeKeyword('EXISTS', "Expected 'EXISTS' after 'IF'.");
|
|
2237
|
-
ifExists = true;
|
|
2238
|
-
}
|
|
2239
|
-
|
|
2240
|
-
const name = this.tableIdentifier();
|
|
2241
|
-
|
|
2242
|
-
return {
|
|
2243
|
-
type: 'drop',
|
|
2244
|
-
objectType,
|
|
2245
|
-
name,
|
|
2246
|
-
ifExists,
|
|
2247
|
-
loc: _createLoc(startToken, this.previous()),
|
|
2248
|
-
};
|
|
2249
|
-
}
|
|
2250
|
-
|
|
2251
|
-
/**
|
|
2252
|
-
* Parse ALTER TABLE statement
|
|
2253
|
-
* @returns AST for ALTER TABLE statement
|
|
2254
|
-
*/
|
|
2255
|
-
private alterTableStatement(startToken: Token, _withClause?: AST.WithClause): AST.AlterTableStmt {
|
|
2256
|
-
this.consumeKeyword('TABLE', "Expected 'TABLE' after ALTER.");
|
|
2257
|
-
|
|
2258
|
-
const table = this.tableIdentifier();
|
|
2259
|
-
|
|
2260
|
-
let action: AST.AlterTableAction;
|
|
2261
|
-
|
|
2262
|
-
if (this.peekKeyword('RENAME')) {
|
|
2263
|
-
this.consumeKeyword('RENAME', "Expected RENAME.");
|
|
2264
|
-
if (this.matchKeyword('COLUMN')) {
|
|
2265
|
-
const oldName = this.consumeIdentifier(['key', 'action', 'set', 'default', 'check', 'unique', 'references', 'on', 'cascade', 'restrict', 'like'], "Expected old column name after RENAME COLUMN.");
|
|
2266
|
-
this.consumeKeyword('TO', "Expected 'TO' after old column name.");
|
|
2267
|
-
const newName = this.consumeIdentifier(['key', 'action', 'set', 'default', 'check', 'unique', 'references', 'on', 'cascade', 'restrict', 'like'], "Expected new column name after TO.");
|
|
2268
|
-
action = { type: 'renameColumn', oldName, newName };
|
|
2269
|
-
} else {
|
|
2270
|
-
this.consumeKeyword('TO', "Expected 'TO' after RENAME.");
|
|
2271
|
-
const newName = this.consumeIdentifier(['key', 'action', 'set', 'default', 'check', 'unique', 'references', 'on', 'cascade', 'restrict', 'like'], "Expected new table name after RENAME TO.");
|
|
2272
|
-
action = { type: 'renameTable', newName };
|
|
2273
|
-
}
|
|
2274
|
-
} else if (this.peekKeyword('ADD')) {
|
|
2275
|
-
this.consumeKeyword('ADD', "Expected ADD.");
|
|
2276
|
-
if (this.peekKeyword('CONSTRAINT')) {
|
|
2277
|
-
// ADD CONSTRAINT ... - let tableConstraint parse everything including CONSTRAINT keyword
|
|
2278
|
-
const constraint = this.tableConstraint();
|
|
2279
|
-
action = { type: 'addConstraint', constraint };
|
|
2280
|
-
} else {
|
|
2281
|
-
// ADD [COLUMN] column_def
|
|
2282
|
-
this.matchKeyword('COLUMN');
|
|
2283
|
-
const column = this.columnDefinition();
|
|
2284
|
-
action = { type: 'addColumn', column };
|
|
2285
|
-
}
|
|
2286
|
-
} else if (this.peekKeyword('DROP')) {
|
|
2287
|
-
this.consumeKeyword('DROP', "Expected DROP.");
|
|
2288
|
-
this.matchKeyword('COLUMN');
|
|
2289
|
-
const name = this.consumeIdentifier(['key', 'action', 'set', 'default', 'check', 'unique', 'references', 'on', 'cascade', 'restrict', 'like'], "Expected column name after DROP COLUMN.");
|
|
2290
|
-
action = { type: 'dropColumn', name };
|
|
2291
|
-
} else {
|
|
2292
|
-
throw this.error(this.peek(), "Expected RENAME, ADD, or DROP after table name in ALTER TABLE.");
|
|
2293
|
-
}
|
|
2294
|
-
|
|
2295
|
-
return {
|
|
2296
|
-
type: 'alterTable',
|
|
2297
|
-
table,
|
|
2298
|
-
action,
|
|
2299
|
-
loc: _createLoc(startToken, this.previous()),
|
|
2300
|
-
};
|
|
2301
|
-
}
|
|
2302
|
-
|
|
2303
|
-
/**
|
|
2304
|
-
* Parse BEGIN statement
|
|
2305
|
-
* @returns AST for BEGIN statement
|
|
2306
|
-
*/
|
|
2307
|
-
private beginStatement(startToken: Token, _withClause?: AST.WithClause): AST.BeginStmt {
|
|
2308
|
-
// Skip optional TRANSACTION keyword
|
|
2309
|
-
this.matchKeyword('TRANSACTION');
|
|
2310
|
-
|
|
2311
|
-
return { type: 'begin', loc: _createLoc(startToken, this.previous()) };
|
|
2312
|
-
}
|
|
2313
|
-
|
|
2314
|
-
/**
|
|
2315
|
-
* Parse COMMIT statement
|
|
2316
|
-
* @returns AST for COMMIT statement
|
|
2317
|
-
*/
|
|
2318
|
-
private commitStatement(startToken: Token, _withClause?: AST.WithClause): AST.CommitStmt {
|
|
2319
|
-
this.matchKeyword('TRANSACTION');
|
|
2320
|
-
return { type: 'commit', loc: _createLoc(startToken, this.previous()) };
|
|
2321
|
-
}
|
|
2322
|
-
|
|
2323
|
-
/**
|
|
2324
|
-
* Parse ROLLBACK statement
|
|
2325
|
-
* @returns AST for ROLLBACK statement
|
|
2326
|
-
*/
|
|
2327
|
-
private rollbackStatement(startToken: Token, _withClause?: AST.WithClause): AST.RollbackStmt {
|
|
2328
|
-
this.matchKeyword('TRANSACTION');
|
|
2329
|
-
|
|
2330
|
-
let savepoint: string | undefined;
|
|
2331
|
-
if (this.matchKeyword('TO')) {
|
|
2332
|
-
this.matchKeyword('SAVEPOINT');
|
|
2333
|
-
if (!this.check(TokenType.IDENTIFIER)) {
|
|
2334
|
-
throw this.error(this.peek(), "Expected savepoint name after ROLLBACK TO.");
|
|
2335
|
-
}
|
|
2336
|
-
savepoint = this.getIdentifierValue(this.advance());
|
|
2337
|
-
}
|
|
2338
|
-
return { type: 'rollback', savepoint, loc: _createLoc(startToken, this.previous()) };
|
|
2339
|
-
}
|
|
2340
|
-
|
|
2341
|
-
/**
|
|
2342
|
-
* Parse SAVEPOINT statement
|
|
2343
|
-
* @returns AST for SAVEPOINT statement
|
|
2344
|
-
*/
|
|
2345
|
-
private savepointStatement(startToken: Token, _withClause?: AST.WithClause): AST.SavepointStmt {
|
|
2346
|
-
const name = this.consumeIdentifier("Expected savepoint name after SAVEPOINT.");
|
|
2347
|
-
return { type: 'savepoint', name, loc: _createLoc(startToken, this.previous()) };
|
|
2348
|
-
}
|
|
2349
|
-
|
|
2350
|
-
/**
|
|
2351
|
-
* Parse RELEASE statement
|
|
2352
|
-
* @returns AST for RELEASE statement
|
|
2353
|
-
*/
|
|
2354
|
-
private releaseStatement(startToken: Token, _withClause?: AST.WithClause): AST.ReleaseStmt {
|
|
2355
|
-
this.matchKeyword('SAVEPOINT');
|
|
2356
|
-
const name = this.consumeIdentifier("Expected savepoint name after RELEASE [SAVEPOINT].");
|
|
2357
|
-
return { type: 'release', savepoint: name, loc: _createLoc(startToken, this.previous()) };
|
|
2358
|
-
}
|
|
2359
|
-
|
|
2360
|
-
/**
|
|
2361
|
-
* Parse PRAGMA statement
|
|
2362
|
-
* @returns AST for PRAGMA statement
|
|
2363
|
-
*/
|
|
2364
|
-
private pragmaStatement(startToken: Token, _withClause?: AST.WithClause): AST.PragmaStmt {
|
|
2365
|
-
const nameValue = this.nameValueItem("pragma");
|
|
2366
|
-
return { type: 'pragma', ...nameValue, loc: _createLoc(startToken, this.previous()) };
|
|
2367
|
-
}
|
|
2368
|
-
|
|
2369
|
-
// === Declarative schema parsing ===
|
|
2370
|
-
|
|
2371
|
-
private declareSchemaStatement(startToken: Token): AST.DeclareSchemaStmt {
|
|
2372
|
-
this.consumeKeyword('SCHEMA', "Expected 'SCHEMA' after DECLARE.");
|
|
2373
|
-
const schemaName = this.consumeIdentifier(['temp', 'temporary'], "Expected schema name after DECLARE.");
|
|
2374
|
-
let version: string | undefined;
|
|
2375
|
-
let using: { defaultVtabModule?: string; defaultVtabArgs?: string } | undefined;
|
|
2376
|
-
|
|
2377
|
-
// Optional: version 'semver'
|
|
2378
|
-
// no-op
|
|
2379
|
-
if (this.matchKeyword('VERSION')) {
|
|
2380
|
-
const tok = this.consume(TokenType.STRING, "Expected version string after VERSION.");
|
|
2381
|
-
version = String(tok.literal);
|
|
2382
|
-
}
|
|
2383
|
-
|
|
2384
|
-
// Optional: using ( default_vtab_module = 'memory', default_vtab_args = '[]' )
|
|
2385
|
-
if (this.match(TokenType.USING)) {
|
|
2386
|
-
this.consume(TokenType.LPAREN, "Expected '(' after USING.");
|
|
2387
|
-
using = {};
|
|
2388
|
-
if (!this.check(TokenType.RPAREN)) {
|
|
2389
|
-
do {
|
|
2390
|
-
const optName = this.consumeIdentifier("Expected option name inside USING().").toLowerCase();
|
|
2391
|
-
this.consume(TokenType.EQUAL, "Expected '=' after option name in USING().");
|
|
2392
|
-
if (optName === 'default_vtab_module') {
|
|
2393
|
-
const t = this.consume(TokenType.STRING, "Expected string for default_vtab_module.");
|
|
2394
|
-
using.defaultVtabModule = String(t.literal);
|
|
2395
|
-
} else if (optName === 'default_vtab_args') {
|
|
2396
|
-
const t = this.consume(TokenType.STRING, "Expected JSON string for default_vtab_args.");
|
|
2397
|
-
using.defaultVtabArgs = String(t.literal);
|
|
2398
|
-
} else {
|
|
2399
|
-
// Consume simple literal/identifier for forward compatibility
|
|
2400
|
-
if (this.check(TokenType.STRING) || this.check(TokenType.INTEGER) || this.check(TokenType.FLOAT) || this.check(TokenType.IDENTIFIER)) {
|
|
2401
|
-
this.advance();
|
|
2402
|
-
}
|
|
2403
|
-
}
|
|
2404
|
-
} while (this.match(TokenType.COMMA));
|
|
2405
|
-
}
|
|
2406
|
-
this.consume(TokenType.RPAREN, "Expected ')' after USING options.");
|
|
2407
|
-
}
|
|
2408
|
-
|
|
2409
|
-
// Block
|
|
2410
|
-
// Parse declaration block delimited by '{' '}'
|
|
2411
|
-
this.consume(TokenType.LBRACE, "Expected '{' to start schema declaration block.");
|
|
2412
|
-
const items: AST.DeclareItem[] = [];
|
|
2413
|
-
|
|
2414
|
-
while (!this.check(TokenType.RBRACE)) {
|
|
2415
|
-
if (this.isAtEnd()) break;
|
|
2416
|
-
// table ...
|
|
2417
|
-
if (this.peekKeyword('TABLE')) {
|
|
2418
|
-
this.advance();
|
|
2419
|
-
items.push(this.declareTableItem());
|
|
2420
|
-
} else if (this.peekKeyword('INDEX')) {
|
|
2421
|
-
this.advance();
|
|
2422
|
-
items.push(this.declareIndexItem());
|
|
2423
|
-
} else if (this.peekKeyword('VIEW')) {
|
|
2424
|
-
this.advance();
|
|
2425
|
-
items.push(this.declareViewItem());
|
|
2426
|
-
} else if (this.peekKeyword('SEED')) {
|
|
2427
|
-
this.advance();
|
|
2428
|
-
items.push(this.declareSeedItem());
|
|
2429
|
-
} else {
|
|
2430
|
-
// Fallback: ignore unrecognized item (domain, collation, import)
|
|
2431
|
-
const start = this.peek();
|
|
2432
|
-
// consume until semicolon
|
|
2433
|
-
while (!this.isAtEnd() && !this.check(TokenType.SEMICOLON) && !(this.check(TokenType.IDENTIFIER) && this.peek().lexeme === '}')) {
|
|
2434
|
-
this.advance();
|
|
2435
|
-
}
|
|
2436
|
-
const endTok = this.previous();
|
|
2437
|
-
items.push({ type: 'declareIgnored', kind: 'domain', text: this.sourceSlice(start.startOffset, endTok.endOffset) } as unknown as AST.DeclareIgnoredItem);
|
|
2438
|
-
}
|
|
2439
|
-
this.match(TokenType.SEMICOLON);
|
|
2440
|
-
}
|
|
2441
|
-
|
|
2442
|
-
this.consume(TokenType.RBRACE, "Expected '}' to close schema declaration block.");
|
|
2443
|
-
|
|
2444
|
-
const endTok = this.previous();
|
|
2445
|
-
return { type: 'declareSchema', schemaName, version, using, items, loc: _createLoc(startToken, endTok) };
|
|
2446
|
-
}
|
|
2447
|
-
|
|
2448
|
-
private declareTableItem(): AST.DeclaredTable {
|
|
2449
|
-
const tableName = this.consumeIdentifier(['key', 'action', 'set', 'default', 'check', 'unique', 'references', 'on', 'cascade', 'restrict', 'like'], 'Expected table name in declaration.');
|
|
2450
|
-
let moduleName: string | undefined;
|
|
2451
|
-
let moduleArgs: Record<string, SqlValue> | undefined;
|
|
2452
|
-
const columns: AST.ColumnDef[] = [];
|
|
2453
|
-
const constraints: AST.TableConstraint[] = [];
|
|
2454
|
-
|
|
2455
|
-
// Optional USING module
|
|
2456
|
-
if (this.match(TokenType.USING)) {
|
|
2457
|
-
if (this.check(TokenType.IDENTIFIER)) {
|
|
2458
|
-
moduleName = this.getIdentifierValue(this.advance());
|
|
2459
|
-
}
|
|
2460
|
-
if (this.match(TokenType.LPAREN)) {
|
|
2461
|
-
moduleArgs = {};
|
|
2462
|
-
let positionalIndex = 0;
|
|
2463
|
-
if (!this.check(TokenType.RPAREN)) {
|
|
2464
|
-
do {
|
|
2465
|
-
// Check if this is a positional argument (string/number literal) or named argument (identifier=value)
|
|
2466
|
-
if (this.check(TokenType.STRING) || this.check(TokenType.INTEGER) || this.check(TokenType.FLOAT)) {
|
|
2467
|
-
// Positional argument
|
|
2468
|
-
const token = this.advance();
|
|
2469
|
-
moduleArgs[String(positionalIndex++)] = token.literal;
|
|
2470
|
-
} else if (this.check(TokenType.IDENTIFIER)) {
|
|
2471
|
-
// Could be named argument or identifier value
|
|
2472
|
-
const nv = this.nameValueItem('module argument');
|
|
2473
|
-
moduleArgs[nv.name] = nv.value && nv.value.type === 'literal' ? getSyncLiteral(nv.value) : (nv.value && nv.value.type === 'identifier' ? nv.value.name : null);
|
|
2474
|
-
} else {
|
|
2475
|
-
throw this.error(this.peek(), "Expected module argument (string, number, or name=value pair).");
|
|
2476
|
-
}
|
|
2477
|
-
} while (this.match(TokenType.COMMA));
|
|
2478
|
-
}
|
|
2479
|
-
this.consume(TokenType.RPAREN, "Expected ')' after module arguments.");
|
|
2480
|
-
}
|
|
2481
|
-
}
|
|
2482
|
-
|
|
2483
|
-
// Column list can be in parens (...) or braces {...}
|
|
2484
|
-
const useBraces = this.check(TokenType.LBRACE);
|
|
2485
|
-
if (useBraces) {
|
|
2486
|
-
this.consume(TokenType.LBRACE, "Expected '{' before column definitions.");
|
|
2487
|
-
} else {
|
|
2488
|
-
this.consume(TokenType.LPAREN, "Expected '(' or '{' before column definitions.");
|
|
2489
|
-
}
|
|
2490
|
-
|
|
2491
|
-
if (!this.check(useBraces ? TokenType.RBRACE : TokenType.RPAREN)) {
|
|
2492
|
-
do {
|
|
2493
|
-
// Distinguish table constraint vs column definition by lookahead for '(' or constraint keywords
|
|
2494
|
-
if (this.peekKeyword('CONSTRAINT') || this.peekKeyword('PRIMARY') || this.peekKeyword('UNIQUE') || this.peekKeyword('CHECK') || this.peekKeyword('FOREIGN')) {
|
|
2495
|
-
constraints.push(this.tableConstraint());
|
|
2496
|
-
} else {
|
|
2497
|
-
columns.push(this.columnDefinition());
|
|
2498
|
-
}
|
|
2499
|
-
} while (this.match(TokenType.COMMA) && !this.check(useBraces ? TokenType.RBRACE : TokenType.RPAREN));
|
|
2500
|
-
}
|
|
2501
|
-
|
|
2502
|
-
if (useBraces) {
|
|
2503
|
-
this.consume(TokenType.RBRACE, "Expected '}' after table definition.");
|
|
2504
|
-
} else {
|
|
2505
|
-
this.consume(TokenType.RPAREN, "Expected ')' after table definition.");
|
|
2506
|
-
}
|
|
2507
|
-
|
|
2508
|
-
// Build the CREATE TABLE AST node for this declared table
|
|
2509
|
-
const tableStmt: AST.CreateTableStmt = {
|
|
2510
|
-
type: 'createTable',
|
|
2511
|
-
table: { type: 'identifier', name: tableName },
|
|
2512
|
-
ifNotExists: false,
|
|
2513
|
-
columns,
|
|
2514
|
-
constraints,
|
|
2515
|
-
isTemporary: false,
|
|
2516
|
-
moduleName,
|
|
2517
|
-
moduleArgs
|
|
2518
|
-
};
|
|
2519
|
-
|
|
2520
|
-
return { type: 'declaredTable', tableStmt };
|
|
2521
|
-
}
|
|
2522
|
-
|
|
2523
|
-
private declareIndexItem(): AST.DeclaredIndex {
|
|
2524
|
-
const indexName = this.consumeIdentifier('Expected index name.');
|
|
2525
|
-
this.consumeKeyword('ON', "Expected 'ON' after index name.");
|
|
2526
|
-
const tableName = this.consumeIdentifier('Expected table name after ON.');
|
|
2527
|
-
this.consume(TokenType.LPAREN, "Expected '(' before index columns.");
|
|
2528
|
-
const columns = this.indexedColumnList();
|
|
2529
|
-
this.consume(TokenType.RPAREN, "Expected ')' after index columns.");
|
|
2530
|
-
|
|
2531
|
-
const indexStmt: AST.CreateIndexStmt = {
|
|
2532
|
-
type: 'createIndex',
|
|
2533
|
-
index: { type: 'identifier', name: indexName },
|
|
2534
|
-
table: { type: 'identifier', name: tableName },
|
|
2535
|
-
ifNotExists: false,
|
|
2536
|
-
columns,
|
|
2537
|
-
isUnique: false
|
|
2538
|
-
};
|
|
2539
|
-
|
|
2540
|
-
return { type: 'declaredIndex', indexStmt };
|
|
2541
|
-
}
|
|
2542
|
-
|
|
2543
|
-
private declareViewItem(): AST.DeclaredView {
|
|
2544
|
-
const viewName = this.consumeIdentifier('Expected view name.');
|
|
2545
|
-
let columns: string[] | undefined;
|
|
2546
|
-
if (this.match(TokenType.LPAREN)) {
|
|
2547
|
-
columns = this.identifierList();
|
|
2548
|
-
this.consume(TokenType.RPAREN, "Expected ')' after view columns.");
|
|
2549
|
-
}
|
|
2550
|
-
this.consumeKeyword('AS', "Expected AS before SELECT in view declaration.");
|
|
2551
|
-
const selTok = this.consume(TokenType.SELECT, "Expected SELECT after AS in view declaration.");
|
|
2552
|
-
const select = this.selectStatement(selTok);
|
|
2553
|
-
|
|
2554
|
-
const viewStmt: AST.CreateViewStmt = {
|
|
2555
|
-
type: 'createView',
|
|
2556
|
-
view: { type: 'identifier', name: viewName },
|
|
2557
|
-
ifNotExists: false,
|
|
2558
|
-
columns,
|
|
2559
|
-
select,
|
|
2560
|
-
isTemporary: false
|
|
2561
|
-
};
|
|
2562
|
-
|
|
2563
|
-
return { type: 'declaredView', viewStmt };
|
|
2564
|
-
}
|
|
2565
|
-
|
|
2566
|
-
private declareSeedItem(): AST.DeclaredSeed {
|
|
2567
|
-
// seed <table> ( (...), (...) ) or seed <table> values (col, ...) values (...), (...)
|
|
2568
|
-
const tableName = this.consumeIdentifier('Expected table name after SEED.');
|
|
2569
|
-
|
|
2570
|
-
let columns: string[] | undefined;
|
|
2571
|
-
const rows: SqlValue[][] = [];
|
|
2572
|
-
|
|
2573
|
-
// Check for column list syntax: seed table (cols...) values (...)
|
|
2574
|
-
if (this.matchKeyword('VALUES')) {
|
|
2575
|
-
this.consume(TokenType.LPAREN, "Expected '(' before seed column list.");
|
|
2576
|
-
columns = this.identifierList();
|
|
2577
|
-
this.consume(TokenType.RPAREN, "Expected ')' after seed column list.");
|
|
2578
|
-
this.consumeKeyword('VALUES', "Expected VALUES to introduce seed rows.");
|
|
2579
|
-
}
|
|
2580
|
-
|
|
2581
|
-
// Parse seed rows: ( (...), (...) )
|
|
2582
|
-
this.consume(TokenType.LPAREN, "Expected '(' before seed rows.");
|
|
2583
|
-
|
|
2584
|
-
do {
|
|
2585
|
-
this.consume(TokenType.LPAREN, "Expected '(' before seed row values.");
|
|
2586
|
-
const rowValues: SqlValue[] = [];
|
|
2587
|
-
if (!this.check(TokenType.RPAREN)) {
|
|
2588
|
-
do {
|
|
2589
|
-
const expr = this.expression();
|
|
2590
|
-
// Evaluate literal expressions to SqlValue
|
|
2591
|
-
if (expr.type === 'literal') {
|
|
2592
|
-
rowValues.push(getSyncLiteral(expr));
|
|
2593
|
-
} else {
|
|
2594
|
-
throw this.error(this.peek(), "Seed data must contain only literal values.");
|
|
2595
|
-
}
|
|
2596
|
-
} while (this.match(TokenType.COMMA));
|
|
2597
|
-
}
|
|
2598
|
-
this.consume(TokenType.RPAREN, "Expected ')' after seed row values.");
|
|
2599
|
-
rows.push(rowValues);
|
|
2600
|
-
} while (this.match(TokenType.COMMA));
|
|
2601
|
-
|
|
2602
|
-
this.consume(TokenType.RPAREN, "Expected ')' after seed rows.");
|
|
2603
|
-
|
|
2604
|
-
return { type: 'declaredSeed', tableName, columns, seedData: rows };
|
|
2605
|
-
}
|
|
2606
|
-
|
|
2607
|
-
private diffSchemaStatement(startToken: Token): AST.DiffSchemaStmt {
|
|
2608
|
-
this.consumeKeyword('SCHEMA', "Expected SCHEMA after DIFF.");
|
|
2609
|
-
const schemaName = this.consumeIdentifier(['temp', 'temporary'], 'Expected schema name after DIFF SCHEMA.');
|
|
2610
|
-
return { type: 'diffSchema', schemaName, loc: _createLoc(startToken, this.previous()) };
|
|
2611
|
-
}
|
|
2612
|
-
|
|
2613
|
-
private applySchemaStatement(startToken: Token): AST.ApplySchemaStmt {
|
|
2614
|
-
this.consumeKeyword('SCHEMA', "Expected SCHEMA after APPLY.");
|
|
2615
|
-
const schemaName = this.consumeIdentifier(['temp', 'temporary'], 'Expected schema name after APPLY SCHEMA.');
|
|
2616
|
-
let toVersion: string | undefined;
|
|
2617
|
-
let withSeed = false;
|
|
2618
|
-
let options: AST.ApplySchemaStmt['options'] | undefined;
|
|
2619
|
-
|
|
2620
|
-
if (this.matchKeyword('TO')) {
|
|
2621
|
-
this.consumeKeyword('VERSION', "Expected VERSION after TO.");
|
|
2622
|
-
const tok = this.consume(TokenType.STRING, "Expected version string after TO VERSION.");
|
|
2623
|
-
toVersion = String(tok.literal);
|
|
2624
|
-
}
|
|
2625
|
-
|
|
2626
|
-
// Check for WITH SEED
|
|
2627
|
-
if (this.matchKeyword('WITH')) {
|
|
2628
|
-
this.consumeKeyword('SEED', "Expected SEED after WITH.");
|
|
2629
|
-
withSeed = true;
|
|
2630
|
-
}
|
|
2631
|
-
|
|
2632
|
-
if (this.matchKeyword('OPTIONS')) {
|
|
2633
|
-
this.consume(TokenType.LPAREN, "Expected '(' after OPTIONS.");
|
|
2634
|
-
options = {};
|
|
2635
|
-
if (!this.check(TokenType.RPAREN)) {
|
|
2636
|
-
do {
|
|
2637
|
-
const key = this.consumeIdentifier('Expected option key.').toLowerCase();
|
|
2638
|
-
this.consume(TokenType.EQUAL, "Expected '=' after option key.");
|
|
2639
|
-
if (key === 'dry_run') options.dryRun = this.consumeBooleanLiteral();
|
|
2640
|
-
else if (key === 'validate_only') options.validateOnly = this.consumeBooleanLiteral();
|
|
2641
|
-
else if (key === 'allow_destructive') options.allowDestructive = this.consumeBooleanLiteral();
|
|
2642
|
-
else if (key === 'rename_policy') {
|
|
2643
|
-
const vtok = this.consume(TokenType.STRING, "Expected string for rename_policy.");
|
|
2644
|
-
options.renamePolicy = String(vtok.literal) as 'require-hint' | 'infer-id';
|
|
2645
|
-
} else {
|
|
2646
|
-
// consume literal
|
|
2647
|
-
if (this.check(TokenType.STRING) || this.check(TokenType.INTEGER) || this.check(TokenType.FLOAT) || this.check(TokenType.IDENTIFIER)) this.advance();
|
|
2648
|
-
}
|
|
2649
|
-
} while (this.match(TokenType.COMMA));
|
|
2650
|
-
}
|
|
2651
|
-
this.consume(TokenType.RPAREN, "Expected ')' after OPTIONS.");
|
|
2652
|
-
}
|
|
2653
|
-
|
|
2654
|
-
return { type: 'applySchema', schemaName, toVersion, withSeed, options, loc: _createLoc(startToken, this.previous()) };
|
|
2655
|
-
}
|
|
2656
|
-
|
|
2657
|
-
private explainSchemaStatement(startToken: Token): AST.ExplainSchemaStmt {
|
|
2658
|
-
this.consumeKeyword('SCHEMA', "Expected SCHEMA after EXPLAIN.");
|
|
2659
|
-
const schemaName = this.consumeIdentifier(['temp', 'temporary'], 'Expected schema name after EXPLAIN SCHEMA.');
|
|
2660
|
-
let version: string | undefined;
|
|
2661
|
-
|
|
2662
|
-
if (this.matchKeyword('VERSION')) {
|
|
2663
|
-
const tok = this.consume(TokenType.STRING, "Expected version string after VERSION.");
|
|
2664
|
-
version = String(tok.literal);
|
|
2665
|
-
}
|
|
2666
|
-
|
|
2667
|
-
return { type: 'explainSchema', schemaName, version, loc: _createLoc(startToken, this.previous()) };
|
|
2668
|
-
}
|
|
2669
|
-
|
|
2670
|
-
private consumeBooleanLiteral(): boolean {
|
|
2671
|
-
if (this.match(TokenType.TRUE)) return true;
|
|
2672
|
-
if (this.match(TokenType.FALSE)) return false;
|
|
2673
|
-
if (this.check(TokenType.STRING)) {
|
|
2674
|
-
const t = this.advance();
|
|
2675
|
-
const v = String(t.literal).toLowerCase();
|
|
2676
|
-
return v === 'true' || v === '1';
|
|
2677
|
-
}
|
|
2678
|
-
if (this.check(TokenType.INTEGER)) {
|
|
2679
|
-
const t = this.advance();
|
|
2680
|
-
return Number(t.literal) !== 0;
|
|
2681
|
-
}
|
|
2682
|
-
return false;
|
|
2683
|
-
}
|
|
2684
|
-
|
|
2685
|
-
private sourceSlice(_start: number, _end: number): string {
|
|
2686
|
-
// Lexer tokens include offsets; this.tokens array belongs to this parser, but we don't have direct source here.
|
|
2687
|
-
// Return an empty string as placeholder; canonicalization is future work.
|
|
2688
|
-
return '';
|
|
2689
|
-
}
|
|
2690
|
-
|
|
2691
|
-
private nameValueItem(context: string): { name: string, value?: AST.IdentifierExpr | AST.LiteralExpr } {
|
|
2692
|
-
const name = this.consumeIdentifier(`Expected ${context} name.`);
|
|
2693
|
-
|
|
2694
|
-
let value: AST.LiteralExpr | AST.IdentifierExpr | undefined;
|
|
2695
|
-
if (this.match(TokenType.EQUAL)) {
|
|
2696
|
-
if (this.check(TokenType.IDENTIFIER)) {
|
|
2697
|
-
value = { type: 'identifier', name: this.getIdentifierValue(this.advance()) };
|
|
2698
|
-
} else if (this.match(TokenType.STRING, TokenType.INTEGER, TokenType.FLOAT, TokenType.NULL, TokenType.TRUE, TokenType.FALSE)) {
|
|
2699
|
-
const token = this.previous();
|
|
2700
|
-
let literal_value: SqlValue;
|
|
2701
|
-
if (token.type === TokenType.NULL) {
|
|
2702
|
-
literal_value = null;
|
|
2703
|
-
} else if (token.type === TokenType.TRUE) {
|
|
2704
|
-
literal_value = 1;
|
|
2705
|
-
} else if (token.type === TokenType.FALSE) {
|
|
2706
|
-
literal_value = 0;
|
|
2707
|
-
} else {
|
|
2708
|
-
literal_value = token.literal;
|
|
2709
|
-
}
|
|
2710
|
-
value = { type: 'literal', value: literal_value };
|
|
2711
|
-
} else if (this.match(TokenType.MINUS)) {
|
|
2712
|
-
if (this.check(TokenType.INTEGER) || this.check(TokenType.FLOAT)) {
|
|
2713
|
-
const token = this.advance();
|
|
2714
|
-
value = { type: 'literal', value: -token.literal };
|
|
2715
|
-
} else {
|
|
2716
|
-
throw this.error(this.peek(), "Expected number after '-'.");
|
|
2717
|
-
}
|
|
2718
|
-
} else {
|
|
2719
|
-
throw this.error(this.peek(), `Expected ${context} value (identifier, string, number, or NULL).`);
|
|
2720
|
-
}
|
|
2721
|
-
}
|
|
2722
|
-
// If no '=' is found, value remains undefined (reading mode)
|
|
2723
|
-
|
|
2724
|
-
return { name: name.toLowerCase(), value };
|
|
2725
|
-
}
|
|
2726
|
-
|
|
2727
|
-
// --- Supporting Clause / Definition Parsers ---
|
|
2728
|
-
|
|
2729
|
-
/** @internal Parses a comma-separated list of indexed columns */
|
|
2730
|
-
private indexedColumnList(): AST.IndexedColumn[] {
|
|
2731
|
-
const columns: AST.IndexedColumn[] = [];
|
|
2732
|
-
do {
|
|
2733
|
-
columns.push(this.indexedColumn());
|
|
2734
|
-
} while (this.match(TokenType.COMMA));
|
|
2735
|
-
return columns;
|
|
2736
|
-
}
|
|
2737
|
-
|
|
2738
|
-
/** @internal Parses a single indexed column definition */
|
|
2739
|
-
private indexedColumn(): AST.IndexedColumn {
|
|
2740
|
-
const expr = this.expression();
|
|
2741
|
-
|
|
2742
|
-
let name: string | undefined;
|
|
2743
|
-
if (expr.type === 'column' && !expr.table && !expr.schema) {
|
|
2744
|
-
name = expr.name;
|
|
2745
|
-
}
|
|
2746
|
-
|
|
2747
|
-
let direction: 'asc' | 'desc' | undefined;
|
|
2748
|
-
if (this.match(TokenType.ASC)) {
|
|
2749
|
-
direction = 'asc';
|
|
2750
|
-
} else if (this.match(TokenType.DESC)) {
|
|
2751
|
-
direction = 'desc';
|
|
2752
|
-
}
|
|
2753
|
-
|
|
2754
|
-
if (name) {
|
|
2755
|
-
return { name, direction };
|
|
2756
|
-
} else {
|
|
2757
|
-
return { expr, direction };
|
|
2758
|
-
}
|
|
2759
|
-
}
|
|
2760
|
-
|
|
2761
|
-
/**
|
|
2762
|
-
* @internal Helper to extract the identifier value from a token.
|
|
2763
|
-
* For quoted identifiers (double-quoted, backtick, bracket), returns the unquoted value.
|
|
2764
|
-
* For unquoted identifiers, returns the lexeme.
|
|
2765
|
-
*/
|
|
2766
|
-
private getIdentifierValue(token: Token): string {
|
|
2767
|
-
return token.literal !== undefined ? String(token.literal) : token.lexeme;
|
|
2768
|
-
}
|
|
2769
|
-
|
|
2770
|
-
/** @internal Helper to consume an IDENTIFIER token and return its lexeme */
|
|
2771
|
-
private consumeIdentifier(errorMessage: string): string;
|
|
2772
|
-
private consumeIdentifier(availableKeywords: string[], errorMessage: string): string;
|
|
2773
|
-
private consumeIdentifier(errorMessageOrKeywords: string | string[], errorMessage?: string): string {
|
|
2774
|
-
if (typeof errorMessageOrKeywords === 'string') {
|
|
2775
|
-
// Single parameter version - no contextual keywords
|
|
2776
|
-
return this.consumeIdentifierOrContextualKeyword([], errorMessageOrKeywords);
|
|
2777
|
-
} else {
|
|
2778
|
-
// Two parameter version - with contextual keywords
|
|
2779
|
-
return this.consumeIdentifierOrContextualKeyword(errorMessageOrKeywords, errorMessage!);
|
|
2780
|
-
}
|
|
2781
|
-
}
|
|
2782
|
-
|
|
2783
|
-
/**
|
|
2784
|
-
* @internal Helper to consume an IDENTIFIER token or specified contextual keywords
|
|
2785
|
-
* @param availableKeywords Array of keyword strings that can be used as identifiers in this context
|
|
2786
|
-
* @param errorMessage Error message if no valid token is found
|
|
2787
|
-
* @returns The identifier value (unquoted for quoted identifiers)
|
|
2788
|
-
*/
|
|
2789
|
-
private consumeIdentifierOrContextualKeyword(availableKeywords: string[], errorMessage: string): string {
|
|
2790
|
-
const token = this.peek();
|
|
2791
|
-
|
|
2792
|
-
// First check for regular identifier
|
|
2793
|
-
if (this.check(TokenType.IDENTIFIER)) {
|
|
2794
|
-
return this.getIdentifierValue(this.advance());
|
|
2795
|
-
}
|
|
2796
|
-
|
|
2797
|
-
// Then check for available contextual keywords
|
|
2798
|
-
for (const keyword of availableKeywords) {
|
|
2799
|
-
const keywordUpper = keyword.toUpperCase();
|
|
2800
|
-
const expectedTokenType = TokenType[keywordUpper as keyof typeof TokenType];
|
|
2801
|
-
|
|
2802
|
-
if (expectedTokenType && token.type === expectedTokenType) {
|
|
2803
|
-
// This keyword token is available as an identifier in this context
|
|
2804
|
-
return this.advance().lexeme;
|
|
2805
|
-
}
|
|
2806
|
-
}
|
|
2807
|
-
|
|
2808
|
-
throw this.error(this.peek(), errorMessage);
|
|
2809
|
-
}
|
|
2810
|
-
|
|
2811
|
-
/**
|
|
2812
|
-
* @internal Helper to check if current token is an identifier or available contextual keyword
|
|
2813
|
-
*/
|
|
2814
|
-
private checkIdentifierLike(availableKeywords: string[] = []): boolean {
|
|
2815
|
-
if (this.check(TokenType.IDENTIFIER)) {
|
|
2816
|
-
return true;
|
|
2817
|
-
}
|
|
2818
|
-
|
|
2819
|
-
return this.isContextualKeywordAvailable(availableKeywords);
|
|
2820
|
-
}
|
|
2821
|
-
|
|
2822
|
-
/**
|
|
2823
|
-
* @internal Helper to check if token at offset is an identifier or available contextual keyword
|
|
2824
|
-
*/
|
|
2825
|
-
private checkIdentifierLikeAt(offset: number, availableKeywords: string[] = []): boolean {
|
|
2826
|
-
if (this.checkNext(offset, TokenType.IDENTIFIER)) {
|
|
2827
|
-
return true;
|
|
2828
|
-
}
|
|
2829
|
-
|
|
2830
|
-
if (this.current + offset >= this.tokens.length) return false;
|
|
2831
|
-
const token = this.tokens[this.current + offset];
|
|
2832
|
-
|
|
2833
|
-
for (const keyword of availableKeywords) {
|
|
2834
|
-
const keywordUpper = keyword.toUpperCase();
|
|
2835
|
-
const expectedTokenType = TokenType[keywordUpper as keyof typeof TokenType];
|
|
2836
|
-
|
|
2837
|
-
if (expectedTokenType && token.type === expectedTokenType) {
|
|
2838
|
-
return true;
|
|
2839
|
-
}
|
|
2840
|
-
}
|
|
2841
|
-
|
|
2842
|
-
return false;
|
|
2843
|
-
}
|
|
2844
|
-
|
|
2845
|
-
/**
|
|
2846
|
-
* @internal Helper to check if any of the specified contextual keywords are available at current position
|
|
2847
|
-
*/
|
|
2848
|
-
private isContextualKeywordAvailable(availableKeywords: string[]): boolean {
|
|
2849
|
-
const token = this.peek();
|
|
2850
|
-
|
|
2851
|
-
for (const keyword of availableKeywords) {
|
|
2852
|
-
const keywordUpper = keyword.toUpperCase();
|
|
2853
|
-
const expectedTokenType = TokenType[keywordUpper as keyof typeof TokenType];
|
|
2854
|
-
|
|
2855
|
-
if (expectedTokenType && token.type === expectedTokenType) {
|
|
2856
|
-
return true;
|
|
2857
|
-
}
|
|
2858
|
-
}
|
|
2859
|
-
|
|
2860
|
-
return false;
|
|
2861
|
-
}
|
|
2862
|
-
|
|
2863
|
-
// --- Stubs for required helpers (implement fully for CREATE TABLE) ---
|
|
2864
|
-
|
|
2865
|
-
/** @internal Parses a column definition */
|
|
2866
|
-
private columnDefinition(): AST.ColumnDef {
|
|
2867
|
-
const name = this.consumeIdentifier(['key', 'action', 'set', 'default', 'check', 'unique', 'references', 'on', 'cascade', 'restrict', 'like'], "Expected column name.");
|
|
2868
|
-
|
|
2869
|
-
let dataType: string | undefined;
|
|
2870
|
-
if (this.check(TokenType.IDENTIFIER)) {
|
|
2871
|
-
dataType = this.advance().lexeme;
|
|
2872
|
-
if (this.match(TokenType.LPAREN)) {
|
|
2873
|
-
dataType += '(';
|
|
2874
|
-
let parenLevel = 1;
|
|
2875
|
-
while (parenLevel > 0 && !this.isAtEnd()) {
|
|
2876
|
-
const token = this.peek();
|
|
2877
|
-
if (token.type === TokenType.LPAREN) parenLevel++;
|
|
2878
|
-
if (token.type === TokenType.RPAREN) parenLevel--;
|
|
2879
|
-
if (parenLevel > 0) {
|
|
2880
|
-
dataType += this.advance().lexeme;
|
|
2881
|
-
}
|
|
2882
|
-
}
|
|
2883
|
-
dataType += ')';
|
|
2884
|
-
this.consume(TokenType.RPAREN, "Expected ')' after type parameters.");
|
|
2885
|
-
}
|
|
2886
|
-
}
|
|
2887
|
-
|
|
2888
|
-
const constraints = this.columnConstraintList();
|
|
2889
|
-
|
|
2890
|
-
return { name, dataType, constraints };
|
|
2891
|
-
}
|
|
2892
|
-
|
|
2893
|
-
/** @internal Parses mutation context variable definitions: WITH CONTEXT (var type [NULL], ...) */
|
|
2894
|
-
private parseMutationContextDefinitions(): AST.MutationContextVar[] {
|
|
2895
|
-
this.consume(TokenType.LPAREN, "Expected '(' after WITH CONTEXT.");
|
|
2896
|
-
|
|
2897
|
-
const contextVars: AST.MutationContextVar[] = [];
|
|
2898
|
-
|
|
2899
|
-
do {
|
|
2900
|
-
const name = this.consumeIdentifier("Expected context variable name.");
|
|
2901
|
-
|
|
2902
|
-
let dataType: string | undefined;
|
|
2903
|
-
if (this.check(TokenType.IDENTIFIER)) {
|
|
2904
|
-
dataType = this.advance().lexeme;
|
|
2905
|
-
if (this.match(TokenType.LPAREN)) {
|
|
2906
|
-
dataType += '(';
|
|
2907
|
-
let parenLevel = 1;
|
|
2908
|
-
while (parenLevel > 0 && !this.isAtEnd()) {
|
|
2909
|
-
const token = this.peek();
|
|
2910
|
-
if (token.type === TokenType.LPAREN) parenLevel++;
|
|
2911
|
-
if (token.type === TokenType.RPAREN) parenLevel--;
|
|
2912
|
-
if (parenLevel > 0) {
|
|
2913
|
-
dataType += this.advance().lexeme;
|
|
2914
|
-
}
|
|
2915
|
-
}
|
|
2916
|
-
dataType += ')';
|
|
2917
|
-
this.consume(TokenType.RPAREN, "Expected ')' after type parameters.");
|
|
2918
|
-
}
|
|
2919
|
-
}
|
|
2920
|
-
|
|
2921
|
-
// Check for NULL keyword (explicit nullable marker)
|
|
2922
|
-
const notNull = !this.match(TokenType.NULL);
|
|
2923
|
-
|
|
2924
|
-
contextVars.push({ name, dataType, notNull });
|
|
2925
|
-
|
|
2926
|
-
} while (this.match(TokenType.COMMA));
|
|
2927
|
-
|
|
2928
|
-
this.consume(TokenType.RPAREN, "Expected ')' after mutation context definitions.");
|
|
2929
|
-
|
|
2930
|
-
return contextVars;
|
|
2931
|
-
}
|
|
2932
|
-
|
|
2933
|
-
/** @internal Parses mutation context assignments: WITH CONTEXT var = expr, ... */
|
|
2934
|
-
private parseContextAssignments(): AST.ContextAssignment[] {
|
|
2935
|
-
const assignments: AST.ContextAssignment[] = [];
|
|
2936
|
-
|
|
2937
|
-
do {
|
|
2938
|
-
const name = this.consumeIdentifier("Expected context variable name.");
|
|
2939
|
-
this.consume(TokenType.EQUAL, `Expected '=' after context variable '${name}'.`);
|
|
2940
|
-
const value = this.expression();
|
|
2941
|
-
|
|
2942
|
-
assignments.push({ name, value });
|
|
2943
|
-
|
|
2944
|
-
} while (this.match(TokenType.COMMA));
|
|
2945
|
-
|
|
2946
|
-
return assignments;
|
|
2947
|
-
}
|
|
2948
|
-
|
|
2949
|
-
/** @internal Parses column constraints */
|
|
2950
|
-
private columnConstraintList(): AST.ColumnConstraint[] {
|
|
2951
|
-
const constraints: AST.ColumnConstraint[] = [];
|
|
2952
|
-
while (this.isColumnConstraintStart()) {
|
|
2953
|
-
constraints.push(this.columnConstraint());
|
|
2954
|
-
}
|
|
2955
|
-
return constraints;
|
|
2956
|
-
}
|
|
2957
|
-
|
|
2958
|
-
/** @internal Checks if the current token can start a column constraint */
|
|
2959
|
-
private isColumnConstraintStart(): boolean {
|
|
2960
|
-
return this.check(TokenType.CONSTRAINT) ||
|
|
2961
|
-
this.check(TokenType.PRIMARY) ||
|
|
2962
|
-
this.check(TokenType.NOT) ||
|
|
2963
|
-
this.check(TokenType.NULL) ||
|
|
2964
|
-
this.check(TokenType.UNIQUE) ||
|
|
2965
|
-
this.check(TokenType.CHECK) ||
|
|
2966
|
-
this.check(TokenType.DEFAULT) ||
|
|
2967
|
-
this.check(TokenType.COLLATE) ||
|
|
2968
|
-
this.check(TokenType.REFERENCES) ||
|
|
2969
|
-
this.check(TokenType.GENERATED);
|
|
2970
|
-
}
|
|
2971
|
-
|
|
2972
|
-
/** @internal Parses a single column constraint */
|
|
2973
|
-
private columnConstraint(): AST.ColumnConstraint {
|
|
2974
|
-
let name: string | undefined;
|
|
2975
|
-
const startToken = this.peek(); // Capture start token
|
|
2976
|
-
let endToken = startToken; // Initialize end token
|
|
2977
|
-
|
|
2978
|
-
if (this.match(TokenType.CONSTRAINT)) {
|
|
2979
|
-
name = this.consumeIdentifier("Expected constraint name after CONSTRAINT.");
|
|
2980
|
-
endToken = this.previous();
|
|
2981
|
-
}
|
|
2982
|
-
|
|
2983
|
-
if (this.match(TokenType.PRIMARY)) {
|
|
2984
|
-
this.consume(TokenType.KEY, "Expected KEY after PRIMARY.");
|
|
2985
|
-
const direction = this.match(TokenType.ASC) ? 'asc' : this.match(TokenType.DESC) ? 'desc' : undefined;
|
|
2986
|
-
if (direction) endToken = this.previous();
|
|
2987
|
-
const onConflict = this.parseConflictClause();
|
|
2988
|
-
if (onConflict) endToken = this.previous(); // Update endToken if conflict clause was parsed
|
|
2989
|
-
const autoincrement = this.match(TokenType.AUTOINCREMENT);
|
|
2990
|
-
if (autoincrement) endToken = this.previous();
|
|
2991
|
-
return { type: 'primaryKey', name, onConflict, autoincrement, direction, loc: _createLoc(startToken, endToken) };
|
|
2992
|
-
} else if (this.match(TokenType.NOT)) {
|
|
2993
|
-
this.consume(TokenType.NULL, "Expected NULL after NOT.");
|
|
2994
|
-
endToken = this.previous();
|
|
2995
|
-
const onConflict = this.parseConflictClause();
|
|
2996
|
-
if (onConflict) endToken = this.previous(); // Update endToken if conflict clause was parsed
|
|
2997
|
-
return { type: 'notNull', name, onConflict, loc: _createLoc(startToken, endToken) };
|
|
2998
|
-
} else if (this.match(TokenType.NULL)) {
|
|
2999
|
-
endToken = this.previous();
|
|
3000
|
-
const onConflict = this.parseConflictClause();
|
|
3001
|
-
if (onConflict) endToken = this.previous(); // Update endToken if conflict clause was parsed
|
|
3002
|
-
return { type: 'null', name, onConflict, loc: _createLoc(startToken, endToken) };
|
|
3003
|
-
} else if (this.match(TokenType.UNIQUE)) {
|
|
3004
|
-
endToken = this.previous();
|
|
3005
|
-
const onConflict = this.parseConflictClause();
|
|
3006
|
-
if (onConflict) endToken = this.previous(); // Update endToken if conflict clause was parsed
|
|
3007
|
-
return { type: 'unique', name, onConflict, loc: _createLoc(startToken, endToken) };
|
|
3008
|
-
} else if (this.match(TokenType.CHECK)) {
|
|
3009
|
-
// --- Parse optional ON clause before parentheses --- //
|
|
3010
|
-
let operations: RowOp[] | undefined;
|
|
3011
|
-
if (this.matchKeyword('ON')) {
|
|
3012
|
-
operations = this.parseRowOpList();
|
|
3013
|
-
}
|
|
3014
|
-
// --- End Parse ON clause --- //
|
|
3015
|
-
this.consume(TokenType.LPAREN, "Expected '(' after CHECK.");
|
|
3016
|
-
const expr = this.expression();
|
|
3017
|
-
endToken = this.consume(TokenType.RPAREN, "Expected ')' after CHECK expression.");
|
|
3018
|
-
// No DEFERRABLE syntax supported; deferral is auto-detected by the planner
|
|
3019
|
-
return {
|
|
3020
|
-
type: 'check',
|
|
3021
|
-
name,
|
|
3022
|
-
expr,
|
|
3023
|
-
operations,
|
|
3024
|
-
loc: _createLoc(startToken, endToken)
|
|
3025
|
-
};
|
|
3026
|
-
} else if (this.match(TokenType.DEFAULT)) {
|
|
3027
|
-
const expr = this.expression();
|
|
3028
|
-
endToken = this.previous();
|
|
3029
|
-
return { type: 'default', name, expr, loc: _createLoc(startToken, endToken) };
|
|
3030
|
-
} else if (this.match(TokenType.COLLATE)) {
|
|
3031
|
-
if (!this.check(TokenType.IDENTIFIER)) {
|
|
3032
|
-
throw this.error(this.peek(), "Expected collation name after COLLATE.");
|
|
3033
|
-
}
|
|
3034
|
-
const collation = this.getIdentifierValue(this.advance());
|
|
3035
|
-
endToken = this.previous();
|
|
3036
|
-
return { type: 'collate', name, collation, loc: _createLoc(startToken, endToken) };
|
|
3037
|
-
} else if (this.match(TokenType.REFERENCES)) {
|
|
3038
|
-
const fkClause = this.foreignKeyClause();
|
|
3039
|
-
endToken = this.previous(); // End token is end of FK clause
|
|
3040
|
-
return { type: 'foreignKey', name, foreignKey: fkClause, loc: _createLoc(startToken, endToken) };
|
|
3041
|
-
} else if (this.match(TokenType.GENERATED)) {
|
|
3042
|
-
this.consume(TokenType.ALWAYS, "Expected ALWAYS after GENERATED.");
|
|
3043
|
-
this.consume(TokenType.AS, "Expected AS after GENERATED ALWAYS.");
|
|
3044
|
-
this.consume(TokenType.LPAREN, "Expected '(' after AS.");
|
|
3045
|
-
const expr = this.expression();
|
|
3046
|
-
this.consume(TokenType.RPAREN, "Expected ')' after generated expression.");
|
|
3047
|
-
endToken = this.previous();
|
|
3048
|
-
let stored = false;
|
|
3049
|
-
if (this.match(TokenType.STORED)) {
|
|
3050
|
-
stored = true;
|
|
3051
|
-
endToken = this.previous();
|
|
3052
|
-
} else if (this.match(TokenType.VIRTUAL)) {
|
|
3053
|
-
endToken = this.previous();
|
|
3054
|
-
}
|
|
3055
|
-
return { type: 'generated', name, generated: { expr, stored }, loc: _createLoc(startToken, endToken) };
|
|
3056
|
-
}
|
|
3057
|
-
|
|
3058
|
-
throw this.error(this.peek(), "Expected column constraint type (PRIMARY KEY, NOT NULL, UNIQUE, CHECK, DEFAULT, COLLATE, REFERENCES, GENERATED).");
|
|
3059
|
-
}
|
|
3060
|
-
|
|
3061
|
-
/** @internal Parses a table constraint */
|
|
3062
|
-
private tableConstraint(): AST.TableConstraint {
|
|
3063
|
-
let name: string | undefined;
|
|
3064
|
-
const startToken = this.peek(); // Capture start token
|
|
3065
|
-
let endToken = startToken; // Initialize end token
|
|
3066
|
-
|
|
3067
|
-
if (this.match(TokenType.CONSTRAINT)) {
|
|
3068
|
-
name = this.consumeIdentifier("Expected constraint name after CONSTRAINT.");
|
|
3069
|
-
endToken = this.previous();
|
|
3070
|
-
}
|
|
3071
|
-
|
|
3072
|
-
if (this.match(TokenType.PRIMARY)) {
|
|
3073
|
-
this.consume(TokenType.KEY, "Expected KEY after PRIMARY.");
|
|
3074
|
-
this.consume(TokenType.LPAREN, "Expected '(' before PRIMARY KEY columns.");
|
|
3075
|
-
|
|
3076
|
-
// Handle empty PRIMARY KEY () for singleton tables (Third Manifesto feature)
|
|
3077
|
-
let columns: { name: string; direction?: 'asc' | 'desc' }[] = [];
|
|
3078
|
-
if (!this.check(TokenType.RPAREN)) {
|
|
3079
|
-
columns = this.identifierListWithDirection();
|
|
3080
|
-
}
|
|
3081
|
-
|
|
3082
|
-
endToken = this.consume(TokenType.RPAREN, "Expected ')' after PRIMARY KEY columns.");
|
|
3083
|
-
const onConflict = this.parseConflictClause();
|
|
3084
|
-
if (onConflict) endToken = this.previous();
|
|
3085
|
-
return { type: 'primaryKey', name, columns, onConflict, loc: _createLoc(startToken, endToken) };
|
|
3086
|
-
} else if (this.match(TokenType.UNIQUE)) {
|
|
3087
|
-
this.consume(TokenType.LPAREN, "Expected '(' before UNIQUE columns.");
|
|
3088
|
-
const columnsSimple = this.identifierList();
|
|
3089
|
-
const columns = columnsSimple.map(name => ({ name }));
|
|
3090
|
-
endToken = this.consume(TokenType.RPAREN, "Expected ')' after UNIQUE columns.");
|
|
3091
|
-
const onConflict = this.parseConflictClause();
|
|
3092
|
-
if (onConflict) endToken = this.previous();
|
|
3093
|
-
return { type: 'unique', name, columns, onConflict, loc: _createLoc(startToken, endToken) };
|
|
3094
|
-
} else if (this.match(TokenType.CHECK)) {
|
|
3095
|
-
// --- Parse optional ON clause before parentheses --- //
|
|
3096
|
-
let operations: RowOp[] | undefined;
|
|
3097
|
-
if (this.matchKeyword('ON')) {
|
|
3098
|
-
operations = this.parseRowOpList();
|
|
3099
|
-
}
|
|
3100
|
-
// --- End Parse ON clause --- //
|
|
3101
|
-
this.consume(TokenType.LPAREN, "Expected '(' after CHECK.");
|
|
3102
|
-
const expr = this.expression();
|
|
3103
|
-
endToken = this.consume(TokenType.RPAREN, "Expected ')' after CHECK expression.");
|
|
3104
|
-
// No DEFERRABLE syntax supported; deferral is auto-detected by the planner
|
|
3105
|
-
return {
|
|
3106
|
-
type: 'check',
|
|
3107
|
-
name,
|
|
3108
|
-
expr,
|
|
3109
|
-
operations,
|
|
3110
|
-
loc: _createLoc(startToken, endToken)
|
|
3111
|
-
};
|
|
3112
|
-
} else if (this.match(TokenType.FOREIGN)) {
|
|
3113
|
-
this.consume(TokenType.KEY, "Expected KEY after FOREIGN.");
|
|
3114
|
-
this.consume(TokenType.LPAREN, "Expected '(' before FOREIGN KEY columns.");
|
|
3115
|
-
const columns = this.identifierList().map(name => ({ name }));
|
|
3116
|
-
this.consume(TokenType.RPAREN, "Expected ')' after FOREIGN KEY columns.");
|
|
3117
|
-
const fkClause = this.foreignKeyClause();
|
|
3118
|
-
endToken = this.previous(); // End token is end of FK clause
|
|
3119
|
-
return { type: 'foreignKey', name, columns, foreignKey: fkClause, loc: _createLoc(startToken, endToken) };
|
|
3120
|
-
}
|
|
3121
|
-
|
|
3122
|
-
throw this.error(this.peek(), "Expected table constraint type (PRIMARY KEY, UNIQUE, CHECK, FOREIGN KEY).");
|
|
3123
|
-
}
|
|
3124
|
-
|
|
3125
|
-
/** @internal Parses a foreign key clause */
|
|
3126
|
-
private foreignKeyClause(): AST.ForeignKeyClause {
|
|
3127
|
-
this.consume(TokenType.REFERENCES, "Expected REFERENCES for foreign key.");
|
|
3128
|
-
const table = this.consumeIdentifier("Expected foreign table name.");
|
|
3129
|
-
let columns: string[] | undefined;
|
|
3130
|
-
if (this.match(TokenType.LPAREN)) {
|
|
3131
|
-
columns = this.identifierList();
|
|
3132
|
-
this.consume(TokenType.RPAREN, "Expected ')' after foreign columns.");
|
|
3133
|
-
}
|
|
3134
|
-
|
|
3135
|
-
let onDelete: AST.ForeignKeyAction | undefined;
|
|
3136
|
-
let onUpdate: AST.ForeignKeyAction | undefined;
|
|
3137
|
-
let deferrable: boolean | undefined;
|
|
3138
|
-
let initiallyDeferred: boolean | undefined;
|
|
3139
|
-
|
|
3140
|
-
while (this.check(TokenType.ON) || this.check(TokenType.DEFERRABLE) || this.check(TokenType.NOT)) {
|
|
3141
|
-
if (this.match(TokenType.ON)) {
|
|
3142
|
-
if (this.match(TokenType.DELETE)) {
|
|
3143
|
-
onDelete = this.parseForeignKeyAction();
|
|
3144
|
-
} else if (this.match(TokenType.UPDATE)) {
|
|
3145
|
-
onUpdate = this.parseForeignKeyAction();
|
|
3146
|
-
} else {
|
|
3147
|
-
throw this.error(this.peek(), "Expected DELETE or UPDATE after ON.");
|
|
3148
|
-
}
|
|
3149
|
-
} else if (this.match(TokenType.DEFERRABLE)) {
|
|
3150
|
-
deferrable = true;
|
|
3151
|
-
if (this.match(TokenType.INITIALLY)) {
|
|
3152
|
-
if (this.match(TokenType.DEFERRED)) {
|
|
3153
|
-
initiallyDeferred = true;
|
|
3154
|
-
} else if (this.match(TokenType.IMMEDIATE)) {
|
|
3155
|
-
initiallyDeferred = false;
|
|
3156
|
-
} else {
|
|
3157
|
-
throw this.error(this.peek(), "Expected DEFERRED or IMMEDIATE after INITIALLY.");
|
|
3158
|
-
}
|
|
3159
|
-
}
|
|
3160
|
-
} else if (this.match(TokenType.NOT)) {
|
|
3161
|
-
this.consume(TokenType.DEFERRABLE, "Expected DEFERRABLE after NOT.");
|
|
3162
|
-
deferrable = false;
|
|
3163
|
-
if (this.match(TokenType.INITIALLY)) {
|
|
3164
|
-
if (this.match(TokenType.DEFERRED)) {
|
|
3165
|
-
initiallyDeferred = true;
|
|
3166
|
-
} else if (this.match(TokenType.IMMEDIATE)) {
|
|
3167
|
-
initiallyDeferred = false;
|
|
3168
|
-
} else {
|
|
3169
|
-
throw this.error(this.peek(), "Expected DEFERRED or IMMEDIATE after INITIALLY.");
|
|
3170
|
-
}
|
|
3171
|
-
}
|
|
3172
|
-
} else {
|
|
3173
|
-
break;
|
|
3174
|
-
}
|
|
3175
|
-
}
|
|
3176
|
-
|
|
3177
|
-
return { table, columns, onDelete, onUpdate, deferrable, initiallyDeferred };
|
|
3178
|
-
}
|
|
3179
|
-
|
|
3180
|
-
/** @internal Parses the ON CONFLICT clause */
|
|
3181
|
-
private parseConflictClause(): ConflictResolution | undefined {
|
|
3182
|
-
if (this.match(TokenType.ON)) {
|
|
3183
|
-
this.consume(TokenType.CONFLICT, "Expected CONFLICT after ON.");
|
|
3184
|
-
if (this.match(TokenType.ROLLBACK)) return ConflictResolution.ROLLBACK;
|
|
3185
|
-
if (this.match(TokenType.ABORT)) return ConflictResolution.ABORT;
|
|
3186
|
-
if (this.match(TokenType.FAIL)) return ConflictResolution.FAIL;
|
|
3187
|
-
if (this.match(TokenType.IGNORE)) return ConflictResolution.IGNORE;
|
|
3188
|
-
if (this.match(TokenType.REPLACE)) return ConflictResolution.REPLACE;
|
|
3189
|
-
throw this.error(this.peek(), "Expected conflict resolution algorithm (ROLLBACK, ABORT, FAIL, IGNORE, REPLACE).");
|
|
3190
|
-
}
|
|
3191
|
-
return undefined;
|
|
3192
|
-
}
|
|
3193
|
-
|
|
3194
|
-
/** @internal Parses the foreign key action */
|
|
3195
|
-
private parseForeignKeyAction(): AST.ForeignKeyAction {
|
|
3196
|
-
if (this.match(TokenType.SET)) {
|
|
3197
|
-
if (this.match(TokenType.NULL)) return 'setNull';
|
|
3198
|
-
if (this.match(TokenType.DEFAULT)) return 'setDefault';
|
|
3199
|
-
throw this.error(this.peek(), "Expected NULL or DEFAULT after SET.");
|
|
3200
|
-
} else if (this.match(TokenType.CASCADE)) {
|
|
3201
|
-
return 'cascade';
|
|
3202
|
-
} else if (this.match(TokenType.RESTRICT)) {
|
|
3203
|
-
return 'restrict';
|
|
3204
|
-
} else if (this.match(TokenType.NO)) {
|
|
3205
|
-
this.consume(TokenType.ACTION, "Expected ACTION after NO.");
|
|
3206
|
-
return 'noAction';
|
|
3207
|
-
}
|
|
3208
|
-
throw this.error(this.peek(), "Expected foreign key action (SET NULL, SET DEFAULT, CASCADE, RESTRICT, NO ACTION).");
|
|
3209
|
-
}
|
|
3210
|
-
|
|
3211
|
-
/** @internal Parses a comma-separated list of identifiers, optionally with ASC/DESC */
|
|
3212
|
-
private identifierList(): string[] {
|
|
3213
|
-
const identifiers: string[] = [];
|
|
3214
|
-
do {
|
|
3215
|
-
identifiers.push(this.consumeIdentifier(['key', 'action', 'set', 'default', 'check', 'unique', 'references', 'on', 'cascade', 'restrict', 'like'], "Expected identifier in list."));
|
|
3216
|
-
} while (this.match(TokenType.COMMA));
|
|
3217
|
-
return identifiers;
|
|
3218
|
-
}
|
|
3219
|
-
|
|
3220
|
-
/** @internal Parses a comma-separated list of identifiers, optionally with ASC/DESC */
|
|
3221
|
-
private identifierListWithDirection(): { name: string; direction?: 'asc' | 'desc' }[] {
|
|
3222
|
-
const identifiers: { name: string; direction?: 'asc' | 'desc' }[] = [];
|
|
3223
|
-
do {
|
|
3224
|
-
const name = this.consumeIdentifier(['key', 'action', 'set', 'default', 'check', 'unique', 'references', 'on', 'cascade', 'restrict', 'like'], "Expected identifier in list.");
|
|
3225
|
-
const direction = this.match(TokenType.ASC) ? 'asc' : this.match(TokenType.DESC) ? 'desc' : undefined;
|
|
3226
|
-
identifiers.push({ name, direction });
|
|
3227
|
-
} while (this.match(TokenType.COMMA));
|
|
3228
|
-
return identifiers;
|
|
3229
|
-
}
|
|
3230
|
-
|
|
3231
|
-
// --- Helper method to peek keywords case-insensitively ---
|
|
3232
|
-
private peekKeyword(keyword: string): boolean {
|
|
3233
|
-
if (this.isAtEnd()) return false;
|
|
3234
|
-
const token = this.peek();
|
|
3235
|
-
|
|
3236
|
-
// The keyword lookup string should be uppercase to match TokenType enum keys (e.g., TokenType.SELECT)
|
|
3237
|
-
const keywordKey = keyword.toUpperCase();
|
|
3238
|
-
const expectedTokenType = TokenType[keywordKey as keyof typeof TokenType];
|
|
3239
|
-
|
|
3240
|
-
// Check if the current token's type is the expected specific keyword TokenType.
|
|
3241
|
-
// This assumes the lexer has already correctly typed true keywords.
|
|
3242
|
-
if (expectedTokenType !== undefined && token.type === expectedTokenType) {
|
|
3243
|
-
return true;
|
|
3244
|
-
}
|
|
3245
|
-
|
|
3246
|
-
// Fallback: if the token is a generic IDENTIFIER, check if its lexeme matches the keyword.
|
|
3247
|
-
// This handles contextual keywords like FIRST, LAST that aren't reserved keywords.
|
|
3248
|
-
if (token.type === TokenType.IDENTIFIER && token.lexeme.toUpperCase() === keywordKey) {
|
|
3249
|
-
return true;
|
|
3250
|
-
}
|
|
3251
|
-
|
|
3252
|
-
return false;
|
|
3253
|
-
}
|
|
3254
|
-
|
|
3255
|
-
// --- Helper method to match keywords case-insensitively ---
|
|
3256
|
-
private matchKeyword(keyword: string): boolean {
|
|
3257
|
-
if (this.isAtEnd()) return false;
|
|
3258
|
-
if (this.peekKeyword(keyword)) {
|
|
3259
|
-
this.advance();
|
|
3260
|
-
return true;
|
|
3261
|
-
}
|
|
3262
|
-
return false;
|
|
3263
|
-
}
|
|
3264
|
-
|
|
3265
|
-
// --- Helper method to consume keywords case-insensitively ---
|
|
3266
|
-
private consumeKeyword(keyword: string, message: string): Token {
|
|
3267
|
-
if (this.peekKeyword(keyword)) {
|
|
3268
|
-
return this.advance();
|
|
3269
|
-
}
|
|
3270
|
-
throw this.error(this.peek(), message);
|
|
3271
|
-
}
|
|
3272
|
-
|
|
3273
|
-
/** Parses the list of operations for CHECK ON */
|
|
3274
|
-
private parseRowOpList(): RowOp[] {
|
|
3275
|
-
const operations: RowOp[] = [];
|
|
3276
|
-
|
|
3277
|
-
// Parse operations in a comma-separated list
|
|
3278
|
-
do {
|
|
3279
|
-
if (this.match(TokenType.INSERT)) {
|
|
3280
|
-
operations.push('insert');
|
|
3281
|
-
} else if (this.match(TokenType.UPDATE)) {
|
|
3282
|
-
operations.push('update');
|
|
3283
|
-
} else if (this.match(TokenType.DELETE)) {
|
|
3284
|
-
operations.push('delete');
|
|
3285
|
-
} else {
|
|
3286
|
-
throw this.error(this.peek(), "Expected INSERT, UPDATE, or DELETE after ON.");
|
|
3287
|
-
}
|
|
3288
|
-
} while (this.match(TokenType.COMMA));
|
|
3289
|
-
|
|
3290
|
-
// Optional: Check for duplicates? The design allows them but ignores them.
|
|
3291
|
-
return operations;
|
|
3292
|
-
}
|
|
3293
|
-
|
|
3294
|
-
/**
|
|
3295
|
-
* Parses a CASE expression
|
|
3296
|
-
* CASE [base_expr] WHEN cond THEN result ... [ELSE else_result] END
|
|
3297
|
-
* CASE WHEN cond THEN result ... [ELSE else_result] END
|
|
3298
|
-
*/
|
|
3299
|
-
private parseCaseExpression(startToken: Token): AST.CaseExpr {
|
|
3300
|
-
let baseExpr: AST.Expression | undefined;
|
|
3301
|
-
const whenThenClauses: AST.CaseExprWhenThenClause[] = [];
|
|
3302
|
-
let elseExpr: AST.Expression | undefined;
|
|
3303
|
-
let endToken = startToken; // Initialize with CASE token
|
|
3304
|
-
|
|
3305
|
-
// Check if it's CASE expr WHEN ... or CASE WHEN ...
|
|
3306
|
-
if (!this.peekKeyword('WHEN')) { // Changed from checkKeyword
|
|
3307
|
-
baseExpr = this.expression();
|
|
3308
|
-
}
|
|
3309
|
-
|
|
3310
|
-
while (this.matchKeyword('WHEN')) {
|
|
3311
|
-
const whenCondition = this.expression();
|
|
3312
|
-
this.consumeKeyword('THEN', "Expected 'THEN' after WHEN condition in CASE expression.");
|
|
3313
|
-
const thenResult = this.expression();
|
|
3314
|
-
whenThenClauses.push({ when: whenCondition, then: thenResult });
|
|
3315
|
-
endToken = this.previous(); // Update endToken to the end of the THEN expression
|
|
3316
|
-
}
|
|
3317
|
-
|
|
3318
|
-
if (whenThenClauses.length === 0) {
|
|
3319
|
-
throw this.error(this.peek(), "CASE expression must have at least one WHEN clause.");
|
|
3320
|
-
}
|
|
3321
|
-
|
|
3322
|
-
if (this.matchKeyword('ELSE')) {
|
|
3323
|
-
elseExpr = this.expression();
|
|
3324
|
-
endToken = this.previous(); // Update endToken to the end of the ELSE expression
|
|
3325
|
-
}
|
|
3326
|
-
|
|
3327
|
-
endToken = this.consumeKeyword('END', "Expected 'END' to terminate CASE expression.");
|
|
3328
|
-
|
|
3329
|
-
return {
|
|
3330
|
-
type: 'case',
|
|
3331
|
-
baseExpr,
|
|
3332
|
-
whenThenClauses,
|
|
3333
|
-
elseExpr,
|
|
3334
|
-
loc: _createLoc(startToken, endToken),
|
|
3335
|
-
};
|
|
3336
|
-
}
|
|
3337
|
-
|
|
3338
|
-
// Helper to check if a token lexeme is a common type name keyword for CAST
|
|
3339
|
-
private isTypeNameKeyword(lexeme: string): boolean {
|
|
3340
|
-
const typeKeywords = ['TEXT', 'INTEGER', 'REAL', 'BLOB', 'NUMERIC', 'VARCHAR', 'CHAR', 'DATE', 'DATETIME', 'BOOLEAN', 'INT'];
|
|
3341
|
-
return typeKeywords.includes(lexeme.toUpperCase());
|
|
3342
|
-
}
|
|
3343
|
-
|
|
3344
|
-
private statementSupportsWithClause(statement: AST.AstNode): boolean {
|
|
3345
|
-
return statement.type === 'select' ||
|
|
3346
|
-
statement.type === 'insert' ||
|
|
3347
|
-
statement.type === 'update' ||
|
|
3348
|
-
statement.type === 'delete';
|
|
3349
|
-
}
|
|
3350
|
-
|
|
3351
|
-
// DEFERRABLE syntax not supported for CHECK constraints in Quereus.
|
|
3352
|
-
}
|