snapshot of https://github.com/sqlc-dev/sqlc.git (main, 2026-09-16; blobs > 131072B omitted)
Some checks failed
kotlin / test (push) Has been skipped
python / test (push) Has been skipped
typescript / test (push) Has been skipped
go / build darwin/amd64 (push) Has been cancelled
go / build linux/amd64 (push) Has been cancelled
go / build windows/amd64 (push) Has been cancelled
go / build darwin/arm64 (push) Has been cancelled
go / build linux/arm64 (push) Has been cancelled
go / build windows/arm64 (push) Has been cancelled
go / test (push) Has been cancelled
go / vuln_check (push) Has been cancelled
Some checks failed
kotlin / test (push) Has been skipped
python / test (push) Has been skipped
typescript / test (push) Has been skipped
go / build darwin/amd64 (push) Has been cancelled
go / build linux/amd64 (push) Has been cancelled
go / build windows/amd64 (push) Has been cancelled
go / build darwin/arm64 (push) Has been cancelled
go / build linux/arm64 (push) Has been cancelled
go / build windows/arm64 (push) Has been cancelled
go / test (push) Has been cancelled
go / vuln_check (push) Has been cancelled
This commit is contained in:
546
internal/analysis/analysis.pb.go
Normal file
546
internal/analysis/analysis.pb.go
Normal file
@@ -0,0 +1,546 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.30.0
|
||||
// protoc (unknown)
|
||||
// source: analysis/analysis.proto
|
||||
|
||||
package analysis
|
||||
|
||||
import (
|
||||
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||
reflect "reflect"
|
||||
sync "sync"
|
||||
)
|
||||
|
||||
const (
|
||||
// Verify that this generated code is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
|
||||
// Verify that runtime/protoimpl is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
|
||||
)
|
||||
|
||||
type Identifier struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Catalog string `protobuf:"bytes,1,opt,name=catalog,proto3" json:"catalog,omitempty"`
|
||||
Schema string `protobuf:"bytes,2,opt,name=schema,proto3" json:"schema,omitempty"`
|
||||
Name string `protobuf:"bytes,3,opt,name=name,proto3" json:"name,omitempty"`
|
||||
}
|
||||
|
||||
func (x *Identifier) Reset() {
|
||||
*x = Identifier{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_analysis_analysis_proto_msgTypes[0]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
}
|
||||
|
||||
func (x *Identifier) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*Identifier) ProtoMessage() {}
|
||||
|
||||
func (x *Identifier) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_analysis_analysis_proto_msgTypes[0]
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use Identifier.ProtoReflect.Descriptor instead.
|
||||
func (*Identifier) Descriptor() ([]byte, []int) {
|
||||
return file_analysis_analysis_proto_rawDescGZIP(), []int{0}
|
||||
}
|
||||
|
||||
func (x *Identifier) GetCatalog() string {
|
||||
if x != nil {
|
||||
return x.Catalog
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *Identifier) GetSchema() string {
|
||||
if x != nil {
|
||||
return x.Schema
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *Identifier) GetName() string {
|
||||
if x != nil {
|
||||
return x.Name
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type Column struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
|
||||
OriginalName string `protobuf:"bytes,2,opt,name=original_name,json=originalName,proto3" json:"original_name,omitempty"`
|
||||
DataType string `protobuf:"bytes,3,opt,name=data_type,json=dataType,proto3" json:"data_type,omitempty"`
|
||||
NotNull bool `protobuf:"varint,4,opt,name=not_null,json=notNull,proto3" json:"not_null,omitempty"`
|
||||
Unsigned bool `protobuf:"varint,5,opt,name=unsigned,proto3" json:"unsigned,omitempty"`
|
||||
IsArray bool `protobuf:"varint,6,opt,name=is_array,json=isArray,proto3" json:"is_array,omitempty"`
|
||||
ArrayDims int32 `protobuf:"varint,7,opt,name=array_dims,json=arrayDims,proto3" json:"array_dims,omitempty"`
|
||||
Comment string `protobuf:"bytes,8,opt,name=comment,proto3" json:"comment,omitempty"`
|
||||
Length int32 `protobuf:"varint,9,opt,name=length,proto3" json:"length,omitempty"` // *int
|
||||
IsNamedParam bool `protobuf:"varint,10,opt,name=is_named_param,json=isNamedParam,proto3" json:"is_named_param,omitempty"`
|
||||
IsFuncCall bool `protobuf:"varint,11,opt,name=is_func_call,json=isFuncCall,proto3" json:"is_func_call,omitempty"`
|
||||
Scope string `protobuf:"bytes,12,opt,name=scope,proto3" json:"scope,omitempty"`
|
||||
Table *Identifier `protobuf:"bytes,13,opt,name=table,proto3" json:"table,omitempty"`
|
||||
TableAlias string `protobuf:"bytes,14,opt,name=table_alias,json=tableAlias,proto3" json:"table_alias,omitempty"`
|
||||
Type *Identifier `protobuf:"bytes,15,opt,name=type,proto3" json:"type,omitempty"`
|
||||
EmbedTable *Identifier `protobuf:"bytes,16,opt,name=embed_table,json=embedTable,proto3" json:"embed_table,omitempty"`
|
||||
IsSqlcSlice bool `protobuf:"varint,17,opt,name=is_sqlc_slice,json=isSqlcSlice,proto3" json:"is_sqlc_slice,omitempty"`
|
||||
}
|
||||
|
||||
func (x *Column) Reset() {
|
||||
*x = Column{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_analysis_analysis_proto_msgTypes[1]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
}
|
||||
|
||||
func (x *Column) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*Column) ProtoMessage() {}
|
||||
|
||||
func (x *Column) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_analysis_analysis_proto_msgTypes[1]
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use Column.ProtoReflect.Descriptor instead.
|
||||
func (*Column) Descriptor() ([]byte, []int) {
|
||||
return file_analysis_analysis_proto_rawDescGZIP(), []int{1}
|
||||
}
|
||||
|
||||
func (x *Column) GetName() string {
|
||||
if x != nil {
|
||||
return x.Name
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *Column) GetOriginalName() string {
|
||||
if x != nil {
|
||||
return x.OriginalName
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *Column) GetDataType() string {
|
||||
if x != nil {
|
||||
return x.DataType
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *Column) GetNotNull() bool {
|
||||
if x != nil {
|
||||
return x.NotNull
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (x *Column) GetUnsigned() bool {
|
||||
if x != nil {
|
||||
return x.Unsigned
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (x *Column) GetIsArray() bool {
|
||||
if x != nil {
|
||||
return x.IsArray
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (x *Column) GetArrayDims() int32 {
|
||||
if x != nil {
|
||||
return x.ArrayDims
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *Column) GetComment() string {
|
||||
if x != nil {
|
||||
return x.Comment
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *Column) GetLength() int32 {
|
||||
if x != nil {
|
||||
return x.Length
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *Column) GetIsNamedParam() bool {
|
||||
if x != nil {
|
||||
return x.IsNamedParam
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (x *Column) GetIsFuncCall() bool {
|
||||
if x != nil {
|
||||
return x.IsFuncCall
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (x *Column) GetScope() string {
|
||||
if x != nil {
|
||||
return x.Scope
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *Column) GetTable() *Identifier {
|
||||
if x != nil {
|
||||
return x.Table
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *Column) GetTableAlias() string {
|
||||
if x != nil {
|
||||
return x.TableAlias
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *Column) GetType() *Identifier {
|
||||
if x != nil {
|
||||
return x.Type
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *Column) GetEmbedTable() *Identifier {
|
||||
if x != nil {
|
||||
return x.EmbedTable
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *Column) GetIsSqlcSlice() bool {
|
||||
if x != nil {
|
||||
return x.IsSqlcSlice
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type Parameter struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Number int32 `protobuf:"varint,1,opt,name=number,proto3" json:"number,omitempty"`
|
||||
Column *Column `protobuf:"bytes,2,opt,name=column,proto3" json:"column,omitempty"`
|
||||
}
|
||||
|
||||
func (x *Parameter) Reset() {
|
||||
*x = Parameter{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_analysis_analysis_proto_msgTypes[2]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
}
|
||||
|
||||
func (x *Parameter) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*Parameter) ProtoMessage() {}
|
||||
|
||||
func (x *Parameter) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_analysis_analysis_proto_msgTypes[2]
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use Parameter.ProtoReflect.Descriptor instead.
|
||||
func (*Parameter) Descriptor() ([]byte, []int) {
|
||||
return file_analysis_analysis_proto_rawDescGZIP(), []int{2}
|
||||
}
|
||||
|
||||
func (x *Parameter) GetNumber() int32 {
|
||||
if x != nil {
|
||||
return x.Number
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *Parameter) GetColumn() *Column {
|
||||
if x != nil {
|
||||
return x.Column
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type Analysis struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Columns []*Column `protobuf:"bytes,1,rep,name=columns,proto3" json:"columns,omitempty"`
|
||||
Params []*Parameter `protobuf:"bytes,2,rep,name=params,proto3" json:"params,omitempty"`
|
||||
}
|
||||
|
||||
func (x *Analysis) Reset() {
|
||||
*x = Analysis{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_analysis_analysis_proto_msgTypes[3]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
}
|
||||
|
||||
func (x *Analysis) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*Analysis) ProtoMessage() {}
|
||||
|
||||
func (x *Analysis) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_analysis_analysis_proto_msgTypes[3]
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use Analysis.ProtoReflect.Descriptor instead.
|
||||
func (*Analysis) Descriptor() ([]byte, []int) {
|
||||
return file_analysis_analysis_proto_rawDescGZIP(), []int{3}
|
||||
}
|
||||
|
||||
func (x *Analysis) GetColumns() []*Column {
|
||||
if x != nil {
|
||||
return x.Columns
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *Analysis) GetParams() []*Parameter {
|
||||
if x != nil {
|
||||
return x.Params
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var File_analysis_analysis_proto protoreflect.FileDescriptor
|
||||
|
||||
var file_analysis_analysis_proto_rawDesc = []byte{
|
||||
0x0a, 0x17, 0x61, 0x6e, 0x61, 0x6c, 0x79, 0x73, 0x69, 0x73, 0x2f, 0x61, 0x6e, 0x61, 0x6c, 0x79,
|
||||
0x73, 0x69, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x08, 0x61, 0x6e, 0x61, 0x6c, 0x79,
|
||||
0x73, 0x69, 0x73, 0x22, 0x52, 0x0a, 0x0a, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65,
|
||||
0x72, 0x12, 0x18, 0x0a, 0x07, 0x63, 0x61, 0x74, 0x61, 0x6c, 0x6f, 0x67, 0x18, 0x01, 0x20, 0x01,
|
||||
0x28, 0x09, 0x52, 0x07, 0x63, 0x61, 0x74, 0x61, 0x6c, 0x6f, 0x67, 0x12, 0x16, 0x0a, 0x06, 0x73,
|
||||
0x63, 0x68, 0x65, 0x6d, 0x61, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x63, 0x68,
|
||||
0x65, 0x6d, 0x61, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28,
|
||||
0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0xb1, 0x04, 0x0a, 0x06, 0x43, 0x6f, 0x6c, 0x75,
|
||||
0x6d, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09,
|
||||
0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x23, 0x0a, 0x0d, 0x6f, 0x72, 0x69, 0x67, 0x69, 0x6e,
|
||||
0x61, 0x6c, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x6f,
|
||||
0x72, 0x69, 0x67, 0x69, 0x6e, 0x61, 0x6c, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x64,
|
||||
0x61, 0x74, 0x61, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08,
|
||||
0x64, 0x61, 0x74, 0x61, 0x54, 0x79, 0x70, 0x65, 0x12, 0x19, 0x0a, 0x08, 0x6e, 0x6f, 0x74, 0x5f,
|
||||
0x6e, 0x75, 0x6c, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x6e, 0x6f, 0x74, 0x4e,
|
||||
0x75, 0x6c, 0x6c, 0x12, 0x1a, 0x0a, 0x08, 0x75, 0x6e, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x18,
|
||||
0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x75, 0x6e, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x12,
|
||||
0x19, 0x0a, 0x08, 0x69, 0x73, 0x5f, 0x61, 0x72, 0x72, 0x61, 0x79, 0x18, 0x06, 0x20, 0x01, 0x28,
|
||||
0x08, 0x52, 0x07, 0x69, 0x73, 0x41, 0x72, 0x72, 0x61, 0x79, 0x12, 0x1d, 0x0a, 0x0a, 0x61, 0x72,
|
||||
0x72, 0x61, 0x79, 0x5f, 0x64, 0x69, 0x6d, 0x73, 0x18, 0x07, 0x20, 0x01, 0x28, 0x05, 0x52, 0x09,
|
||||
0x61, 0x72, 0x72, 0x61, 0x79, 0x44, 0x69, 0x6d, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x63, 0x6f, 0x6d,
|
||||
0x6d, 0x65, 0x6e, 0x74, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x63, 0x6f, 0x6d, 0x6d,
|
||||
0x65, 0x6e, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x6c, 0x65, 0x6e, 0x67, 0x74, 0x68, 0x18, 0x09, 0x20,
|
||||
0x01, 0x28, 0x05, 0x52, 0x06, 0x6c, 0x65, 0x6e, 0x67, 0x74, 0x68, 0x12, 0x24, 0x0a, 0x0e, 0x69,
|
||||
0x73, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x64, 0x5f, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x18, 0x0a, 0x20,
|
||||
0x01, 0x28, 0x08, 0x52, 0x0c, 0x69, 0x73, 0x4e, 0x61, 0x6d, 0x65, 0x64, 0x50, 0x61, 0x72, 0x61,
|
||||
0x6d, 0x12, 0x20, 0x0a, 0x0c, 0x69, 0x73, 0x5f, 0x66, 0x75, 0x6e, 0x63, 0x5f, 0x63, 0x61, 0x6c,
|
||||
0x6c, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x69, 0x73, 0x46, 0x75, 0x6e, 0x63, 0x43,
|
||||
0x61, 0x6c, 0x6c, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x18, 0x0c, 0x20, 0x01,
|
||||
0x28, 0x09, 0x52, 0x05, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x12, 0x2a, 0x0a, 0x05, 0x74, 0x61, 0x62,
|
||||
0x6c, 0x65, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x61, 0x6e, 0x61, 0x6c, 0x79,
|
||||
0x73, 0x69, 0x73, 0x2e, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x52, 0x05,
|
||||
0x74, 0x61, 0x62, 0x6c, 0x65, 0x12, 0x1f, 0x0a, 0x0b, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x61,
|
||||
0x6c, 0x69, 0x61, 0x73, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x74, 0x61, 0x62, 0x6c,
|
||||
0x65, 0x41, 0x6c, 0x69, 0x61, 0x73, 0x12, 0x28, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x0f,
|
||||
0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x61, 0x6e, 0x61, 0x6c, 0x79, 0x73, 0x69, 0x73, 0x2e,
|
||||
0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65,
|
||||
0x12, 0x35, 0x0a, 0x0b, 0x65, 0x6d, 0x62, 0x65, 0x64, 0x5f, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x18,
|
||||
0x10, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x61, 0x6e, 0x61, 0x6c, 0x79, 0x73, 0x69, 0x73,
|
||||
0x2e, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x52, 0x0a, 0x65, 0x6d, 0x62,
|
||||
0x65, 0x64, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x12, 0x22, 0x0a, 0x0d, 0x69, 0x73, 0x5f, 0x73, 0x71,
|
||||
0x6c, 0x63, 0x5f, 0x73, 0x6c, 0x69, 0x63, 0x65, 0x18, 0x11, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0b,
|
||||
0x69, 0x73, 0x53, 0x71, 0x6c, 0x63, 0x53, 0x6c, 0x69, 0x63, 0x65, 0x22, 0x4d, 0x0a, 0x09, 0x50,
|
||||
0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x12, 0x16, 0x0a, 0x06, 0x6e, 0x75, 0x6d, 0x62,
|
||||
0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x06, 0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72,
|
||||
0x12, 0x28, 0x0a, 0x06, 0x63, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b,
|
||||
0x32, 0x10, 0x2e, 0x61, 0x6e, 0x61, 0x6c, 0x79, 0x73, 0x69, 0x73, 0x2e, 0x43, 0x6f, 0x6c, 0x75,
|
||||
0x6d, 0x6e, 0x52, 0x06, 0x63, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x22, 0x63, 0x0a, 0x08, 0x41, 0x6e,
|
||||
0x61, 0x6c, 0x79, 0x73, 0x69, 0x73, 0x12, 0x2a, 0x0a, 0x07, 0x63, 0x6f, 0x6c, 0x75, 0x6d, 0x6e,
|
||||
0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x61, 0x6e, 0x61, 0x6c, 0x79, 0x73,
|
||||
0x69, 0x73, 0x2e, 0x43, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x52, 0x07, 0x63, 0x6f, 0x6c, 0x75, 0x6d,
|
||||
0x6e, 0x73, 0x12, 0x2b, 0x0a, 0x06, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x18, 0x02, 0x20, 0x03,
|
||||
0x28, 0x0b, 0x32, 0x13, 0x2e, 0x61, 0x6e, 0x61, 0x6c, 0x79, 0x73, 0x69, 0x73, 0x2e, 0x50, 0x61,
|
||||
0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x52, 0x06, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x42,
|
||||
0x89, 0x01, 0x0a, 0x0c, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x6e, 0x61, 0x6c, 0x79, 0x73, 0x69, 0x73,
|
||||
0x42, 0x0d, 0x41, 0x6e, 0x61, 0x6c, 0x79, 0x73, 0x69, 0x73, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50,
|
||||
0x01, 0x5a, 0x2a, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x73, 0x71,
|
||||
0x6c, 0x63, 0x2d, 0x64, 0x65, 0x76, 0x2f, 0x73, 0x71, 0x6c, 0x63, 0x2f, 0x69, 0x6e, 0x74, 0x65,
|
||||
0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x61, 0x6e, 0x61, 0x6c, 0x79, 0x73, 0x69, 0x73, 0xa2, 0x02, 0x03,
|
||||
0x41, 0x58, 0x58, 0xaa, 0x02, 0x08, 0x41, 0x6e, 0x61, 0x6c, 0x79, 0x73, 0x69, 0x73, 0xca, 0x02,
|
||||
0x08, 0x41, 0x6e, 0x61, 0x6c, 0x79, 0x73, 0x69, 0x73, 0xe2, 0x02, 0x14, 0x41, 0x6e, 0x61, 0x6c,
|
||||
0x79, 0x73, 0x69, 0x73, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61,
|
||||
0xea, 0x02, 0x08, 0x41, 0x6e, 0x61, 0x6c, 0x79, 0x73, 0x69, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f,
|
||||
0x74, 0x6f, 0x33,
|
||||
}
|
||||
|
||||
var (
|
||||
file_analysis_analysis_proto_rawDescOnce sync.Once
|
||||
file_analysis_analysis_proto_rawDescData = file_analysis_analysis_proto_rawDesc
|
||||
)
|
||||
|
||||
func file_analysis_analysis_proto_rawDescGZIP() []byte {
|
||||
file_analysis_analysis_proto_rawDescOnce.Do(func() {
|
||||
file_analysis_analysis_proto_rawDescData = protoimpl.X.CompressGZIP(file_analysis_analysis_proto_rawDescData)
|
||||
})
|
||||
return file_analysis_analysis_proto_rawDescData
|
||||
}
|
||||
|
||||
var file_analysis_analysis_proto_msgTypes = make([]protoimpl.MessageInfo, 4)
|
||||
var file_analysis_analysis_proto_goTypes = []interface{}{
|
||||
(*Identifier)(nil), // 0: analysis.Identifier
|
||||
(*Column)(nil), // 1: analysis.Column
|
||||
(*Parameter)(nil), // 2: analysis.Parameter
|
||||
(*Analysis)(nil), // 3: analysis.Analysis
|
||||
}
|
||||
var file_analysis_analysis_proto_depIdxs = []int32{
|
||||
0, // 0: analysis.Column.table:type_name -> analysis.Identifier
|
||||
0, // 1: analysis.Column.type:type_name -> analysis.Identifier
|
||||
0, // 2: analysis.Column.embed_table:type_name -> analysis.Identifier
|
||||
1, // 3: analysis.Parameter.column:type_name -> analysis.Column
|
||||
1, // 4: analysis.Analysis.columns:type_name -> analysis.Column
|
||||
2, // 5: analysis.Analysis.params:type_name -> analysis.Parameter
|
||||
6, // [6:6] is the sub-list for method output_type
|
||||
6, // [6:6] is the sub-list for method input_type
|
||||
6, // [6:6] is the sub-list for extension type_name
|
||||
6, // [6:6] is the sub-list for extension extendee
|
||||
0, // [0:6] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_analysis_analysis_proto_init() }
|
||||
func file_analysis_analysis_proto_init() {
|
||||
if File_analysis_analysis_proto != nil {
|
||||
return
|
||||
}
|
||||
if !protoimpl.UnsafeEnabled {
|
||||
file_analysis_analysis_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
|
||||
switch v := v.(*Identifier); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
case 1:
|
||||
return &v.sizeCache
|
||||
case 2:
|
||||
return &v.unknownFields
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
file_analysis_analysis_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
|
||||
switch v := v.(*Column); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
case 1:
|
||||
return &v.sizeCache
|
||||
case 2:
|
||||
return &v.unknownFields
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
file_analysis_analysis_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
|
||||
switch v := v.(*Parameter); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
case 1:
|
||||
return &v.sizeCache
|
||||
case 2:
|
||||
return &v.unknownFields
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
file_analysis_analysis_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} {
|
||||
switch v := v.(*Analysis); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
case 1:
|
||||
return &v.sizeCache
|
||||
case 2:
|
||||
return &v.unknownFields
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
type x struct{}
|
||||
out := protoimpl.TypeBuilder{
|
||||
File: protoimpl.DescBuilder{
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: file_analysis_analysis_proto_rawDesc,
|
||||
NumEnums: 0,
|
||||
NumMessages: 4,
|
||||
NumExtensions: 0,
|
||||
NumServices: 0,
|
||||
},
|
||||
GoTypes: file_analysis_analysis_proto_goTypes,
|
||||
DependencyIndexes: file_analysis_analysis_proto_depIdxs,
|
||||
MessageInfos: file_analysis_analysis_proto_msgTypes,
|
||||
}.Build()
|
||||
File_analysis_analysis_proto = out.File
|
||||
file_analysis_analysis_proto_rawDesc = nil
|
||||
file_analysis_analysis_proto_goTypes = nil
|
||||
file_analysis_analysis_proto_depIdxs = nil
|
||||
}
|
||||
2078
internal/analysis/analysis_vtproto.pb.go
Normal file
2078
internal/analysis/analysis_vtproto.pb.go
Normal file
File diff suppressed because it is too large
Load Diff
131
internal/analyzer/analyzer.go
Normal file
131
internal/analyzer/analyzer.go
Normal file
@@ -0,0 +1,131 @@
|
||||
package analyzer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"log/slog"
|
||||
|
||||
"google.golang.org/protobuf/proto"
|
||||
|
||||
"github.com/sqlc-dev/sqlc/internal/analysis"
|
||||
"github.com/sqlc-dev/sqlc/internal/cache"
|
||||
"github.com/sqlc-dev/sqlc/internal/config"
|
||||
"github.com/sqlc-dev/sqlc/internal/sql/ast"
|
||||
"github.com/sqlc-dev/sqlc/internal/sql/named"
|
||||
)
|
||||
|
||||
type CachedAnalyzer struct {
|
||||
a Analyzer
|
||||
config config.Config
|
||||
configBytes []byte
|
||||
db config.Database
|
||||
store *cache.Cache
|
||||
}
|
||||
|
||||
func Cached(a Analyzer, c config.Config, db config.Database) *CachedAnalyzer {
|
||||
return &CachedAnalyzer{
|
||||
a: a,
|
||||
config: c,
|
||||
db: db,
|
||||
}
|
||||
}
|
||||
|
||||
// Create a new error here
|
||||
|
||||
func (c *CachedAnalyzer) Analyze(ctx context.Context, n ast.Node, q string, schema []string, np *named.ParamSet) (*analysis.Analysis, error) {
|
||||
result, rerun, err := c.analyze(ctx, n, q, schema, np)
|
||||
if rerun {
|
||||
if err != nil {
|
||||
slog.Warn("first analysis failed with error", "err", err)
|
||||
}
|
||||
return c.a.Analyze(ctx, n, q, schema, np)
|
||||
}
|
||||
return result, err
|
||||
}
|
||||
|
||||
// The name of the sole output blob a QueryAnalysis action produces.
|
||||
const analysisOutput = "analysis.pb"
|
||||
|
||||
func (c *CachedAnalyzer) analyze(ctx context.Context, n ast.Node, q string, schema []string, np *named.ParamSet) (*analysis.Analysis, bool, error) {
|
||||
// Only cache queries for managed databases. We can't be certain the
|
||||
// database is in an unchanged state otherwise
|
||||
if !c.db.Managed {
|
||||
return nil, true, nil
|
||||
}
|
||||
|
||||
if c.store == nil {
|
||||
var err error
|
||||
c.store, err = cache.Open()
|
||||
if err != nil {
|
||||
return nil, true, err
|
||||
}
|
||||
}
|
||||
store := c.store
|
||||
|
||||
if c.configBytes == nil {
|
||||
var err error
|
||||
c.configBytes, err = json.Marshal(c.config)
|
||||
if err != nil {
|
||||
return nil, true, err
|
||||
}
|
||||
}
|
||||
|
||||
// Analyzing a query is an action whose inputs are the configuration, the
|
||||
// schema migrations, and the query itself. (The sqlc binary is an
|
||||
// implicit input of every action.)
|
||||
action := store.NewAction("QueryAnalysis").
|
||||
AddInput("config", c.configBytes)
|
||||
for _, m := range schema {
|
||||
action.AddInput("schema", []byte(m))
|
||||
}
|
||||
actionDigest := action.AddInput("query", []byte(q)).Digest()
|
||||
|
||||
if cached, err := store.Actions.Get(actionDigest); err == nil {
|
||||
contents, err := store.CAS.Get(cached.Outputs[analysisOutput])
|
||||
if err == nil {
|
||||
var a analysis.Analysis
|
||||
if err := proto.Unmarshal(contents, &a); err == nil {
|
||||
return &a, false, nil
|
||||
}
|
||||
}
|
||||
if !errors.Is(err, cache.ErrNotFound) {
|
||||
slog.Warn("reading analysis from cache failed", "err", err)
|
||||
}
|
||||
}
|
||||
|
||||
result, err := c.a.Analyze(ctx, n, q, schema, np)
|
||||
|
||||
if err == nil {
|
||||
contents, err := proto.Marshal(result)
|
||||
if err != nil {
|
||||
slog.Warn("unable to marshal analysis", "err", err)
|
||||
return result, false, nil
|
||||
}
|
||||
outDigest, err := store.CAS.Put(contents)
|
||||
if err != nil {
|
||||
slog.Warn("saving analysis to cache failed", "err", err)
|
||||
return result, false, nil
|
||||
}
|
||||
err = store.Actions.Put(actionDigest, &cache.ActionResult{
|
||||
Outputs: map[string]cache.Digest{analysisOutput: outDigest},
|
||||
})
|
||||
if err != nil {
|
||||
slog.Warn("saving analysis action result failed", "err", err)
|
||||
}
|
||||
}
|
||||
|
||||
return result, false, err
|
||||
}
|
||||
|
||||
func (c *CachedAnalyzer) Close(ctx context.Context) error {
|
||||
if c.store != nil {
|
||||
c.store.Close()
|
||||
}
|
||||
return c.a.Close(ctx)
|
||||
}
|
||||
|
||||
type Analyzer interface {
|
||||
Analyze(context.Context, ast.Node, string, []string, *named.ParamSet) (*analysis.Analysis, error)
|
||||
Close(context.Context) error
|
||||
}
|
||||
40
internal/bundler/multipart.go
Normal file
40
internal/bundler/multipart.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package bundler
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
pb "github.com/sqlc-dev/sqlc/internal/quickdb/v1"
|
||||
"github.com/sqlc-dev/sqlc/internal/sql/sqlpath"
|
||||
)
|
||||
|
||||
func readFiles(dir string, paths []string) ([]*pb.File, error) {
|
||||
files, err := sqlpath.Glob(paths)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var out []*pb.File
|
||||
for _, file := range files {
|
||||
f, err := readFile(dir, file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, f)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func readFile(dir string, path string) (*pb.File, error) {
|
||||
rel, err := filepath.Rel(dir, path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
blob, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &pb.File{
|
||||
Name: rel,
|
||||
Contents: blob,
|
||||
}, nil
|
||||
}
|
||||
164
internal/bundler/upload.go
Normal file
164
internal/bundler/upload.go
Normal file
@@ -0,0 +1,164 @@
|
||||
package bundler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"google.golang.org/protobuf/proto"
|
||||
|
||||
"github.com/sqlc-dev/sqlc/internal/config"
|
||||
"github.com/sqlc-dev/sqlc/internal/info"
|
||||
"github.com/sqlc-dev/sqlc/internal/plugin"
|
||||
"github.com/sqlc-dev/sqlc/internal/quickdb"
|
||||
pb "github.com/sqlc-dev/sqlc/internal/quickdb/v1"
|
||||
)
|
||||
|
||||
var ErrNoProject = errors.New(`project uploads require a cloud project
|
||||
|
||||
If you don't have a project, you can create one from the sqlc Cloud
|
||||
dashboard at https://dashboard.sqlc.dev/. If you have a project, ensure
|
||||
you've set its id as the value of the "project" field within the "cloud"
|
||||
section of your sqlc configuration. The id will look similar to
|
||||
"01HA8TWGMYPHK0V2GGMB3R2TP9".`)
|
||||
var ErrNoAuthToken = errors.New(`project uploads require an auth token
|
||||
|
||||
If you don't have an auth token, you can create one from the sqlc Cloud
|
||||
dashboard at https://dashboard.sqlc.dev/. If you have an auth token, ensure
|
||||
you've set it as the value of the SQLC_AUTH_TOKEN environment variable.`)
|
||||
|
||||
type Uploader struct {
|
||||
configPath string
|
||||
config *config.Config
|
||||
dir string
|
||||
client pb.QuickClient
|
||||
}
|
||||
|
||||
type QuerySetArchive struct {
|
||||
Name string
|
||||
Queries []string
|
||||
Schema []string
|
||||
Request *plugin.GenerateRequest
|
||||
}
|
||||
|
||||
func NewUploader(configPath, dir string, conf *config.Config) *Uploader {
|
||||
return &Uploader{
|
||||
configPath: configPath,
|
||||
config: conf,
|
||||
dir: dir,
|
||||
}
|
||||
}
|
||||
|
||||
func (up *Uploader) Validate() error {
|
||||
if up.config.Cloud.Project == "" {
|
||||
return ErrNoProject
|
||||
}
|
||||
if up.config.Cloud.AuthToken == "" {
|
||||
return ErrNoAuthToken
|
||||
}
|
||||
if up.client == nil {
|
||||
client, err := quickdb.NewClientFromConfig(up.config.Cloud)
|
||||
if err != nil {
|
||||
return fmt.Errorf("client init failed: %w", err)
|
||||
}
|
||||
up.client = client
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var envvars = []string{
|
||||
"GITHUB_REPOSITORY",
|
||||
"GITHUB_REF",
|
||||
"GITHUB_REF_NAME",
|
||||
"GITHUB_REF_TYPE",
|
||||
"GITHUB_SHA",
|
||||
}
|
||||
|
||||
func annotate() map[string]string {
|
||||
labels := map[string]string{}
|
||||
for _, ev := range envvars {
|
||||
key := strings.ReplaceAll(strings.ToLower(ev), "_", ".")
|
||||
labels[key] = os.Getenv(ev)
|
||||
}
|
||||
return labels
|
||||
}
|
||||
|
||||
func BuildRequest(ctx context.Context, dir, configPath string, results []*QuerySetArchive, tags []string) (*pb.UploadArchiveRequest, error) {
|
||||
conf, err := readFile(dir, configPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res := &pb.UploadArchiveRequest{
|
||||
SqlcVersion: info.Version,
|
||||
Config: conf,
|
||||
Tags: tags,
|
||||
Annotations: annotate(),
|
||||
}
|
||||
for i, result := range results {
|
||||
schema, err := readFiles(dir, result.Schema)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
queries, err := readFiles(dir, result.Queries)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
name := result.Name
|
||||
if name == "" {
|
||||
name = fmt.Sprintf("queryset_%d", i)
|
||||
}
|
||||
genreq, err := proto.Marshal(result.Request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res.QuerySets = append(res.QuerySets, &pb.QuerySet{
|
||||
Name: name,
|
||||
Schema: schema,
|
||||
Queries: queries,
|
||||
CodegenRequest: &pb.File{
|
||||
Name: "codegen_request.pb",
|
||||
Contents: genreq,
|
||||
},
|
||||
})
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (up *Uploader) buildRequest(ctx context.Context, results []*QuerySetArchive, tags []string) (*pb.UploadArchiveRequest, error) {
|
||||
return BuildRequest(ctx, up.dir, up.configPath, results, tags)
|
||||
}
|
||||
|
||||
func (up *Uploader) DumpRequestOut(ctx context.Context, result []*QuerySetArchive) error {
|
||||
req, err := up.buildRequest(ctx, result, []string{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
slog.Info("config", "file", req.Config.Name, "bytes", len(req.Config.Contents))
|
||||
for _, qs := range req.QuerySets {
|
||||
slog.Info("codegen_request", "queryset", qs.Name, "file", "codegen_request.pb")
|
||||
for _, file := range qs.Schema {
|
||||
slog.Info("schema", "queryset", qs.Name, "file", file.Name, "bytes", len(file.Contents))
|
||||
}
|
||||
for _, file := range qs.Queries {
|
||||
slog.Info("query", "queryset", qs.Name, "file", file.Name, "bytes", len(file.Contents))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (up *Uploader) Upload(ctx context.Context, result []*QuerySetArchive, tags []string) error {
|
||||
if err := up.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
req, err := up.buildRequest(ctx, result, tags)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := up.client.UploadArchive(ctx, req); err != nil {
|
||||
return fmt.Errorf("upload error: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
209
internal/cache/action.go
vendored
Normal file
209
internal/cache/action.go
vendored
Normal file
@@ -0,0 +1,209 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// ActionResult records the outputs of a completed action, mirroring Bazel's
|
||||
// ActionResult message. Outputs are not stored inline: each named output is a
|
||||
// digest pointing into the CAS.
|
||||
type ActionResult struct {
|
||||
// Outputs maps an output name (e.g. "analysis.pb", "plugin.wasm") to the
|
||||
// CAS digest of its contents.
|
||||
Outputs map[string]Digest `json:"outputs"`
|
||||
}
|
||||
|
||||
// ActionCache maps action digests to ActionResults, stored as JSON files at
|
||||
// ac/<xx>/<hash> under the cache root. Unlike CAS entries, action cache
|
||||
// entries are not self-validating — the value is not derivable from the key —
|
||||
// so Get additionally checks that every referenced output still exists in the
|
||||
// CAS before reporting a hit, exactly like Bazel's disk cache does.
|
||||
//
|
||||
// Entry I/O goes through the same os.Root as the CAS, confining every read
|
||||
// and write to the cache directory.
|
||||
type ActionCache struct {
|
||||
root *os.Root
|
||||
cas *CAS
|
||||
}
|
||||
|
||||
func newActionCache(root *os.Root, cas *CAS) *ActionCache {
|
||||
return &ActionCache{root: root, cas: cas}
|
||||
}
|
||||
|
||||
// path returns an entry's path relative to the cache root.
|
||||
func (a *ActionCache) path(d Digest) string {
|
||||
return filepath.Join("ac", d.Hash[:2], d.Hash)
|
||||
}
|
||||
|
||||
// Get returns the cached result for an action, or ErrNotFound on a miss. An
|
||||
// entry whose outputs are missing or corrupt in the CAS is treated as a miss
|
||||
// and evicted.
|
||||
func (a *ActionCache) Get(action Digest) (*ActionResult, error) {
|
||||
if !action.valid() {
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
path := a.path(action)
|
||||
data, err := a.root.ReadFile(path)
|
||||
if err != nil {
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
return nil, fmt.Errorf("cache: %w", err)
|
||||
}
|
||||
var result ActionResult
|
||||
if err := json.Unmarshal(data, &result); err != nil {
|
||||
a.root.Remove(path)
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
for _, d := range result.Outputs {
|
||||
if !a.cas.Contains(d) {
|
||||
a.root.Remove(path)
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// PutTree stores every file under dir in the CAS and records them as the
|
||||
// action's outputs, named by their paths relative to dir. Use this for
|
||||
// actions whose tool writes an output directory, like WASM compilation.
|
||||
func (a *ActionCache) PutTree(action Digest, dir string) error {
|
||||
outputs := map[string]Digest{}
|
||||
err := filepath.WalkDir(dir, func(path string, entry fs.DirEntry, err error) error {
|
||||
if err != nil || entry.IsDir() {
|
||||
return err
|
||||
}
|
||||
rel, err := filepath.Rel(dir, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
d, err := a.cas.Put(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
outputs[filepath.ToSlash(rel)] = d
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("cache: %w", err)
|
||||
}
|
||||
if len(outputs) == 0 {
|
||||
return fmt.Errorf("cache: no outputs found under %s", dir)
|
||||
}
|
||||
return a.Put(action, &ActionResult{Outputs: outputs})
|
||||
}
|
||||
|
||||
// GetTree materializes a cached action's outputs as files under dir, or
|
||||
// returns ErrNotFound on a miss. A file already present is reused only if its
|
||||
// contents still hash to the expected digest; otherwise it is rewritten from
|
||||
// the CAS. Writes are staged, fsynced, and renamed, so a crash cannot leave a
|
||||
// right-sized but torn file that later reads would trust.
|
||||
func (a *ActionCache) GetTree(action Digest, dir string) error {
|
||||
result, err := a.Get(action)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for rel, d := range result.Outputs {
|
||||
// Output names come from the action cache entry, which — unlike a CAS
|
||||
// blob — is not self-validating, so a tampered entry could carry a
|
||||
// name like "../../etc/x". Reject anything that isn't a relative path
|
||||
// confined to dir; this is the confinement the os.Root gives the rest
|
||||
// of the cache, which GetTree can't use because dir is a caller-owned
|
||||
// path a tool must read by absolute name.
|
||||
if !filepath.IsLocal(rel) {
|
||||
return fmt.Errorf("cache: unsafe output path %q in action %s", rel, action)
|
||||
}
|
||||
path := filepath.Join(dir, filepath.FromSlash(rel))
|
||||
// Trust an existing file only if it still hashes to the digest;
|
||||
// size alone can mask in-place corruption that would make the
|
||||
// consuming tool hard-fail with no way to repair (see wazero).
|
||||
if existing, err := os.ReadFile(path); err == nil && DigestOf(existing) == d {
|
||||
continue
|
||||
}
|
||||
data, err := a.cas.Get(d)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
|
||||
return fmt.Errorf("cache: %w", err)
|
||||
}
|
||||
if err := writeFileAtomic(path, data); err != nil {
|
||||
return fmt.Errorf("cache: %w", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// writeFileAtomic writes data to path via a staged temp file in the same
|
||||
// directory, fsynced before an atomic rename, so a reader never observes a
|
||||
// partial or torn file even across a crash.
|
||||
func writeFileAtomic(path string, data []byte) error {
|
||||
f, err := os.CreateTemp(filepath.Dir(path), "."+filepath.Base(path)+"-*")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tmp := f.Name()
|
||||
if _, err := f.Write(data); err != nil {
|
||||
f.Close()
|
||||
os.Remove(tmp)
|
||||
return err
|
||||
}
|
||||
if err := f.Sync(); err != nil {
|
||||
f.Close()
|
||||
os.Remove(tmp)
|
||||
return err
|
||||
}
|
||||
if err := f.Close(); err != nil {
|
||||
os.Remove(tmp)
|
||||
return err
|
||||
}
|
||||
if err := os.Rename(tmp, path); err != nil {
|
||||
os.Remove(tmp)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Put records the result of an action. All outputs must already be in the
|
||||
// CAS; writes are staged and renamed so concurrent processes never observe a
|
||||
// partial entry.
|
||||
func (a *ActionCache) Put(action Digest, result *ActionResult) error {
|
||||
for name, d := range result.Outputs {
|
||||
if !a.cas.Contains(d) {
|
||||
return fmt.Errorf("cache: output %q (%s) missing from CAS", name, d)
|
||||
}
|
||||
}
|
||||
data, err := json.Marshal(result)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cache: %w", err)
|
||||
}
|
||||
path := a.path(action)
|
||||
if err := a.root.MkdirAll(filepath.Dir(path), 0755); err != nil {
|
||||
return fmt.Errorf("cache: %w", err)
|
||||
}
|
||||
f, name, err := a.cas.createTemp(action.Hash[:8] + "-")
|
||||
if err != nil {
|
||||
return fmt.Errorf("cache: %w", err)
|
||||
}
|
||||
defer a.root.Remove(name)
|
||||
if _, err := f.Write(data); err != nil {
|
||||
f.Close()
|
||||
return fmt.Errorf("cache: %w", err)
|
||||
}
|
||||
if err := f.Close(); err != nil {
|
||||
return fmt.Errorf("cache: %w", err)
|
||||
}
|
||||
if err := a.root.Rename(name, path); err != nil {
|
||||
return fmt.Errorf("cache: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
103
internal/cache/cache.go
vendored
Normal file
103
internal/cache/cache.go
vendored
Normal file
@@ -0,0 +1,103 @@
|
||||
// Package cache implements sqlc's on-disk cache, designed after Bazel's
|
||||
// local disk cache.
|
||||
//
|
||||
// It has two halves:
|
||||
//
|
||||
// - A content-addressable store (CAS) holding blobs keyed by the SHA-256
|
||||
// hash of their contents, laid out as cas/<xx>/<hash>.
|
||||
// - An action cache (AC) mapping the digest of an action — a description of
|
||||
// cacheable work and all of its inputs — to the digests of the outputs
|
||||
// that work produced, laid out as ac/<xx>/<hash>.
|
||||
//
|
||||
// Work whose output is not derivable from its inputs, like query analysis,
|
||||
// uses both halves, just like Bazel: hash the action, look its digest up in
|
||||
// the action cache, then fetch the referenced output blobs from the CAS.
|
||||
//
|
||||
// Remote fetches with a declared checksum, like WASM plugins, need no action
|
||||
// cache entry at all: the declared sha256 is itself a content address, so the
|
||||
// blob is stored and loaded directly from the CAS keyed by that checksum.
|
||||
package cache
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// Cache bundles the CAS and the action cache that shares it. All storage
|
||||
// I/O is confined to the cache directory through an os.Root; callers should
|
||||
// Close the cache when finished with it to release the root.
|
||||
type Cache struct {
|
||||
root *os.Root
|
||||
CAS *CAS
|
||||
Actions *ActionCache
|
||||
}
|
||||
|
||||
// Dir returns the cache root, defaulting to os.UserCacheDir(). The location
|
||||
// can be overridden with the SQLCCACHE environment variable.
|
||||
func Dir() (string, error) {
|
||||
cache := os.Getenv("SQLCCACHE")
|
||||
if cache != "" {
|
||||
return cache, nil
|
||||
}
|
||||
cacheHome, err := os.UserCacheDir()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return filepath.Join(cacheHome, "sqlc"), nil
|
||||
}
|
||||
|
||||
// Open returns the cache rooted at Dir().
|
||||
func Open() (*Cache, error) {
|
||||
root, err := Dir()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return OpenAt(root)
|
||||
}
|
||||
|
||||
// OpenAt returns the cache rooted at the given directory, creating it if
|
||||
// necessary.
|
||||
func OpenAt(dir string) (*Cache, error) {
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
return nil, fmt.Errorf("failed to create %s directory: %w", dir, err)
|
||||
}
|
||||
root, err := os.OpenRoot(dir)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cache: %w", err)
|
||||
}
|
||||
cas, err := newCAS(root)
|
||||
if err != nil {
|
||||
root.Close()
|
||||
return nil, err
|
||||
}
|
||||
return &Cache{
|
||||
root: root,
|
||||
CAS: cas,
|
||||
Actions: newActionCache(root, cas),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Close releases the cache's handle on its root directory.
|
||||
func (c *Cache) Close() error {
|
||||
return c.root.Close()
|
||||
}
|
||||
|
||||
// ExecDir creates a fresh, private scratch directory for materializing the
|
||||
// output tree of the given action, for tools that need their outputs on disk
|
||||
// (like wazero's compilation cache). Each call returns a new directory under
|
||||
// exec/, so two concurrent processes never share one — otherwise a tool
|
||||
// staging files there (wazero writes <key>.tmp files in place) could be swept
|
||||
// into the other's PutTree. The caller must remove it when done; its contents
|
||||
// are always reproducible from the CAS, so losing it is harmless.
|
||||
func (c *Cache) ExecDir(action Digest) (string, error) {
|
||||
base := filepath.Join(c.root.Name(), "exec")
|
||||
if err := os.MkdirAll(base, 0755); err != nil {
|
||||
return "", fmt.Errorf("failed to create %s directory: %w", base, err)
|
||||
}
|
||||
dir, err := os.MkdirTemp(base, action.Hash+"-")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("cache: %w", err)
|
||||
}
|
||||
return dir, nil
|
||||
}
|
||||
138
internal/cache/cas.go
vendored
Normal file
138
internal/cache/cas.go
vendored
Normal file
@@ -0,0 +1,138 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"math/rand/v2"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// ErrNotFound is returned when a blob or action result is not in the cache.
|
||||
var ErrNotFound = errors.New("cache: not found")
|
||||
|
||||
// CAS is an on-disk content-addressable store keyed by SHA-256, modeled on
|
||||
// Bazel's disk cache. Blobs live at cas/<xx>/<hash> under the cache root,
|
||||
// where <xx> is the first two hex characters of the hash. Because a blob's
|
||||
// name is derived from its contents, entries never change once written:
|
||||
// writers race benignly and readers can detect corruption by re-hashing.
|
||||
//
|
||||
// Content with an externally declared checksum (remotely fetched plugins)
|
||||
// needs no action cache entry: the declared sha256 is the address, so it is
|
||||
// stored and loaded directly — see SHA256Digest.
|
||||
//
|
||||
// All I/O goes through an os.Root, so no entry name — hash-derived or read
|
||||
// from an action cache entry — can escape the cache directory.
|
||||
type CAS struct {
|
||||
root *os.Root
|
||||
}
|
||||
|
||||
func newCAS(root *os.Root) (*CAS, error) {
|
||||
if err := root.MkdirAll("tmp", 0755); err != nil {
|
||||
return nil, fmt.Errorf("cache: create tmp: %w", err)
|
||||
}
|
||||
return &CAS{root: root}, nil
|
||||
}
|
||||
|
||||
// path returns a blob's path relative to the cache root.
|
||||
func (c *CAS) path(d Digest) string {
|
||||
return filepath.Join("cas", d.Hash[:2], d.Hash)
|
||||
}
|
||||
|
||||
// Filename returns the path of a stored blob, for a consumer that needs the
|
||||
// file rather than its bytes — SQLite, for one, opens a database by name. A
|
||||
// blob is named after the hash of its contents, so the file at this path never
|
||||
// changes and any number of processes may read it at once.
|
||||
//
|
||||
// It reports false when the blob is not stored.
|
||||
func (c *CAS) Filename(d Digest) (string, bool) {
|
||||
if !c.Contains(d) {
|
||||
return "", false
|
||||
}
|
||||
return filepath.Join(c.root.Name(), c.path(d)), true
|
||||
}
|
||||
|
||||
// createTemp creates a staging file under tmp/ in the cache root, returning
|
||||
// the open file and its root-relative name.
|
||||
func (c *CAS) createTemp(prefix string) (*os.File, string, error) {
|
||||
for range 10000 {
|
||||
name := filepath.Join("tmp", prefix+strconv.FormatUint(rand.Uint64(), 36))
|
||||
f, err := c.root.OpenFile(name, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0644)
|
||||
if errors.Is(err, fs.ErrExist) {
|
||||
continue
|
||||
}
|
||||
return f, name, err
|
||||
}
|
||||
return nil, "", errors.New("cache: could not create temp file")
|
||||
}
|
||||
|
||||
// Put stores a blob and returns its digest. Writing is atomic: the blob is
|
||||
// staged in a temp file and renamed into place, so concurrent sqlc processes
|
||||
// never observe partial entries.
|
||||
func (c *CAS) Put(data []byte) (Digest, error) {
|
||||
d := DigestOf(data)
|
||||
path := c.path(d)
|
||||
// Skip the write only when an entry of the right size already exists; a
|
||||
// wrong-sized entry is corrupt and is atomically replaced by the rename
|
||||
// below.
|
||||
if fi, err := c.root.Stat(path); err == nil && fi.Size() == d.SizeBytes {
|
||||
return d, nil
|
||||
}
|
||||
if err := c.root.MkdirAll(filepath.Dir(path), 0755); err != nil {
|
||||
return Digest{}, fmt.Errorf("cache: %w", err)
|
||||
}
|
||||
f, name, err := c.createTemp(d.Hash[:8] + "-")
|
||||
if err != nil {
|
||||
return Digest{}, fmt.Errorf("cache: %w", err)
|
||||
}
|
||||
defer c.root.Remove(name)
|
||||
if _, err := f.Write(data); err != nil {
|
||||
f.Close()
|
||||
return Digest{}, fmt.Errorf("cache: %w", err)
|
||||
}
|
||||
if err := f.Close(); err != nil {
|
||||
return Digest{}, fmt.Errorf("cache: %w", err)
|
||||
}
|
||||
if err := c.root.Rename(name, path); err != nil {
|
||||
return Digest{}, fmt.Errorf("cache: %w", err)
|
||||
}
|
||||
return d, nil
|
||||
}
|
||||
|
||||
// Get returns the blob for a digest. Contents are re-hashed before being
|
||||
// returned; a corrupt entry is evicted and reported as ErrNotFound so
|
||||
// callers simply redo the work that produced it.
|
||||
func (c *CAS) Get(d Digest) ([]byte, error) {
|
||||
if !d.valid() {
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
path := c.path(d)
|
||||
data, err := c.root.ReadFile(path)
|
||||
if err != nil {
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
return nil, fmt.Errorf("cache: %w", err)
|
||||
}
|
||||
if DigestOf(data).Hash != d.Hash {
|
||||
c.root.Remove(path)
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// Contains reports whether a blob with the given digest is present, checking
|
||||
// size when the digest carries one. It does not verify contents; Get performs
|
||||
// full verification.
|
||||
func (c *CAS) Contains(d Digest) bool {
|
||||
if !d.valid() {
|
||||
return false
|
||||
}
|
||||
fi, err := c.root.Stat(c.path(d))
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return d.SizeBytes < 0 || fi.Size() == d.SizeBytes
|
||||
}
|
||||
100
internal/cache/digest.go
vendored
Normal file
100
internal/cache/digest.go
vendored
Normal file
@@ -0,0 +1,100 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"hash"
|
||||
)
|
||||
|
||||
// Digest identifies a blob by its SHA-256 hash and size, mirroring the
|
||||
// Digest message from Bazel's remote execution API. The size is stored
|
||||
// alongside the hash so that entries can be validated without reading blob
|
||||
// contents; a negative size means the size is unknown, as with a checksum
|
||||
// declared in a configuration file.
|
||||
type Digest struct {
|
||||
// Hash is the lowercase hex-encoded SHA-256 hash of the blob.
|
||||
Hash string `json:"hash"`
|
||||
// SizeBytes is the length of the blob in bytes, or negative if unknown.
|
||||
SizeBytes int64 `json:"size_bytes"`
|
||||
}
|
||||
|
||||
func (d Digest) String() string {
|
||||
return fmt.Sprintf("sha256:%s/%d", d.Hash, d.SizeBytes)
|
||||
}
|
||||
|
||||
func (d Digest) valid() bool {
|
||||
if len(d.Hash) != 64 {
|
||||
return false
|
||||
}
|
||||
_, err := hex.DecodeString(d.Hash)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// DigestOf returns the Digest of a blob.
|
||||
func DigestOf(data []byte) Digest {
|
||||
sum := sha256.Sum256(data)
|
||||
return Digest{
|
||||
Hash: hex.EncodeToString(sum[:]),
|
||||
SizeBytes: int64(len(data)),
|
||||
}
|
||||
}
|
||||
|
||||
// SHA256Digest returns a Digest referencing a blob by a declared SHA-256
|
||||
// checksum whose size is not known, suitable for looking up remotely fetched
|
||||
// content in the CAS.
|
||||
func SHA256Digest(hexhash string) Digest {
|
||||
return Digest{
|
||||
Hash: hexhash,
|
||||
SizeBytes: -1,
|
||||
}
|
||||
}
|
||||
|
||||
// An Action describes a unit of cacheable work, playing the role of Bazel's
|
||||
// Action message: a mnemonic naming the kind of work plus the complete set of
|
||||
// inputs that determine its outputs. Two actions with the same digest are
|
||||
// assumed to produce the same outputs.
|
||||
//
|
||||
// Inputs are hashed incrementally with length-prefixed framing so that the
|
||||
// boundary between inputs is unambiguous ("ab"+"c" hashes differently from
|
||||
// "a"+"bc").
|
||||
type Action struct {
|
||||
hasher hash.Hash
|
||||
}
|
||||
|
||||
// NewAction starts building an action key for the given mnemonic, e.g.
|
||||
// "QueryAnalysis". The sha256 of the sqlc binary itself is always the first
|
||||
// input: the tool that executes an action determines its outputs just as
|
||||
// much as the declared inputs do, so a rebuilt sqlc never reuses stale
|
||||
// entries. The binary's digest is memoized in the cache — see toolDigest —
|
||||
// which is why actions are created through a Cache.
|
||||
func (c *Cache) NewAction(mnemonic string) *Action {
|
||||
a := &Action{hasher: sha256.New()}
|
||||
a.write([]byte(mnemonic))
|
||||
a.AddInput("tool", c.toolDigest())
|
||||
return a
|
||||
}
|
||||
|
||||
// AddInput mixes a named input into the action key. Order matters: callers
|
||||
// must add inputs in a deterministic order.
|
||||
func (a *Action) AddInput(name string, data []byte) *Action {
|
||||
a.write([]byte(name))
|
||||
a.write(data)
|
||||
return a
|
||||
}
|
||||
|
||||
// Digest returns the action's digest, used as the action cache key.
|
||||
func (a *Action) Digest() Digest {
|
||||
return Digest{
|
||||
Hash: hex.EncodeToString(a.hasher.Sum(nil)),
|
||||
SizeBytes: 0,
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Action) write(data []byte) {
|
||||
var frame [8]byte
|
||||
binary.LittleEndian.PutUint64(frame[:], uint64(len(data)))
|
||||
a.hasher.Write(frame[:])
|
||||
a.hasher.Write(data)
|
||||
}
|
||||
95
internal/cache/tool.go
vendored
Normal file
95
internal/cache/tool.go
vendored
Normal file
@@ -0,0 +1,95 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
"github.com/sqlc-dev/sqlc/internal/info"
|
||||
)
|
||||
|
||||
// The sha256 of the sqlc binary is an input to every action — a rebuilt sqlc
|
||||
// may analyze queries or embed a different wazero than the one that produced
|
||||
// a cache entry, even when the version string is unchanged (dev builds).
|
||||
//
|
||||
// Hashing a ~100MB executable costs tens of milliseconds, too much to pay on
|
||||
// every short-lived sqlc process, so the digest is memoized on disk keyed by
|
||||
// the executable's path, size, and mtime — the same trick Bazel's file
|
||||
// digest cache uses. A warm run costs one stat and a tiny read; only a
|
||||
// rebuilt (or moved) binary is re-hashed.
|
||||
var tool struct {
|
||||
sync.Mutex
|
||||
digest []byte
|
||||
}
|
||||
|
||||
type toolMemo struct {
|
||||
Path string `json:"path"`
|
||||
SizeBytes int64 `json:"size_bytes"`
|
||||
MtimeNS int64 `json:"mtime_ns"`
|
||||
SHA256 string `json:"sha256"`
|
||||
}
|
||||
|
||||
const toolMemoPath = "tool"
|
||||
|
||||
func (c *Cache) toolDigest() []byte {
|
||||
tool.Lock()
|
||||
defer tool.Unlock()
|
||||
if tool.digest == nil {
|
||||
tool.digest = c.computeToolDigest()
|
||||
}
|
||||
return tool.digest
|
||||
}
|
||||
|
||||
func (c *Cache) computeToolDigest() []byte {
|
||||
path, err := os.Executable()
|
||||
if err != nil {
|
||||
return []byte(info.Version)
|
||||
}
|
||||
fi, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return []byte(info.Version)
|
||||
}
|
||||
|
||||
var memo toolMemo
|
||||
if data, err := c.root.ReadFile(toolMemoPath); err == nil {
|
||||
if err := json.Unmarshal(data, &memo); err == nil &&
|
||||
memo.Path == path &&
|
||||
memo.SizeBytes == fi.Size() &&
|
||||
memo.MtimeNS == fi.ModTime().UnixNano() &&
|
||||
memo.SHA256 != "" {
|
||||
return []byte(memo.SHA256)
|
||||
}
|
||||
}
|
||||
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return []byte(info.Version)
|
||||
}
|
||||
defer f.Close()
|
||||
h := sha256.New()
|
||||
if _, err := io.Copy(h, f); err != nil {
|
||||
return []byte(info.Version)
|
||||
}
|
||||
sum := hex.EncodeToString(h.Sum(nil))
|
||||
|
||||
memo = toolMemo{
|
||||
Path: path,
|
||||
SizeBytes: fi.Size(),
|
||||
MtimeNS: fi.ModTime().UnixNano(),
|
||||
SHA256: sum,
|
||||
}
|
||||
if data, err := json.Marshal(memo); err == nil {
|
||||
if f, name, err := c.CAS.createTemp("tool-"); err == nil {
|
||||
if _, werr := f.Write(data); werr == nil && f.Close() == nil {
|
||||
c.root.Rename(name, toolMemoPath)
|
||||
} else {
|
||||
f.Close()
|
||||
}
|
||||
c.root.Remove(name)
|
||||
}
|
||||
}
|
||||
return []byte(sum)
|
||||
}
|
||||
275
internal/cmd/analyze.go
Normal file
275
internal/cmd/analyze.go
Normal file
@@ -0,0 +1,275 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/sqlc-dev/sqlc/internal/compiler"
|
||||
"github.com/sqlc-dev/sqlc/internal/config"
|
||||
"github.com/sqlc-dev/sqlc/internal/core"
|
||||
"github.com/sqlc-dev/sqlc/internal/multierr"
|
||||
"github.com/sqlc-dev/sqlc/internal/opts"
|
||||
"github.com/sqlc-dev/sqlc/internal/sql/ast"
|
||||
)
|
||||
|
||||
func newAnalyzeCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "analyze [query-file]",
|
||||
Short: "Analyze a query against a schema and output the result columns and parameters",
|
||||
Long: `Analyze a query file against a schema file and output the inferred result
|
||||
columns and parameters as JSON.
|
||||
|
||||
Unlike "sqlc generate", this command does not require a configuration file and
|
||||
does not connect to a database. It uses sqlc's native static analysis to infer
|
||||
types from the provided schema.
|
||||
|
||||
Examples:
|
||||
# Analyze a PostgreSQL query
|
||||
sqlc analyze --dialect postgresql --schema schema.sql query.sql
|
||||
|
||||
# Analyze a MySQL query
|
||||
sqlc analyze --dialect mysql --schema schema.sql query.sql
|
||||
|
||||
# Analyze a SQLite query
|
||||
sqlc analyze --dialect sqlite --schema schema.sql query.sql
|
||||
|
||||
# Analyze a ClickHouse query
|
||||
sqlc analyze --dialect clickhouse --schema schema.sql query.sql
|
||||
|
||||
# Analyze a GoogleSQL (BigQuery, Spanner) query
|
||||
sqlc analyze --dialect googlesql --schema schema.sql query.sql
|
||||
|
||||
# Analyze a SQL Server (T-SQL) query
|
||||
sqlc analyze --dialect mssql --schema schema.sql query.sql
|
||||
|
||||
# Analyze a DuckDB query
|
||||
sqlc analyze --dialect duckdb --schema schema.sql query.sql
|
||||
|
||||
# Analyze a query piped via stdin
|
||||
echo "-- name: GetAuthor :one
|
||||
SELECT * FROM authors WHERE id = $1;" | sqlc analyze --dialect postgresql --schema schema.sql
|
||||
|
||||
# Include the statement AST in the output
|
||||
sqlc analyze --dialect postgresql --schema schema.sql --ast query.sql`,
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
dialect, err := cmd.Flags().GetString("dialect")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if dialect == "" {
|
||||
return fmt.Errorf("--dialect flag is required (postgresql, mysql, sqlite, clickhouse, googlesql, mssql, or duckdb)")
|
||||
}
|
||||
|
||||
schemaPath, err := cmd.Flags().GetString("schema")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if schemaPath == "" {
|
||||
return fmt.Errorf("--schema flag is required")
|
||||
}
|
||||
|
||||
includeAST, err := cmd.Flags().GetBool("ast")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// The query comes from a file argument or, when none is given, from
|
||||
// stdin. The compiler reads queries from files, so stdin is written to
|
||||
// a temporary file.
|
||||
var queryPath string
|
||||
if len(args) == 1 {
|
||||
queryPath = args[0]
|
||||
} else {
|
||||
stat, err := os.Stdin.Stat()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to stat stdin: %w", err)
|
||||
}
|
||||
if (stat.Mode() & os.ModeCharDevice) != 0 {
|
||||
return fmt.Errorf("no query provided. Specify a query file or pipe SQL via stdin")
|
||||
}
|
||||
data, err := io.ReadAll(cmd.InOrStdin())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read stdin: %w", err)
|
||||
}
|
||||
tmp, err := os.CreateTemp("", "sqlc-analyze-*.sql")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create temp file: %w", err)
|
||||
}
|
||||
defer os.Remove(tmp.Name())
|
||||
if _, err := tmp.Write(data); err != nil {
|
||||
tmp.Close()
|
||||
return fmt.Errorf("failed to write temp file: %w", err)
|
||||
}
|
||||
if err := tmp.Close(); err != nil {
|
||||
return fmt.Errorf("failed to close temp file: %w", err)
|
||||
}
|
||||
queryPath = tmp.Name()
|
||||
}
|
||||
|
||||
var engine config.Engine
|
||||
switch dialect {
|
||||
case "postgresql", "postgres", "pg":
|
||||
engine = config.EnginePostgreSQL
|
||||
case "mysql":
|
||||
engine = config.EngineMySQL
|
||||
case "sqlite":
|
||||
engine = config.EngineSQLite
|
||||
case "clickhouse":
|
||||
engine = config.EngineClickHouse
|
||||
case "googlesql":
|
||||
engine = config.EngineGoogleSQL
|
||||
case "mssql", "sqlserver":
|
||||
engine = config.EngineMSSQL
|
||||
case "duckdb":
|
||||
engine = config.EngineDuckDB
|
||||
default:
|
||||
return fmt.Errorf("unsupported dialect: %s (use postgresql, mysql, sqlite, clickhouse, googlesql, mssql, or duckdb)", dialect)
|
||||
}
|
||||
|
||||
sql := config.SQL{
|
||||
Engine: engine,
|
||||
Schema: config.Paths{schemaPath},
|
||||
Queries: config.Paths{queryPath},
|
||||
}
|
||||
combo := config.Combine(config.Config{}, sql)
|
||||
parserOpts := opts.Parser{}
|
||||
|
||||
ctx := cmd.Context()
|
||||
c, err := compiler.NewCompiler(sql, combo, parserOpts, compiler.WithCoreAnalysis())
|
||||
if err != nil {
|
||||
return fmt.Errorf("error creating compiler: %w", err)
|
||||
}
|
||||
defer c.Close(ctx)
|
||||
|
||||
if err := c.ParseCatalog(sql.Schema); err != nil {
|
||||
return fmt.Errorf("error parsing schema: %w", formatParseError(err))
|
||||
}
|
||||
if err := c.ParseQueries(sql.Queries, parserOpts); err != nil {
|
||||
return fmt.Errorf("error parsing queries: %w", formatParseError(err))
|
||||
}
|
||||
|
||||
result := c.Result()
|
||||
|
||||
out := make([]analyzedQuery, 0, len(result.Queries))
|
||||
for _, q := range result.Queries {
|
||||
out = append(out, newAnalyzedQuery(q, includeAST))
|
||||
}
|
||||
|
||||
stdout := cmd.OutOrStdout()
|
||||
encoder := json.NewEncoder(stdout)
|
||||
encoder.SetIndent("", " ")
|
||||
if err := encoder.Encode(out); err != nil {
|
||||
return fmt.Errorf("failed to encode analysis: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
cmd.Flags().StringP("dialect", "d", "", "SQL dialect to use (postgresql, mysql, sqlite, clickhouse, googlesql, mssql, or duckdb)")
|
||||
cmd.Flags().StringP("schema", "s", "", "path to the schema file")
|
||||
cmd.Flags().BoolP("ast", "", false, "include the statement AST in the output")
|
||||
return cmd
|
||||
}
|
||||
|
||||
// formatParseError unwraps a multierr.Error into a single error containing all
|
||||
// of the underlying file errors, so the analyze command can report each one with
|
||||
// its file location.
|
||||
func formatParseError(err error) error {
|
||||
parserErr, ok := err.(*multierr.Error)
|
||||
if !ok {
|
||||
return err
|
||||
}
|
||||
var msgs []string
|
||||
for _, fileErr := range parserErr.Errs() {
|
||||
msgs = append(msgs, fmt.Sprintf("%s:%d:%d: %s",
|
||||
fileErr.Filename, fileErr.Line, fileErr.Column, fileErr.Err))
|
||||
}
|
||||
if len(msgs) == 0 {
|
||||
return err
|
||||
}
|
||||
return fmt.Errorf("%s", strings.Join(msgs, "; "))
|
||||
}
|
||||
|
||||
type analyzedQuery struct {
|
||||
Name string `json:"name"`
|
||||
Cmd string `json:"cmd"`
|
||||
Columns []analyzedColumn `json:"columns"`
|
||||
Params []analyzedParam `json:"params"`
|
||||
AST *ast.RawStmt `json:"ast,omitempty"`
|
||||
}
|
||||
|
||||
type analyzedColumn struct {
|
||||
Name string `json:"name"`
|
||||
Type *core.TypeExpr `json:"type,omitempty"`
|
||||
Table string `json:"table,omitempty"`
|
||||
}
|
||||
|
||||
type analyzedParam struct {
|
||||
Number int `json:"number"`
|
||||
Column analyzedColumn `json:"column"`
|
||||
}
|
||||
|
||||
func newAnalyzedQuery(q *compiler.Query, includeAST bool) analyzedQuery {
|
||||
aq := analyzedQuery{
|
||||
Name: q.Metadata.Name,
|
||||
Cmd: q.Metadata.Cmd,
|
||||
Columns: make([]analyzedColumn, 0, len(q.Columns)),
|
||||
Params: make([]analyzedParam, 0, len(q.Params)),
|
||||
}
|
||||
for _, col := range q.Columns {
|
||||
aq.Columns = append(aq.Columns, newAnalyzedColumn(col))
|
||||
}
|
||||
for _, p := range q.Params {
|
||||
aq.Params = append(aq.Params, analyzedParam{
|
||||
Number: p.Number,
|
||||
Column: newAnalyzedColumn(p.Column),
|
||||
})
|
||||
}
|
||||
if includeAST {
|
||||
aq.AST = q.RawStmt
|
||||
}
|
||||
return aq
|
||||
}
|
||||
|
||||
func newAnalyzedColumn(col *compiler.Column) analyzedColumn {
|
||||
if col == nil {
|
||||
return analyzedColumn{}
|
||||
}
|
||||
ac := analyzedColumn{
|
||||
Name: col.Name,
|
||||
Type: newAnalyzedType(col),
|
||||
}
|
||||
if col.Table != nil {
|
||||
ac.Table = col.Table.Name
|
||||
}
|
||||
return ac
|
||||
}
|
||||
|
||||
// newAnalyzedType is the column's type as an expression: the one the
|
||||
// analysis core wrote when it did, otherwise the flat description the
|
||||
// compiler holds, which is the data type wrapped in one array node per
|
||||
// dimension with the column's nullability on the outermost node.
|
||||
func newAnalyzedType(col *compiler.Column) *core.TypeExpr {
|
||||
if col.TypeExpr != nil {
|
||||
return col.TypeExpr
|
||||
}
|
||||
if col.DataType == "" {
|
||||
return nil
|
||||
}
|
||||
t := core.ParseTypeExpr(col.DataType)
|
||||
dims := col.ArrayDims
|
||||
if col.IsArray && dims == 0 {
|
||||
dims = 1
|
||||
}
|
||||
for i := 0; i < dims; i++ {
|
||||
t = &core.TypeExpr{Name: "array", Args: []core.TypeArg{{Type: t}}}
|
||||
}
|
||||
t.Nullable = !col.NotNull
|
||||
return t
|
||||
}
|
||||
277
internal/cmd/cmd.go
Normal file
277
internal/cmd/cmd.go
Normal file
@@ -0,0 +1,277 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime/trace"
|
||||
|
||||
"github.com/cubicdaiya/gonp"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
"github.com/sqlc-dev/sqlc/internal/config"
|
||||
"github.com/sqlc-dev/sqlc/internal/info"
|
||||
"github.com/sqlc-dev/sqlc/internal/opts"
|
||||
"github.com/sqlc-dev/sqlc/internal/sqlcdebug"
|
||||
"github.com/sqlc-dev/sqlc/internal/tracer"
|
||||
)
|
||||
|
||||
var debugProcessPlugins = sqlcdebug.New("processplugins")
|
||||
|
||||
func init() {
|
||||
createDBCmd.Flags().StringP("queryset", "", "", "name of the queryset to use")
|
||||
pushCmd.Flags().BoolP("dry-run", "", false, "dump push request (default: false)")
|
||||
initCmd.Flags().BoolP("v1", "", false, "generate v1 config yaml file")
|
||||
initCmd.Flags().BoolP("v2", "", true, "generate v2 config yaml file")
|
||||
initCmd.MarkFlagsMutuallyExclusive("v1", "v2")
|
||||
}
|
||||
|
||||
// Do runs the command logic.
|
||||
func Do(args []string, stdin io.Reader, stdout io.Writer, stderr io.Writer) int {
|
||||
rootCmd := &cobra.Command{Use: "sqlc", SilenceUsage: true}
|
||||
rootCmd.PersistentFlags().StringP("file", "f", "", "specify an alternate config file (default: sqlc.yaml)")
|
||||
|
||||
rootCmd.AddCommand(checkCmd)
|
||||
rootCmd.AddCommand(createDBCmd)
|
||||
rootCmd.AddCommand(diffCmd)
|
||||
rootCmd.AddCommand(genCmd)
|
||||
rootCmd.AddCommand(newFmtCmd())
|
||||
rootCmd.AddCommand(initCmd)
|
||||
rootCmd.AddCommand(newParseCmd())
|
||||
rootCmd.AddCommand(newAnalyzeCmd())
|
||||
rootCmd.AddCommand(versionCmd)
|
||||
rootCmd.AddCommand(verifyCmd)
|
||||
rootCmd.AddCommand(pushCmd)
|
||||
rootCmd.AddCommand(NewCmdVet())
|
||||
|
||||
rootCmd.SetArgs(args)
|
||||
rootCmd.SetIn(stdin)
|
||||
rootCmd.SetOut(stdout)
|
||||
rootCmd.SetErr(stderr)
|
||||
|
||||
ctx := context.Background()
|
||||
if tracer.Path() != "" {
|
||||
tracectx, cleanup, err := tracer.Start(ctx)
|
||||
if err != nil {
|
||||
fmt.Printf("failed to start trace: %v\n", err)
|
||||
return 1
|
||||
}
|
||||
ctx = tracectx
|
||||
defer cleanup()
|
||||
}
|
||||
if err := rootCmd.ExecuteContext(ctx); err != nil {
|
||||
if exitError, ok := err.(*exec.ExitError); ok {
|
||||
return exitError.ExitCode()
|
||||
} else {
|
||||
return 1
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
var version string
|
||||
|
||||
var versionCmd = &cobra.Command{
|
||||
Use: "version",
|
||||
Short: "Print the sqlc version number",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
defer trace.StartRegion(cmd.Context(), "version").End()
|
||||
if version == "" {
|
||||
fmt.Fprintf(cmd.OutOrStdout(), "%s\n", info.Version)
|
||||
} else {
|
||||
fmt.Fprintf(cmd.OutOrStdout(), "%s\n", version)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
var initCmd = &cobra.Command{
|
||||
Use: "init",
|
||||
Short: "Create an empty sqlc.yaml settings file",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
useV1, err := cmd.Flags().GetBool("v1")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var yamlConfig any
|
||||
if useV1 {
|
||||
yamlConfig = config.V1GenerateSettings{Version: "1"}
|
||||
} else {
|
||||
yamlConfig = config.Config{Version: "2"}
|
||||
}
|
||||
|
||||
defer trace.StartRegion(cmd.Context(), "init").End()
|
||||
file := "sqlc.yaml"
|
||||
if f := cmd.Flag("file"); f != nil && f.Changed {
|
||||
file = f.Value.String()
|
||||
if file == "" {
|
||||
return fmt.Errorf("file argument is empty")
|
||||
}
|
||||
}
|
||||
if _, err := os.Stat(file); !os.IsNotExist(err) {
|
||||
fmt.Printf("%s is already created\n", file)
|
||||
return nil
|
||||
}
|
||||
blob, err := yaml.Marshal(yamlConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = os.WriteFile(file, blob, 0644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
configDoc := "https://docs.sqlc.dev/en/stable/reference/config.html"
|
||||
fmt.Printf(
|
||||
"%s is added. Please visit %s to learn more about configuration\n",
|
||||
file,
|
||||
configDoc,
|
||||
)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
type Env struct {
|
||||
DryRun bool
|
||||
Experiment opts.Experiment
|
||||
}
|
||||
|
||||
func ParseEnv(c *cobra.Command) Env {
|
||||
dr := c.Flag("dry-run")
|
||||
return Env{
|
||||
DryRun: dr != nil && dr.Changed,
|
||||
Experiment: opts.ExperimentFromEnv(),
|
||||
}
|
||||
}
|
||||
|
||||
var ErrPluginProcessDisabled = errors.New("plugin: process-based plugins disabled via SQLCDEBUG=processplugins=0")
|
||||
|
||||
func (e *Env) Validate(cfg *config.Config) error {
|
||||
for _, plugin := range cfg.Plugins {
|
||||
if plugin.Process != nil && debugProcessPlugins.Value() == "0" {
|
||||
return ErrPluginProcessDisabled
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func getConfigPath(stderr io.Writer, f *pflag.Flag) (string, string) {
|
||||
if f != nil && f.Changed {
|
||||
file := f.Value.String()
|
||||
if file == "" {
|
||||
fmt.Fprintln(stderr, "error parsing config: file argument is empty")
|
||||
os.Exit(1)
|
||||
}
|
||||
abspath, err := filepath.Abs(file)
|
||||
if err != nil {
|
||||
fmt.Fprintf(stderr, "error parsing config: absolute file path lookup failed: %s\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
return filepath.Dir(abspath), filepath.Base(abspath)
|
||||
} else {
|
||||
wd, err := os.Getwd()
|
||||
if err != nil {
|
||||
fmt.Fprintln(stderr, "error parsing sqlc.json: file does not exist")
|
||||
os.Exit(1)
|
||||
}
|
||||
return wd, ""
|
||||
}
|
||||
}
|
||||
|
||||
var genCmd = &cobra.Command{
|
||||
Use: "generate",
|
||||
Short: "Generate source code from SQL",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
defer trace.StartRegion(cmd.Context(), "generate").End()
|
||||
stderr := cmd.ErrOrStderr()
|
||||
dir, name := getConfigPath(stderr, cmd.Flag("file"))
|
||||
output, err := Generate(cmd.Context(), dir, name, &Options{
|
||||
Env: ParseEnv(cmd),
|
||||
Stderr: stderr,
|
||||
})
|
||||
if err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
defer trace.StartRegion(cmd.Context(), "writefiles").End()
|
||||
for filename, source := range output {
|
||||
os.MkdirAll(filepath.Dir(filename), 0755)
|
||||
if err := os.WriteFile(filename, []byte(source), 0644); err != nil {
|
||||
fmt.Fprintf(stderr, "%s: %s\n", filename, err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
var checkCmd = &cobra.Command{
|
||||
Use: "compile",
|
||||
Short: "Statically check SQL for syntax and type errors",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
defer trace.StartRegion(cmd.Context(), "compile").End()
|
||||
stderr := cmd.ErrOrStderr()
|
||||
dir, name := getConfigPath(stderr, cmd.Flag("file"))
|
||||
_, err := Generate(cmd.Context(), dir, name, &Options{
|
||||
Env: ParseEnv(cmd),
|
||||
Stderr: stderr,
|
||||
})
|
||||
if err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func getLines(f []byte) []string {
|
||||
fp := bytes.NewReader(f)
|
||||
scanner := bufio.NewScanner(fp)
|
||||
lines := make([]string, 0)
|
||||
for scanner.Scan() {
|
||||
lines = append(lines, scanner.Text())
|
||||
}
|
||||
return lines
|
||||
}
|
||||
|
||||
func filterHunks[T gonp.Elem](uniHunks []gonp.UniHunk[T]) []gonp.UniHunk[T] {
|
||||
var out []gonp.UniHunk[T]
|
||||
for i, uniHunk := range uniHunks {
|
||||
var changed bool
|
||||
for _, e := range uniHunk.GetChanges() {
|
||||
switch e.GetType() {
|
||||
case gonp.SesDelete:
|
||||
changed = true
|
||||
case gonp.SesAdd:
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
if changed {
|
||||
out = append(out, uniHunks[i])
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
var diffCmd = &cobra.Command{
|
||||
Use: "diff",
|
||||
Short: "Compare the generated files to the existing files",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
defer trace.StartRegion(cmd.Context(), "diff").End()
|
||||
stderr := cmd.ErrOrStderr()
|
||||
dir, name := getConfigPath(stderr, cmd.Flag("file"))
|
||||
opts := &Options{
|
||||
Env: ParseEnv(cmd),
|
||||
Stderr: stderr,
|
||||
}
|
||||
if err := Diff(cmd.Context(), dir, name, opts); err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
102
internal/cmd/createdb.go
Normal file
102
internal/cmd/createdb.go
Normal file
@@ -0,0 +1,102 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime/trace"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/sqlc-dev/sqlc/internal/config"
|
||||
"github.com/sqlc-dev/sqlc/internal/dbmanager"
|
||||
"github.com/sqlc-dev/sqlc/internal/migrations"
|
||||
"github.com/sqlc-dev/sqlc/internal/sql/sqlpath"
|
||||
)
|
||||
|
||||
var createDBCmd = &cobra.Command{
|
||||
Use: "createdb",
|
||||
Short: "Create an ephemeral database",
|
||||
Args: cobra.NoArgs,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
defer trace.StartRegion(cmd.Context(), "createdb").End()
|
||||
stderr := cmd.ErrOrStderr()
|
||||
dir, filename := getConfigPath(stderr, cmd.Flag("file"))
|
||||
querySetName, err := cmd.Flags().GetString("queryset")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = CreateDB(cmd.Context(), dir, filename, querySetName, &Options{
|
||||
Env: ParseEnv(cmd),
|
||||
Stderr: stderr,
|
||||
})
|
||||
if err != nil {
|
||||
fmt.Fprintln(stderr, err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func CreateDB(ctx context.Context, dir, filename, querySetName string, o *Options) error {
|
||||
_, conf, err := o.ReadConfig(dir, filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Find the first queryset with a managed database
|
||||
var queryset *config.SQL
|
||||
var count int
|
||||
for _, sql := range conf.SQL {
|
||||
if querySetName != "" && sql.Name != querySetName {
|
||||
continue
|
||||
}
|
||||
if sql.Database != nil && sql.Database.Managed {
|
||||
queryset = &sql
|
||||
count += 1
|
||||
}
|
||||
}
|
||||
if queryset == nil && querySetName != "" {
|
||||
return fmt.Errorf("no queryset found with name %q", querySetName)
|
||||
}
|
||||
if queryset == nil {
|
||||
return fmt.Errorf("no querysets configured to use a managed database")
|
||||
}
|
||||
if count > 1 {
|
||||
return fmt.Errorf("multiple querysets configured to use managed databases")
|
||||
}
|
||||
|
||||
switch queryset.Engine {
|
||||
case config.EngineMySQL:
|
||||
// pass
|
||||
case config.EnginePostgreSQL:
|
||||
// pass
|
||||
default:
|
||||
return fmt.Errorf("createdb does not support the %s engine", queryset.Engine)
|
||||
}
|
||||
|
||||
var ddl []string
|
||||
files, err := sqlpath.Glob(queryset.Schema)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, schema := range files {
|
||||
contents, err := os.ReadFile(schema)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read file: %w", err)
|
||||
}
|
||||
ddl = append(ddl, migrations.RemoveRollbackStatements(string(contents)))
|
||||
}
|
||||
|
||||
now := time.Now().UTC().UnixNano()
|
||||
client := dbmanager.NewClient(conf.Servers)
|
||||
resp, err := client.CreateDatabase(ctx, &dbmanager.CreateDatabaseRequest{
|
||||
Engine: string(queryset.Engine),
|
||||
Migrations: ddl,
|
||||
Prefix: fmt.Sprintf("sqlc_createdb_%d", now),
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("managed: create database: %w", err)
|
||||
}
|
||||
fmt.Println(resp.Uri)
|
||||
return nil
|
||||
}
|
||||
59
internal/cmd/diff.go
Normal file
59
internal/cmd/diff.go
Normal file
@@ -0,0 +1,59 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime/trace"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/cubicdaiya/gonp"
|
||||
)
|
||||
|
||||
func Diff(ctx context.Context, dir, name string, opts *Options) error {
|
||||
stderr := opts.Stderr
|
||||
output, err := Generate(ctx, dir, name, opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer trace.StartRegion(ctx, "checkfiles").End()
|
||||
var errored bool
|
||||
|
||||
keys := make([]string, 0, len(output))
|
||||
for k, _ := range output {
|
||||
kk := k
|
||||
keys = append(keys, kk)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
for _, filename := range keys {
|
||||
source := output[filename]
|
||||
if _, err := os.Stat(filename); errors.Is(err, os.ErrNotExist) {
|
||||
errored = true
|
||||
// stdout message
|
||||
continue
|
||||
}
|
||||
existing, err := os.ReadFile(filename)
|
||||
if err != nil {
|
||||
errored = true
|
||||
fmt.Fprintf(stderr, "%s: %s\n", filename, err)
|
||||
continue
|
||||
}
|
||||
diff := gonp.New(getLines(existing), getLines([]byte(source)))
|
||||
diff.Compose()
|
||||
uniHunks := filterHunks(diff.UnifiedHunks())
|
||||
|
||||
if len(uniHunks) > 0 {
|
||||
errored = true
|
||||
fmt.Fprintf(stderr, "--- a%s\n", strings.TrimPrefix(filename, dir))
|
||||
fmt.Fprintf(stderr, "+++ b%s\n", strings.TrimPrefix(filename, dir))
|
||||
diff.FprintUniHunks(stderr, uniHunks)
|
||||
}
|
||||
}
|
||||
if errored {
|
||||
return errors.New("diff found")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
481
internal/cmd/fmt.go
Normal file
481
internal/cmd/fmt.go
Normal file
@@ -0,0 +1,481 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime/trace"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/cubicdaiya/gonp"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/sqlc-dev/sqlc/internal/config"
|
||||
"github.com/sqlc-dev/sqlc/internal/engine/dolphin"
|
||||
"github.com/sqlc-dev/sqlc/internal/engine/postgresql"
|
||||
"github.com/sqlc-dev/sqlc/internal/engine/sqlite"
|
||||
"github.com/sqlc-dev/sqlc/internal/sql/ast"
|
||||
"github.com/sqlc-dev/sqlc/internal/sql/format"
|
||||
"github.com/sqlc-dev/sqlc/internal/sql/sqlpath"
|
||||
)
|
||||
|
||||
// noLineLimit renders without a maximum line width: like gofmt, fmt never
|
||||
// rewraps a line on its own. Breaks come from the author's own line breaks
|
||||
// at the boundaries the printer models, and from comments, which cannot
|
||||
// share a line with the code after them.
|
||||
const noLineLimit = 1 << 30
|
||||
|
||||
// queryFormatter is what the fmt command needs from an engine: a parser
|
||||
// that surfaces the comments its lexer already scans (so statements and
|
||||
// comments come from one pass, and interior comments format along with
|
||||
// their statements) plus dialect-aware formatting. Comments cannot be
|
||||
// reprinted without parser support, so ParseFile is the price of admission.
|
||||
type queryFormatter interface {
|
||||
ParseFile(io.Reader) (*ast.File, error)
|
||||
format.Dialect
|
||||
}
|
||||
|
||||
// newQueryFormatter returns the formatter for engines fmt supports —
|
||||
// SQLite, PostgreSQL and MySQL today. An engine joins by teaching its
|
||||
// parser to surface comments (meyer's, oliphant's and marino's ParseFile
|
||||
// are the templates) and adding its case here.
|
||||
func newQueryFormatter(engine config.Engine) queryFormatter {
|
||||
switch engine {
|
||||
case config.EnginePostgreSQL:
|
||||
return postgresql.NewParser()
|
||||
case config.EngineSQLite:
|
||||
return sqlite.NewParser()
|
||||
case config.EngineMySQL:
|
||||
// The format parser preserves identifier case and proves every
|
||||
// statement via Fingerprint, the way oliphant does for PostgreSQL.
|
||||
return dolphin.NewFormatParser()
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func newFmtCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "fmt",
|
||||
Short: "Format SQL queries",
|
||||
Long: `Format the SQL query files referenced by the configuration file.
|
||||
|
||||
Each query is parsed with the engine's parser and printed back in a canonical
|
||||
form, with its comments kept where they were written. A statement that cannot
|
||||
be proven to survive formatting unchanged is left exactly as written. Files
|
||||
are rewritten in place; pass --diff to print the changes to stdout instead.`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
defer trace.StartRegion(cmd.Context(), "fmt").End()
|
||||
stderr := cmd.ErrOrStderr()
|
||||
stdout := cmd.OutOrStdout()
|
||||
showDiff, err := cmd.Flags().GetBool("diff")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dir, name := getConfigPath(stderr, cmd.Flag("file"))
|
||||
output, err := Format(cmd.Context(), dir, name, &Options{
|
||||
Env: ParseEnv(cmd),
|
||||
Stderr: stderr,
|
||||
})
|
||||
if err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
filenames := make([]string, 0, len(output))
|
||||
for filename := range output {
|
||||
filenames = append(filenames, filename)
|
||||
}
|
||||
sort.Strings(filenames)
|
||||
|
||||
for _, filename := range filenames {
|
||||
existing, err := os.ReadFile(filename)
|
||||
if err != nil {
|
||||
fmt.Fprintf(stderr, "%s: %s\n", filename, err)
|
||||
return err
|
||||
}
|
||||
source := output[filename]
|
||||
if string(existing) == source {
|
||||
continue
|
||||
}
|
||||
if showDiff {
|
||||
diff := gonp.New(getLines(existing), getLines([]byte(source)))
|
||||
diff.Compose()
|
||||
uniHunks := filterHunks(diff.UnifiedHunks())
|
||||
if len(uniHunks) == 0 {
|
||||
continue
|
||||
}
|
||||
rel := filepath.ToSlash(strings.TrimPrefix(filename, dir))
|
||||
fmt.Fprintf(stdout, "--- a%s\n", rel)
|
||||
fmt.Fprintf(stdout, "+++ b%s\n", rel)
|
||||
diff.FprintUniHunks(stdout, uniHunks)
|
||||
continue
|
||||
}
|
||||
if err := os.WriteFile(filename, []byte(source), 0644); err != nil {
|
||||
fmt.Fprintf(stderr, "%s: %s\n", filename, err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
cmd.Flags().Bool("diff", false, "display diffs instead of rewriting files")
|
||||
return cmd
|
||||
}
|
||||
|
||||
// Format formats every query file referenced by the configuration with its
|
||||
// engine's SQL formatter. It returns the formatted contents of each file,
|
||||
// keyed by absolute path. Query files for engines without a formatter are
|
||||
// skipped.
|
||||
func Format(ctx context.Context, dir, filename string, o *Options) (map[string]string, error) {
|
||||
defer trace.StartRegion(ctx, "format").End()
|
||||
stderr := o.Stderr
|
||||
|
||||
configPath, conf, err := o.ReadConfig(dir, filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
base := filepath.Base(configPath)
|
||||
if err := config.Validate(conf); err != nil {
|
||||
fmt.Fprintf(stderr, "error validating %s: %s\n", base, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
engines := map[string]config.Engine{}
|
||||
var files []string
|
||||
for _, sql := range conf.SQL {
|
||||
joined := make([]string, 0, len(sql.Queries))
|
||||
for _, q := range sql.Queries {
|
||||
joined = append(joined, filepath.Join(dir, q))
|
||||
}
|
||||
list, err := sqlpath.Glob(joined)
|
||||
if err != nil {
|
||||
fmt.Fprintf(stderr, "error listing queries: %s\n", err)
|
||||
return nil, err
|
||||
}
|
||||
for _, file := range list {
|
||||
if _, ok := engines[file]; ok {
|
||||
continue
|
||||
}
|
||||
engines[file] = sql.Engine
|
||||
files = append(files, file)
|
||||
}
|
||||
}
|
||||
|
||||
output := map[string]string{}
|
||||
unsupported := map[config.Engine]bool{}
|
||||
for _, file := range files {
|
||||
f := newQueryFormatter(engines[file])
|
||||
if f == nil {
|
||||
if !unsupported[engines[file]] {
|
||||
unsupported[engines[file]] = true
|
||||
fmt.Fprintf(stderr, "sqlc fmt does not yet support the %s engine; query files left unchanged\n", engines[file])
|
||||
}
|
||||
continue
|
||||
}
|
||||
rel, err := filepath.Rel(dir, file)
|
||||
if err != nil {
|
||||
rel = file
|
||||
}
|
||||
contents, err := os.ReadFile(file)
|
||||
if err != nil {
|
||||
fmt.Fprintf(stderr, "%s: skipped: %s\n", rel, err)
|
||||
continue
|
||||
}
|
||||
formatted, err := formatQueries(f, string(contents))
|
||||
if err != nil {
|
||||
// Not every query file parses without the compiler's help — for
|
||||
// example, some engines only accept sqlc.slice() after
|
||||
// preprocessing. Leave those files as they are.
|
||||
fmt.Fprintf(stderr, "%s: skipped: %s\n", rel, err)
|
||||
continue
|
||||
}
|
||||
// File-level belt for the reprinter path: no formatting result may
|
||||
// change the file's comments or its statement count. A violation is
|
||||
// a formatter bug; keep the file as written and say so.
|
||||
before, err1 := f.ParseFile(strings.NewReader(string(contents)))
|
||||
after, err2 := f.ParseFile(strings.NewReader(formatted))
|
||||
if err1 == nil && (err2 != nil || len(before.Stmts) != len(after.Stmts) || !sameComments(before.Comments, after.Comments)) {
|
||||
fmt.Fprintf(stderr, "%s: skipped: formatting would alter the file (this is a bug in sqlc fmt)\n", rel)
|
||||
continue
|
||||
}
|
||||
output[file] = formatted
|
||||
}
|
||||
return output, nil
|
||||
}
|
||||
|
||||
// formatQueries formats each statement in a query file. The comments above
|
||||
// every statement (that's where the "-- name:" annotation lives) are kept
|
||||
// as written; comments inside a statement are threaded through the printer
|
||||
// by position.
|
||||
func formatQueries(f queryFormatter, src string) (string, error) {
|
||||
// The parser hands over statements and comments from one lexer pass;
|
||||
// statement bodies slice the comment list by span.
|
||||
file, err := f.ParseFile(strings.NewReader(src))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
stmts, fileComments := file.Stmts, file.Comments
|
||||
var blocks []string
|
||||
prevEnd := 0
|
||||
for _, stmt := range stmts {
|
||||
if stmt.Raw == nil {
|
||||
continue
|
||||
}
|
||||
start := stmt.Raw.StmtLocation
|
||||
length := stmt.Raw.StmtLen
|
||||
if length == 0 {
|
||||
// An unterminated final statement is reported with a zero length.
|
||||
length = len(src) - start
|
||||
}
|
||||
if start < 0 || start+length > len(src) {
|
||||
return "", fmt.Errorf("statement location out of bounds")
|
||||
}
|
||||
// Engines differ on whether a statement's span covers the comments
|
||||
// above it, so start each segment where the previous one ended: the
|
||||
// gap holds nothing but the previous terminator, whitespace and
|
||||
// comments.
|
||||
segStart := prevEnd
|
||||
if start < segStart {
|
||||
segStart = start
|
||||
}
|
||||
if start+length <= prevEnd {
|
||||
continue
|
||||
}
|
||||
prevEnd = start + length
|
||||
seg := src[segStart : start+length]
|
||||
segOff := segStart
|
||||
// A comment on the same line as the previous statement's terminator
|
||||
// belongs to that statement, not to this one's header.
|
||||
if len(blocks) > 0 {
|
||||
if trailing, remainder := splitTrailingComment(seg); trailing != "" {
|
||||
blocks[len(blocks)-1] += " " + trailing
|
||||
segOff += len(seg) - len(remainder)
|
||||
seg = remainder
|
||||
}
|
||||
}
|
||||
orig := strings.TrimSpace(strings.TrimLeft(seg, "; \t\r\n"))
|
||||
if orig == "" {
|
||||
continue
|
||||
}
|
||||
header, rest := splitLeadingComments(orig)
|
||||
// The statement body's own file offset, so its interior comments
|
||||
// can be sliced out of the file's comment list in the same
|
||||
// coordinates the parser stamped on the nodes.
|
||||
restOff := segOff + strings.Index(seg, orig) + len(orig) - len(rest)
|
||||
var interior []ast.Comment
|
||||
for _, c := range fileComments {
|
||||
if c.Start >= restOff && c.End <= start+length {
|
||||
interior = append(interior, c)
|
||||
}
|
||||
}
|
||||
block := append([]string{}, header...)
|
||||
block = append(block, formatStmt(f, stmt.Raw, rest, interior, src))
|
||||
blocks = append(blocks, strings.Join(block, "\n"))
|
||||
}
|
||||
// Everything after the last statement is its terminator, whitespace and
|
||||
// trailing comments; keep the comments.
|
||||
tailSeg := src[prevEnd:]
|
||||
if len(blocks) > 0 {
|
||||
if trailing, remainder := splitTrailingComment(tailSeg); trailing != "" {
|
||||
blocks[len(blocks)-1] += " " + trailing
|
||||
tailSeg = remainder
|
||||
}
|
||||
}
|
||||
if tail := strings.TrimSpace(strings.TrimLeft(tailSeg, "; \t\r\n")); tail != "" {
|
||||
blocks = append(blocks, tail)
|
||||
}
|
||||
if len(blocks) == 0 {
|
||||
return src, nil
|
||||
}
|
||||
return strings.Join(blocks, "\n\n") + "\n", nil
|
||||
}
|
||||
|
||||
// splitTrailingComment splits a comment sitting on the same line as the
|
||||
// previous statement's terminator off the front of the text between two
|
||||
// statements, returning the comment (or "") and the remaining text.
|
||||
func splitTrailingComment(seg string) (string, string) {
|
||||
k := 0
|
||||
for k < len(seg) && (seg[k] == ';' || seg[k] == ' ' || seg[k] == '\t' || seg[k] == '\r') {
|
||||
k++
|
||||
}
|
||||
rest := seg[k:]
|
||||
line, _, _ := strings.Cut(rest, "\n")
|
||||
trimmed := strings.TrimSpace(line)
|
||||
switch {
|
||||
case strings.HasPrefix(trimmed, "--"), strings.HasPrefix(trimmed, "#"):
|
||||
return trimmed, seg[k+len(line):]
|
||||
case strings.HasPrefix(trimmed, "/*") &&
|
||||
strings.HasSuffix(trimmed, "*/") && strings.Count(trimmed, "*/") == 1:
|
||||
// A block comment contained on the terminator's line.
|
||||
return trimmed, seg[k+len(line):]
|
||||
}
|
||||
return "", seg
|
||||
}
|
||||
|
||||
// splitLeadingComments splits a statement's text into the comment lines above
|
||||
// it and the statement itself. Comment lines are returned with surrounding
|
||||
// whitespace trimmed (block comment continuation lines keep their leading
|
||||
// whitespace); blank lines between comments are kept.
|
||||
func splitLeadingComments(text string) ([]string, string) {
|
||||
var header []string
|
||||
rest := text
|
||||
for rest != "" {
|
||||
line, remainder, found := strings.Cut(rest, "\n")
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if strings.HasPrefix(trimmed, "/*") && !strings.Contains(trimmed, "*/") {
|
||||
// A block comment spanning several lines: commit it to the
|
||||
// header only when its closing line arrives with nothing after
|
||||
// it. Otherwise leave the whole block with the statement.
|
||||
block := []string{trimmed}
|
||||
blockRest := remainder
|
||||
for closed := false; !closed; {
|
||||
if !found || blockRest == "" {
|
||||
return header, rest
|
||||
}
|
||||
var blockLine string
|
||||
blockLine, blockRest, found = strings.Cut(blockRest, "\n")
|
||||
if end := strings.TrimSpace(blockLine); strings.Contains(end, "*/") {
|
||||
if !strings.HasSuffix(end, "*/") || strings.Count(end, "*/") != 1 {
|
||||
return header, rest
|
||||
}
|
||||
closed = true
|
||||
}
|
||||
block = append(block, strings.TrimRight(blockLine, " \t\r"))
|
||||
}
|
||||
header = append(header, block...)
|
||||
rest = blockRest
|
||||
continue
|
||||
}
|
||||
if !isCommentLine(trimmed) {
|
||||
break
|
||||
}
|
||||
header = append(header, trimmed)
|
||||
if !found {
|
||||
rest = ""
|
||||
break
|
||||
}
|
||||
rest = remainder
|
||||
}
|
||||
// A statement never ends with a comment-only line here (the parser found a
|
||||
// statement in this segment), but drop any blank lines left at the end of
|
||||
// the header so comments sit directly above their query.
|
||||
for len(header) > 0 && header[len(header)-1] == "" {
|
||||
header = header[:len(header)-1]
|
||||
}
|
||||
return header, rest
|
||||
}
|
||||
|
||||
func isCommentLine(line string) bool {
|
||||
switch {
|
||||
case line == "":
|
||||
// A blank line between comments.
|
||||
return true
|
||||
case strings.HasPrefix(line, "--"):
|
||||
return true
|
||||
case strings.HasPrefix(line, "#"):
|
||||
// MySQL's line-comment syntax; text that reaches these helpers sits
|
||||
// outside statements, where a # line in a file the engine parsed
|
||||
// can only be a comment.
|
||||
return true
|
||||
case strings.HasPrefix(line, "/*") && strings.HasSuffix(line, "*/") && strings.Count(line, "*/") == 1:
|
||||
// A block comment contained on a single line.
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// fingerprinter is implemented by engines that can reduce a query to a
|
||||
// fingerprint that survives changes in whitespace, case and layout —
|
||||
// PostgreSQL via oliphant's pg_query-compatible Fingerprint. Where it is
|
||||
// available, fmt gets a proof the other checks cannot give: the formatted
|
||||
// statement still parses to the same query as the original.
|
||||
type fingerprinter interface {
|
||||
Fingerprint(string) (string, error)
|
||||
}
|
||||
|
||||
// formatStmt returns the canonical form of a single statement, or the
|
||||
// original text when formatting cannot be proven to preserve the query.
|
||||
func formatStmt(f queryFormatter, raw *ast.RawStmt, orig string, interior []ast.Comment, src string) string {
|
||||
fallback := strings.TrimSuffix(strings.TrimSpace(orig), ";") + ";"
|
||||
if out, ok := formatWithComments(f, raw, interior, src); ok {
|
||||
if fp, ok := f.(fingerprinter); ok && !sameFingerprint(fp, orig, out) {
|
||||
return fallback
|
||||
}
|
||||
return out
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
// sameFingerprint reports that both texts fingerprint successfully to the
|
||||
// same value. Anything less is not a proof, so the caller keeps the
|
||||
// statement as written.
|
||||
func sameFingerprint(fp fingerprinter, orig, out string) bool {
|
||||
a, err1 := fp.Fingerprint(orig)
|
||||
b, err2 := fp.Fingerprint(out)
|
||||
return err1 == nil && err2 == nil && a == b
|
||||
}
|
||||
|
||||
// formatWithComments pretty-prints a statement with its interior comments
|
||||
// and proves the result faithful three ways before accepting it: every
|
||||
// comment survives (multiset equality after reparsing the output), the SQL
|
||||
// still parses as one statement, and the output is a fixed point (printing
|
||||
// the reparsed statement with its reparsed comments reproduces it).
|
||||
func formatWithComments(f queryFormatter, raw *ast.RawStmt, interior []ast.Comment, src string) (string, bool) {
|
||||
out := prettyCommented(raw, f, interior, src)
|
||||
if strings.TrimSpace(strings.TrimSuffix(out, ";")) == "" {
|
||||
return "", false
|
||||
}
|
||||
file, err := f.ParseFile(strings.NewReader(out))
|
||||
if err != nil || len(file.Stmts) != 1 || file.Stmts[0].Raw == nil {
|
||||
return "", false
|
||||
}
|
||||
if !sameComments(interior, file.Comments) {
|
||||
return "", false
|
||||
}
|
||||
again := prettyCommented(file.Stmts[0].Raw, f, file.Comments, out)
|
||||
if !strings.EqualFold(again, out) {
|
||||
return "", false
|
||||
}
|
||||
return out, true
|
||||
}
|
||||
|
||||
// prettyCommented attaches a statement's comments to its nodes and renders
|
||||
// it, returning "" when the printer panics or fails to place every comment.
|
||||
func prettyCommented(raw *ast.RawStmt, f queryFormatter, comments []ast.Comment, src string) (out string) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
out = ""
|
||||
}
|
||||
}()
|
||||
ct := ast.AttachComments(raw, f, comments, src)
|
||||
out = ast.PrettyWithComments(raw, f, noLineLimit, ct)
|
||||
if !ct.Exhausted() {
|
||||
return ""
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// sameComments reports whether two comment lists carry the same comment
|
||||
// texts, as multisets, ignoring surrounding whitespace.
|
||||
func sameComments(a, b []ast.Comment) bool {
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
}
|
||||
counts := make(map[string]int, len(a))
|
||||
for _, c := range a {
|
||||
counts[strings.TrimSpace(c.Text)]++
|
||||
}
|
||||
for _, c := range b {
|
||||
counts[strings.TrimSpace(c.Text)]--
|
||||
}
|
||||
for _, n := range counts {
|
||||
if n != 0 {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
376
internal/cmd/generate.go
Normal file
376
internal/cmd/generate.go
Normal file
@@ -0,0 +1,376 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime/trace"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
|
||||
"github.com/sqlc-dev/sqlc/internal/codegen/golang"
|
||||
genjson "github.com/sqlc-dev/sqlc/internal/codegen/json"
|
||||
"github.com/sqlc-dev/sqlc/internal/compiler"
|
||||
"github.com/sqlc-dev/sqlc/internal/config"
|
||||
"github.com/sqlc-dev/sqlc/internal/config/convert"
|
||||
"github.com/sqlc-dev/sqlc/internal/debug"
|
||||
"github.com/sqlc-dev/sqlc/internal/ext"
|
||||
"github.com/sqlc-dev/sqlc/internal/ext/process"
|
||||
"github.com/sqlc-dev/sqlc/internal/ext/wasm"
|
||||
"github.com/sqlc-dev/sqlc/internal/multierr"
|
||||
"github.com/sqlc-dev/sqlc/internal/opts"
|
||||
"github.com/sqlc-dev/sqlc/internal/plugin"
|
||||
"github.com/sqlc-dev/sqlc/internal/sqlcdebug"
|
||||
)
|
||||
|
||||
var debugDumpCatalog = sqlcdebug.New("dumpcatalog")
|
||||
|
||||
const errMessageNoVersion = `The configuration file must have a version number.
|
||||
Set the version to 1 or 2 at the top of sqlc.json:
|
||||
|
||||
{
|
||||
"version": "1"
|
||||
...
|
||||
}
|
||||
`
|
||||
|
||||
const errMessageUnknownVersion = `The configuration file has an invalid version number.
|
||||
The supported version can only be "1" or "2".
|
||||
`
|
||||
|
||||
const errMessageNoPackages = `No packages are configured`
|
||||
|
||||
func printFileErr(stderr io.Writer, dir string, fileErr *multierr.FileError) {
|
||||
filename, err := filepath.Rel(dir, fileErr.Filename)
|
||||
if err != nil {
|
||||
filename = fileErr.Filename
|
||||
}
|
||||
fmt.Fprintf(stderr, "%s:%d:%d: %s\n", filename, fileErr.Line, fileErr.Column, fileErr.Err)
|
||||
}
|
||||
|
||||
func findPlugin(conf config.Config, name string) (*config.Plugin, error) {
|
||||
for _, plug := range conf.Plugins {
|
||||
if plug.Name == name {
|
||||
return &plug, nil
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("plugin not found")
|
||||
}
|
||||
|
||||
func readConfig(stderr io.Writer, dir, filename string) (string, *config.Config, error) {
|
||||
configPath := ""
|
||||
if filename != "" {
|
||||
configPath = filepath.Join(dir, filename)
|
||||
} else {
|
||||
var yamlMissing, jsonMissing, ymlMissing bool
|
||||
yamlPath := filepath.Join(dir, "sqlc.yaml")
|
||||
ymlPath := filepath.Join(dir, "sqlc.yml")
|
||||
jsonPath := filepath.Join(dir, "sqlc.json")
|
||||
|
||||
if _, err := os.Stat(yamlPath); os.IsNotExist(err) {
|
||||
yamlMissing = true
|
||||
}
|
||||
if _, err := os.Stat(jsonPath); os.IsNotExist(err) {
|
||||
jsonMissing = true
|
||||
}
|
||||
|
||||
if _, err := os.Stat(ymlPath); os.IsNotExist(err) {
|
||||
ymlMissing = true
|
||||
}
|
||||
|
||||
if yamlMissing && ymlMissing && jsonMissing {
|
||||
fmt.Fprintln(stderr, "error parsing configuration files. sqlc.(yaml|yml) or sqlc.json: file does not exist")
|
||||
return "", nil, errors.New("config file missing")
|
||||
}
|
||||
|
||||
if (!yamlMissing || !ymlMissing) && !jsonMissing {
|
||||
fmt.Fprintln(stderr, "error: both sqlc.json and sqlc.(yaml|yml) files present")
|
||||
return "", nil, errors.New("sqlc.json and sqlc.(yaml|yml) present")
|
||||
}
|
||||
|
||||
if jsonMissing {
|
||||
if yamlMissing {
|
||||
configPath = ymlPath
|
||||
} else {
|
||||
configPath = yamlPath
|
||||
}
|
||||
} else {
|
||||
configPath = jsonPath
|
||||
}
|
||||
}
|
||||
|
||||
base := filepath.Base(configPath)
|
||||
file, err := os.Open(configPath)
|
||||
if err != nil {
|
||||
fmt.Fprintf(stderr, "error parsing %s: file does not exist\n", base)
|
||||
return "", nil, err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
conf, err := config.ParseConfig(file)
|
||||
if err != nil {
|
||||
switch err {
|
||||
case config.ErrMissingVersion:
|
||||
fmt.Fprint(stderr, errMessageNoVersion)
|
||||
case config.ErrUnknownVersion:
|
||||
fmt.Fprint(stderr, errMessageUnknownVersion)
|
||||
case config.ErrNoPackages:
|
||||
fmt.Fprint(stderr, errMessageNoPackages)
|
||||
}
|
||||
fmt.Fprintf(stderr, "error parsing %s: %s\n", base, err)
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
return configPath, &conf, nil
|
||||
}
|
||||
|
||||
func Generate(ctx context.Context, dir, filename string, o *Options) (map[string]string, error) {
|
||||
e := o.Env
|
||||
stderr := o.Stderr
|
||||
|
||||
configPath, conf, err := o.ReadConfig(dir, filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
base := filepath.Base(configPath)
|
||||
if err := config.Validate(conf); err != nil {
|
||||
fmt.Fprintf(stderr, "error validating %s: %s\n", base, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := e.Validate(conf); err != nil {
|
||||
fmt.Fprintf(stderr, "error validating %s: %s\n", base, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
g := &generator{
|
||||
dir: dir,
|
||||
output: map[string]string{},
|
||||
}
|
||||
|
||||
if err := processQuerySets(ctx, g, conf, dir, o); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return g.output, nil
|
||||
}
|
||||
|
||||
type generator struct {
|
||||
m sync.Mutex
|
||||
dir string
|
||||
output map[string]string
|
||||
}
|
||||
|
||||
func (g *generator) Pairs(ctx context.Context, conf *config.Config) []OutputPair {
|
||||
var pairs []OutputPair
|
||||
for _, sql := range conf.SQL {
|
||||
if sql.Gen.Go != nil {
|
||||
pairs = append(pairs, OutputPair{
|
||||
SQL: sql,
|
||||
Gen: config.SQLGen{Go: sql.Gen.Go},
|
||||
})
|
||||
}
|
||||
if sql.Gen.JSON != nil {
|
||||
pairs = append(pairs, OutputPair{
|
||||
SQL: sql,
|
||||
Gen: config.SQLGen{JSON: sql.Gen.JSON},
|
||||
})
|
||||
}
|
||||
for i := range sql.Codegen {
|
||||
pairs = append(pairs, OutputPair{
|
||||
SQL: sql,
|
||||
Plugin: &sql.Codegen[i],
|
||||
})
|
||||
}
|
||||
}
|
||||
return pairs
|
||||
}
|
||||
|
||||
func (g *generator) ProcessResult(ctx context.Context, combo config.CombinedSettings, sql OutputPair, result *compiler.Result) error {
|
||||
out, resp, err := codegen(ctx, combo, sql, result)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
files := map[string]string{}
|
||||
for _, file := range resp.Files {
|
||||
files[file.Name] = string(file.Contents)
|
||||
}
|
||||
g.m.Lock()
|
||||
|
||||
// out is specified by the user, not a plugin
|
||||
absout := filepath.Join(g.dir, out)
|
||||
|
||||
// When the Go codegen is configured to emit the models file into a
|
||||
// separate package directory, route that file to its own absolute path.
|
||||
// This is the only file allowed to live outside of `out`.
|
||||
var (
|
||||
modelsFileName string
|
||||
modelsAbsout string
|
||||
modelsAbsfile string
|
||||
)
|
||||
if sql.Gen.Go != nil && sql.Gen.Go.OutputModelsPath != "" && sql.Gen.Go.ModelsEmitEnabled() {
|
||||
modelsFileName = sql.Gen.Go.OutputModelsFileName
|
||||
if modelsFileName == "" {
|
||||
modelsFileName = "models.go"
|
||||
}
|
||||
modelsAbsout = filepath.Join(g.dir, sql.Gen.Go.OutputModelsPath)
|
||||
modelsAbsfile = filepath.Join(modelsAbsout, modelsFileName)
|
||||
}
|
||||
|
||||
for n, source := range files {
|
||||
if modelsFileName != "" && n == modelsFileName {
|
||||
// Models file routed to a separate package directory.
|
||||
if strings.Contains(modelsAbsfile, "..") {
|
||||
return fmt.Errorf("invalid file output path: %s", modelsAbsfile)
|
||||
}
|
||||
if !strings.HasPrefix(modelsAbsfile, modelsAbsout) {
|
||||
return fmt.Errorf("invalid file output path: %s", modelsAbsfile)
|
||||
}
|
||||
g.output[modelsAbsfile] = source
|
||||
continue
|
||||
}
|
||||
filename := filepath.Join(g.dir, out, n)
|
||||
// filepath.Join calls filepath.Clean which should remove all "..", but
|
||||
// double check to make sure
|
||||
if strings.Contains(filename, "..") {
|
||||
return fmt.Errorf("invalid file output path: %s", filename)
|
||||
}
|
||||
// The output file must be contained inside the output directory
|
||||
if !strings.HasPrefix(filename, absout) {
|
||||
return fmt.Errorf("invalid file output path: %s", filename)
|
||||
}
|
||||
g.output[filename] = source
|
||||
}
|
||||
g.m.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
func parse(ctx context.Context, name, dir string, sql config.SQL, combo config.CombinedSettings, parserOpts opts.Parser, stderr io.Writer) (*compiler.Result, bool) {
|
||||
defer trace.StartRegion(ctx, "parse").End()
|
||||
var copts []compiler.Option
|
||||
if parserOpts.Experiment.CoreAnalyzer {
|
||||
copts = append(copts, compiler.WithCoreAnalysis())
|
||||
}
|
||||
c, err := compiler.NewCompiler(sql, combo, parserOpts, copts...)
|
||||
defer func() {
|
||||
if c != nil {
|
||||
c.Close(ctx)
|
||||
}
|
||||
}()
|
||||
if err != nil {
|
||||
fmt.Fprintf(stderr, "error creating compiler: %s\n", err)
|
||||
return nil, true
|
||||
}
|
||||
if err := c.ParseCatalog(sql.Schema); err != nil {
|
||||
fmt.Fprintf(stderr, "# package %s\n", name)
|
||||
if parserErr, ok := err.(*multierr.Error); ok {
|
||||
for _, fileErr := range parserErr.Errs() {
|
||||
printFileErr(stderr, dir, fileErr)
|
||||
}
|
||||
} else {
|
||||
fmt.Fprintf(stderr, "error parsing schema: %s\n", err)
|
||||
}
|
||||
return nil, true
|
||||
}
|
||||
if debugDumpCatalog.Value() == "1" {
|
||||
debug.Dump(c.Catalog())
|
||||
}
|
||||
if err := c.ParseQueries(sql.Queries, parserOpts); err != nil {
|
||||
fmt.Fprintf(stderr, "# package %s\n", name)
|
||||
if parserErr, ok := err.(*multierr.Error); ok {
|
||||
for _, fileErr := range parserErr.Errs() {
|
||||
printFileErr(stderr, dir, fileErr)
|
||||
}
|
||||
} else {
|
||||
fmt.Fprintf(stderr, "error parsing queries: %s\n", err)
|
||||
}
|
||||
return nil, true
|
||||
}
|
||||
return c.Result(), false
|
||||
}
|
||||
|
||||
func codegen(ctx context.Context, combo config.CombinedSettings, sql OutputPair, result *compiler.Result) (string, *plugin.GenerateResponse, error) {
|
||||
defer trace.StartRegion(ctx, "codegen").End()
|
||||
req := codeGenRequest(result, combo)
|
||||
var handler grpc.ClientConnInterface
|
||||
var out string
|
||||
switch {
|
||||
case sql.Plugin != nil:
|
||||
out = sql.Plugin.Out
|
||||
plug, err := findPlugin(combo.Global, sql.Plugin.Plugin)
|
||||
if err != nil {
|
||||
return "", nil, fmt.Errorf("plugin not found: %s", err)
|
||||
}
|
||||
|
||||
switch {
|
||||
case plug.Process != nil:
|
||||
handler = &process.Runner{
|
||||
Cmd: plug.Process.Cmd,
|
||||
Env: plug.Env,
|
||||
Format: plug.Process.Format,
|
||||
}
|
||||
case plug.WASM != nil:
|
||||
handler = &wasm.Runner{
|
||||
URL: plug.WASM.URL,
|
||||
SHA256: plug.WASM.SHA256,
|
||||
Env: plug.Env,
|
||||
}
|
||||
default:
|
||||
return "", nil, fmt.Errorf("unsupported plugin type")
|
||||
}
|
||||
|
||||
opts, err := convert.YAMLtoJSON(sql.Plugin.Options)
|
||||
if err != nil {
|
||||
return "", nil, fmt.Errorf("invalid plugin options: %w", err)
|
||||
}
|
||||
req.PluginOptions = opts
|
||||
|
||||
global, found := combo.Global.Options[plug.Name]
|
||||
if found {
|
||||
opts, err := convert.YAMLtoJSON(global)
|
||||
if err != nil {
|
||||
return "", nil, fmt.Errorf("invalid global options: %w", err)
|
||||
}
|
||||
req.GlobalOptions = opts
|
||||
}
|
||||
|
||||
case sql.Gen.Go != nil:
|
||||
out = combo.Go.Out
|
||||
handler = ext.HandleFunc(golang.Generate)
|
||||
opts, err := json.Marshal(sql.Gen.Go)
|
||||
if err != nil {
|
||||
return "", nil, fmt.Errorf("opts marshal failed: %w", err)
|
||||
}
|
||||
req.PluginOptions = opts
|
||||
|
||||
if combo.Global.Overrides.Go != nil {
|
||||
opts, err := json.Marshal(combo.Global.Overrides.Go)
|
||||
if err != nil {
|
||||
return "", nil, fmt.Errorf("opts marshal failed: %w", err)
|
||||
}
|
||||
req.GlobalOptions = opts
|
||||
}
|
||||
|
||||
case sql.Gen.JSON != nil:
|
||||
out = combo.JSON.Out
|
||||
handler = ext.HandleFunc(genjson.Generate)
|
||||
opts, err := json.Marshal(sql.Gen.JSON)
|
||||
if err != nil {
|
||||
return "", nil, fmt.Errorf("opts marshal failed: %w", err)
|
||||
}
|
||||
req.PluginOptions = opts
|
||||
|
||||
default:
|
||||
return "", nil, fmt.Errorf("missing language backend")
|
||||
}
|
||||
client := plugin.NewCodegenServiceClient(handler)
|
||||
resp, err := client.Generate(ctx, req)
|
||||
return out, resp, err
|
||||
}
|
||||
29
internal/cmd/options.go
Normal file
29
internal/cmd/options.go
Normal file
@@ -0,0 +1,29 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
"github.com/sqlc-dev/sqlc/internal/config"
|
||||
)
|
||||
|
||||
type Options struct {
|
||||
Env Env
|
||||
Stderr io.Writer
|
||||
// TODO: Move these to a command-specific struct
|
||||
Tags []string
|
||||
Against string
|
||||
|
||||
// Testing only
|
||||
MutateConfig func(*config.Config)
|
||||
}
|
||||
|
||||
func (o *Options) ReadConfig(dir, filename string) (string, *config.Config, error) {
|
||||
path, conf, err := readConfig(o.Stderr, dir, filename)
|
||||
if err != nil {
|
||||
return path, conf, err
|
||||
}
|
||||
if o.MutateConfig != nil {
|
||||
o.MutateConfig(conf)
|
||||
}
|
||||
return path, conf, nil
|
||||
}
|
||||
166
internal/cmd/parse.go
Normal file
166
internal/cmd/parse.go
Normal file
@@ -0,0 +1,166 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/sqlc-dev/sqlc/internal/engine/clickhouse"
|
||||
"github.com/sqlc-dev/sqlc/internal/engine/dolphin"
|
||||
"github.com/sqlc-dev/sqlc/internal/engine/duckdb"
|
||||
"github.com/sqlc-dev/sqlc/internal/engine/googlesql"
|
||||
"github.com/sqlc-dev/sqlc/internal/engine/mssql"
|
||||
"github.com/sqlc-dev/sqlc/internal/engine/postgresql"
|
||||
"github.com/sqlc-dev/sqlc/internal/engine/sqlite"
|
||||
"github.com/sqlc-dev/sqlc/internal/metadata"
|
||||
"github.com/sqlc-dev/sqlc/internal/source"
|
||||
"github.com/sqlc-dev/sqlc/internal/sql/ast"
|
||||
)
|
||||
|
||||
// dialectParser is the subset of the engine parsers that the parse command
|
||||
// needs: parsing SQL into statements and reporting the dialect's comment syntax
|
||||
// (used to extract the sqlc query name and command).
|
||||
type dialectParser interface {
|
||||
Parse(io.Reader) ([]ast.Statement, error)
|
||||
CommentSyntax() source.CommentSyntax
|
||||
}
|
||||
|
||||
// parsedStatement is the JSON representation of a single parsed statement. The
|
||||
// name and cmd are extracted from the sqlc query annotation (e.g.
|
||||
// "-- name: GetAuthor :one") and are omitted when the statement has none.
|
||||
type parsedStatement struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
Cmd string `json:"cmd,omitempty"`
|
||||
AST *ast.RawStmt `json:"ast"`
|
||||
}
|
||||
|
||||
func newParseCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "parse [file]",
|
||||
Short: "Parse SQL and output the AST as JSON",
|
||||
Long: `Parse SQL from a file or stdin and output the abstract syntax tree as JSON.
|
||||
|
||||
Each statement is reported with its sqlc query name and command (when the
|
||||
statement carries a "-- name:" annotation) alongside the AST.
|
||||
|
||||
Examples:
|
||||
# Parse a SQL file with PostgreSQL dialect
|
||||
sqlc parse --dialect postgresql schema.sql
|
||||
|
||||
# Parse from stdin with MySQL dialect
|
||||
echo "SELECT * FROM users" | sqlc parse --dialect mysql
|
||||
|
||||
# Parse SQLite SQL
|
||||
sqlc parse --dialect sqlite queries.sql
|
||||
|
||||
# Parse ClickHouse SQL
|
||||
sqlc parse --dialect clickhouse queries.sql
|
||||
|
||||
# Parse GoogleSQL (BigQuery, Spanner)
|
||||
sqlc parse --dialect googlesql queries.sql
|
||||
|
||||
# Parse SQL Server (T-SQL) SQL
|
||||
sqlc parse --dialect mssql queries.sql
|
||||
|
||||
# Parse DuckDB SQL
|
||||
sqlc parse --dialect duckdb queries.sql`,
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
dialect, err := cmd.Flags().GetString("dialect")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if dialect == "" {
|
||||
return fmt.Errorf("--dialect flag is required (postgresql, mysql, sqlite, clickhouse, googlesql, mssql, or duckdb)")
|
||||
}
|
||||
|
||||
// Determine input source
|
||||
var input io.Reader
|
||||
if len(args) == 1 {
|
||||
file, err := os.Open(args[0])
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open file: %w", err)
|
||||
}
|
||||
defer file.Close()
|
||||
input = file
|
||||
} else {
|
||||
// Check if stdin has data
|
||||
stat, err := os.Stdin.Stat()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to stat stdin: %w", err)
|
||||
}
|
||||
if (stat.Mode() & os.ModeCharDevice) != 0 {
|
||||
return fmt.Errorf("no input provided. Specify a file path or pipe SQL via stdin")
|
||||
}
|
||||
input = cmd.InOrStdin()
|
||||
}
|
||||
|
||||
// Select the parser for the requested dialect
|
||||
var parser dialectParser
|
||||
switch dialect {
|
||||
case "postgresql", "postgres", "pg":
|
||||
parser = postgresql.NewParser()
|
||||
case "mysql":
|
||||
parser = dolphin.NewParser()
|
||||
case "sqlite":
|
||||
parser = sqlite.NewParser()
|
||||
case "clickhouse":
|
||||
parser = clickhouse.NewParser()
|
||||
case "googlesql":
|
||||
parser = googlesql.NewParser()
|
||||
case "mssql", "sqlserver":
|
||||
parser = mssql.NewParser()
|
||||
case "duckdb":
|
||||
parser = duckdb.NewParser()
|
||||
default:
|
||||
return fmt.Errorf("unsupported dialect: %s (use postgresql, mysql, sqlite, clickhouse, googlesql, mssql, or duckdb)", dialect)
|
||||
}
|
||||
|
||||
// Read the full source so each statement's name and command can be
|
||||
// extracted from its annotation comment.
|
||||
src, err := io.ReadAll(input)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read input: %w", err)
|
||||
}
|
||||
|
||||
stmts, err := parser.Parse(strings.NewReader(string(src)))
|
||||
if err != nil {
|
||||
return fmt.Errorf("parse error: %w", err)
|
||||
}
|
||||
|
||||
commentSyntax := metadata.CommentSyntax(parser.CommentSyntax())
|
||||
|
||||
// Output the AST as a single JSON document
|
||||
out := make([]parsedStatement, 0, len(stmts))
|
||||
for _, stmt := range stmts {
|
||||
ps := parsedStatement{AST: stmt.Raw}
|
||||
rawSQL, err := source.Pluck(string(src), stmt.Raw.StmtLocation, stmt.Raw.StmtLen)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read statement source: %w", err)
|
||||
}
|
||||
name, cmd, err := metadata.ParseQueryNameAndType(rawSQL, commentSyntax)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse query annotation: %w", err)
|
||||
}
|
||||
ps.Name = name
|
||||
ps.Cmd = cmd
|
||||
out = append(out, ps)
|
||||
}
|
||||
|
||||
stdout := cmd.OutOrStdout()
|
||||
encoder := json.NewEncoder(stdout)
|
||||
encoder.SetIndent("", " ")
|
||||
if err := encoder.Encode(out); err != nil {
|
||||
return fmt.Errorf("failed to encode AST: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
cmd.Flags().StringP("dialect", "d", "", "SQL dialect to use (postgresql, mysql, sqlite, clickhouse, googlesql, mssql, or duckdb)")
|
||||
return cmd
|
||||
}
|
||||
133
internal/cmd/process.go
Normal file
133
internal/cmd/process.go
Normal file
@@ -0,0 +1,133 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"runtime/trace"
|
||||
|
||||
"golang.org/x/sync/errgroup"
|
||||
|
||||
"github.com/sqlc-dev/sqlc/internal/compiler"
|
||||
"github.com/sqlc-dev/sqlc/internal/config"
|
||||
"github.com/sqlc-dev/sqlc/internal/opts"
|
||||
)
|
||||
|
||||
type OutputPair struct {
|
||||
Gen config.SQLGen
|
||||
Plugin *config.Codegen
|
||||
|
||||
config.SQL
|
||||
}
|
||||
|
||||
type ResultProcessor interface {
|
||||
Pairs(context.Context, *config.Config) []OutputPair
|
||||
ProcessResult(context.Context, config.CombinedSettings, OutputPair, *compiler.Result) error
|
||||
}
|
||||
|
||||
func Process(ctx context.Context, rp ResultProcessor, dir, filename string, o *Options) error {
|
||||
e := o.Env
|
||||
stderr := o.Stderr
|
||||
|
||||
configPath, conf, err := o.ReadConfig(dir, filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
base := filepath.Base(configPath)
|
||||
if err := config.Validate(conf); err != nil {
|
||||
fmt.Fprintf(stderr, "error validating %s: %s\n", base, err)
|
||||
return err
|
||||
}
|
||||
|
||||
if err := e.Validate(conf); err != nil {
|
||||
fmt.Fprintf(stderr, "error validating %s: %s\n", base, err)
|
||||
return err
|
||||
}
|
||||
|
||||
return processQuerySets(ctx, rp, conf, dir, o)
|
||||
}
|
||||
|
||||
func processQuerySets(ctx context.Context, rp ResultProcessor, conf *config.Config, dir string, o *Options) error {
|
||||
stderr := o.Stderr
|
||||
|
||||
errored := false
|
||||
|
||||
pairs := rp.Pairs(ctx, conf)
|
||||
grp, gctx := errgroup.WithContext(ctx)
|
||||
grp.SetLimit(runtime.GOMAXPROCS(0))
|
||||
|
||||
stderrs := make([]bytes.Buffer, len(pairs))
|
||||
|
||||
for i, pair := range pairs {
|
||||
sql := pair
|
||||
errout := &stderrs[i]
|
||||
|
||||
grp.Go(func() error {
|
||||
combo := config.Combine(*conf, sql.SQL)
|
||||
if sql.Plugin != nil {
|
||||
combo.Codegen = *sql.Plugin
|
||||
}
|
||||
|
||||
// TODO: This feels like a hack that will bite us later
|
||||
joined := make([]string, 0, len(sql.Schema))
|
||||
for _, s := range sql.Schema {
|
||||
joined = append(joined, filepath.Join(dir, s))
|
||||
}
|
||||
sql.Schema = joined
|
||||
|
||||
joined = make([]string, 0, len(sql.Queries))
|
||||
for _, q := range sql.Queries {
|
||||
joined = append(joined, filepath.Join(dir, q))
|
||||
}
|
||||
sql.Queries = joined
|
||||
|
||||
var name, lang string
|
||||
parseOpts := opts.Parser{
|
||||
Experiment: o.Env.Experiment,
|
||||
}
|
||||
|
||||
switch {
|
||||
case sql.Gen.Go != nil:
|
||||
name = combo.Go.Package
|
||||
lang = "golang"
|
||||
|
||||
case sql.Plugin != nil:
|
||||
lang = fmt.Sprintf("process:%s", sql.Plugin.Plugin)
|
||||
name = sql.Plugin.Plugin
|
||||
}
|
||||
|
||||
packageRegion := trace.StartRegion(gctx, "package")
|
||||
trace.Logf(gctx, "", "name=%s dir=%s plugin=%s", name, dir, lang)
|
||||
|
||||
result, failed := parse(gctx, name, dir, sql.SQL, combo, parseOpts, errout)
|
||||
if failed {
|
||||
packageRegion.End()
|
||||
errored = true
|
||||
return nil
|
||||
}
|
||||
if err := rp.ProcessResult(gctx, combo, sql, result); err != nil {
|
||||
fmt.Fprintf(errout, "# package %s\n", name)
|
||||
fmt.Fprintf(errout, "error generating code: %s\n", err)
|
||||
errored = true
|
||||
}
|
||||
packageRegion.End()
|
||||
return nil
|
||||
})
|
||||
}
|
||||
if err := grp.Wait(); err != nil {
|
||||
return err
|
||||
}
|
||||
if errored {
|
||||
for i, _ := range stderrs {
|
||||
if _, err := io.Copy(stderr, &stderrs[i]); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("errored")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
92
internal/cmd/push.go
Normal file
92
internal/cmd/push.go
Normal file
@@ -0,0 +1,92 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/sqlc-dev/sqlc/internal/bundler"
|
||||
"github.com/sqlc-dev/sqlc/internal/compiler"
|
||||
"github.com/sqlc-dev/sqlc/internal/config"
|
||||
)
|
||||
|
||||
func init() {
|
||||
pushCmd.Flags().StringSliceP("tag", "t", nil, "tag this push with a value")
|
||||
}
|
||||
|
||||
var pushCmd = &cobra.Command{
|
||||
Use: "push",
|
||||
Aliases: []string{"upload"},
|
||||
Short: "Push the schema, queries, and configuration for this project",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
stderr := cmd.ErrOrStderr()
|
||||
dir, name := getConfigPath(stderr, cmd.Flag("file"))
|
||||
tags, err := cmd.Flags().GetStringSlice("tag")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
opts := &Options{
|
||||
Env: ParseEnv(cmd),
|
||||
Stderr: stderr,
|
||||
Tags: tags,
|
||||
}
|
||||
if err := Push(cmd.Context(), dir, name, opts); err != nil {
|
||||
fmt.Fprintf(stderr, "error pushing: %s\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
type pusher struct {
|
||||
m sync.Mutex
|
||||
results []*bundler.QuerySetArchive
|
||||
}
|
||||
|
||||
func (g *pusher) Pairs(ctx context.Context, conf *config.Config) []OutputPair {
|
||||
var pairs []OutputPair
|
||||
for _, sql := range conf.SQL {
|
||||
pairs = append(pairs, OutputPair{
|
||||
SQL: sql,
|
||||
})
|
||||
}
|
||||
return pairs
|
||||
}
|
||||
|
||||
func (g *pusher) ProcessResult(ctx context.Context, combo config.CombinedSettings, sql OutputPair, result *compiler.Result) error {
|
||||
req := codeGenRequest(result, combo)
|
||||
g.m.Lock()
|
||||
g.results = append(g.results, &bundler.QuerySetArchive{
|
||||
Name: sql.Name,
|
||||
Schema: sql.Schema,
|
||||
Queries: sql.Queries,
|
||||
Request: req,
|
||||
})
|
||||
g.m.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
func Push(ctx context.Context, dir, filename string, opts *Options) error {
|
||||
e := opts.Env
|
||||
stderr := opts.Stderr
|
||||
configPath, conf, err := readConfig(stderr, dir, filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
up := bundler.NewUploader(configPath, dir, conf)
|
||||
if err := up.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
p := &pusher{}
|
||||
if err := Process(ctx, p, dir, filename, opts); err != nil {
|
||||
return err
|
||||
}
|
||||
if e.DryRun {
|
||||
return up.DumpRequestOut(ctx, p.results)
|
||||
} else {
|
||||
return up.Upload(ctx, p.results, opts.Tags)
|
||||
}
|
||||
}
|
||||
233
internal/cmd/shim.go
Normal file
233
internal/cmd/shim.go
Normal file
@@ -0,0 +1,233 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/sqlc-dev/sqlc/internal/compiler"
|
||||
"github.com/sqlc-dev/sqlc/internal/config"
|
||||
"github.com/sqlc-dev/sqlc/internal/config/convert"
|
||||
"github.com/sqlc-dev/sqlc/internal/info"
|
||||
"github.com/sqlc-dev/sqlc/internal/plugin"
|
||||
"github.com/sqlc-dev/sqlc/internal/sql/catalog"
|
||||
)
|
||||
|
||||
func pluginSettings(r *compiler.Result, cs config.CombinedSettings) *plugin.Settings {
|
||||
return &plugin.Settings{
|
||||
Version: cs.Global.Version,
|
||||
Engine: string(cs.Package.Engine),
|
||||
Schema: []string(cs.Package.Schema),
|
||||
Queries: []string(cs.Package.Queries),
|
||||
Codegen: pluginCodegen(cs, cs.Codegen),
|
||||
}
|
||||
}
|
||||
|
||||
func pluginCodegen(cs config.CombinedSettings, s config.Codegen) *plugin.Codegen {
|
||||
opts, err := convert.YAMLtoJSON(s.Options)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
cg := &plugin.Codegen{
|
||||
Out: s.Out,
|
||||
Plugin: s.Plugin,
|
||||
Options: opts,
|
||||
}
|
||||
for _, p := range cs.Global.Plugins {
|
||||
if p.Name == s.Plugin {
|
||||
cg.Env = p.Env
|
||||
cg.Process = pluginProcess(p)
|
||||
cg.Wasm = pluginWASM(p)
|
||||
return cg
|
||||
}
|
||||
}
|
||||
return cg
|
||||
}
|
||||
|
||||
func pluginProcess(p config.Plugin) *plugin.Codegen_Process {
|
||||
if p.Process != nil {
|
||||
return &plugin.Codegen_Process{
|
||||
Cmd: p.Process.Cmd,
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func pluginWASM(p config.Plugin) *plugin.Codegen_WASM {
|
||||
if p.WASM != nil {
|
||||
return &plugin.Codegen_WASM{
|
||||
Url: p.WASM.URL,
|
||||
Sha256: p.WASM.SHA256,
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func pluginCatalog(c *catalog.Catalog) *plugin.Catalog {
|
||||
var schemas []*plugin.Schema
|
||||
for _, s := range c.Schemas {
|
||||
var enums []*plugin.Enum
|
||||
var cts []*plugin.CompositeType
|
||||
for _, typ := range s.Types {
|
||||
switch typ := typ.(type) {
|
||||
case *catalog.Enum:
|
||||
enums = append(enums, &plugin.Enum{
|
||||
Name: typ.Name,
|
||||
Comment: typ.Comment,
|
||||
Vals: typ.Vals,
|
||||
})
|
||||
case *catalog.CompositeType:
|
||||
cts = append(cts, &plugin.CompositeType{
|
||||
Name: typ.Name,
|
||||
Comment: typ.Comment,
|
||||
})
|
||||
}
|
||||
}
|
||||
var tables []*plugin.Table
|
||||
for _, t := range s.Tables {
|
||||
var columns []*plugin.Column
|
||||
for _, c := range t.Columns {
|
||||
l := -1
|
||||
if c.Length != nil {
|
||||
l = *c.Length
|
||||
}
|
||||
columns = append(columns, &plugin.Column{
|
||||
Name: c.Name,
|
||||
Type: &plugin.Identifier{
|
||||
Catalog: c.Type.Catalog,
|
||||
Schema: c.Type.Schema,
|
||||
Name: c.Type.Name,
|
||||
},
|
||||
Comment: c.Comment,
|
||||
NotNull: c.IsNotNull,
|
||||
Unsigned: c.IsUnsigned,
|
||||
IsArray: c.IsArray,
|
||||
ArrayDims: int32(c.ArrayDims),
|
||||
Length: int32(l),
|
||||
Table: &plugin.Identifier{
|
||||
Catalog: t.Rel.Catalog,
|
||||
Schema: t.Rel.Schema,
|
||||
Name: t.Rel.Name,
|
||||
},
|
||||
})
|
||||
}
|
||||
tables = append(tables, &plugin.Table{
|
||||
Rel: &plugin.Identifier{
|
||||
Catalog: t.Rel.Catalog,
|
||||
Schema: t.Rel.Schema,
|
||||
Name: t.Rel.Name,
|
||||
},
|
||||
Columns: columns,
|
||||
Comment: t.Comment,
|
||||
})
|
||||
}
|
||||
schemas = append(schemas, &plugin.Schema{
|
||||
Comment: s.Comment,
|
||||
Name: s.Name,
|
||||
Tables: tables,
|
||||
Enums: enums,
|
||||
CompositeTypes: cts,
|
||||
})
|
||||
}
|
||||
return &plugin.Catalog{
|
||||
Name: c.Name,
|
||||
DefaultSchema: c.DefaultSchema,
|
||||
Comment: c.Comment,
|
||||
Schemas: schemas,
|
||||
}
|
||||
}
|
||||
|
||||
func pluginQueries(r *compiler.Result) []*plugin.Query {
|
||||
var out []*plugin.Query
|
||||
for _, q := range r.Queries {
|
||||
var params []*plugin.Parameter
|
||||
var columns []*plugin.Column
|
||||
for _, c := range q.Columns {
|
||||
columns = append(columns, pluginQueryColumn(c))
|
||||
}
|
||||
for _, p := range q.Params {
|
||||
params = append(params, pluginQueryParam(p))
|
||||
}
|
||||
var iit *plugin.Identifier
|
||||
if q.InsertIntoTable != nil {
|
||||
iit = &plugin.Identifier{
|
||||
Catalog: q.InsertIntoTable.Catalog,
|
||||
Schema: q.InsertIntoTable.Schema,
|
||||
Name: q.InsertIntoTable.Name,
|
||||
}
|
||||
}
|
||||
out = append(out, &plugin.Query{
|
||||
Name: q.Metadata.Name,
|
||||
Cmd: q.Metadata.Cmd,
|
||||
Text: q.SQL,
|
||||
Comments: q.Metadata.Comments,
|
||||
Columns: columns,
|
||||
Params: params,
|
||||
Filename: q.Metadata.Filename,
|
||||
InsertIntoTable: iit,
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func pluginQueryColumn(c *compiler.Column) *plugin.Column {
|
||||
l := -1
|
||||
if c.Length != nil {
|
||||
l = *c.Length
|
||||
}
|
||||
out := &plugin.Column{
|
||||
Name: c.Name,
|
||||
OriginalName: c.OriginalName,
|
||||
Comment: c.Comment,
|
||||
NotNull: c.NotNull,
|
||||
Unsigned: c.Unsigned,
|
||||
IsArray: c.IsArray,
|
||||
ArrayDims: int32(c.ArrayDims),
|
||||
Length: int32(l),
|
||||
IsNamedParam: c.IsNamedParam,
|
||||
IsFuncCall: c.IsFuncCall,
|
||||
IsSqlcSlice: c.IsSqlcSlice,
|
||||
}
|
||||
|
||||
if c.Type != nil {
|
||||
out.Type = &plugin.Identifier{
|
||||
Catalog: c.Type.Catalog,
|
||||
Schema: c.Type.Schema,
|
||||
Name: c.Type.Name,
|
||||
}
|
||||
} else {
|
||||
out.Type = &plugin.Identifier{
|
||||
Name: c.DataType,
|
||||
}
|
||||
}
|
||||
|
||||
if c.Table != nil {
|
||||
out.Table = &plugin.Identifier{
|
||||
Catalog: c.Table.Catalog,
|
||||
Schema: c.Table.Schema,
|
||||
Name: c.Table.Name,
|
||||
}
|
||||
}
|
||||
|
||||
if c.EmbedTable != nil {
|
||||
out.EmbedTable = &plugin.Identifier{
|
||||
Catalog: c.EmbedTable.Catalog,
|
||||
Schema: c.EmbedTable.Schema,
|
||||
Name: c.EmbedTable.Name,
|
||||
}
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
func pluginQueryParam(p compiler.Parameter) *plugin.Parameter {
|
||||
return &plugin.Parameter{
|
||||
Number: int32(p.Number),
|
||||
Column: pluginQueryColumn(p.Column),
|
||||
}
|
||||
}
|
||||
|
||||
func codeGenRequest(r *compiler.Result, settings config.CombinedSettings) *plugin.GenerateRequest {
|
||||
return &plugin.GenerateRequest{
|
||||
Settings: pluginSettings(r, settings),
|
||||
Catalog: pluginCatalog(r.Catalog),
|
||||
Queries: pluginQueries(r),
|
||||
SqlcVersion: info.Version,
|
||||
}
|
||||
}
|
||||
156
internal/cmd/verify.go
Normal file
156
internal/cmd/verify.go
Normal file
@@ -0,0 +1,156 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
|
||||
_ "github.com/jackc/pgx/v5/stdlib"
|
||||
"github.com/spf13/cobra"
|
||||
"google.golang.org/protobuf/proto"
|
||||
|
||||
"github.com/sqlc-dev/sqlc/internal/config"
|
||||
"github.com/sqlc-dev/sqlc/internal/dbmanager"
|
||||
"github.com/sqlc-dev/sqlc/internal/migrations"
|
||||
"github.com/sqlc-dev/sqlc/internal/plugin"
|
||||
"github.com/sqlc-dev/sqlc/internal/quickdb"
|
||||
pb "github.com/sqlc-dev/sqlc/internal/quickdb/v1"
|
||||
"github.com/sqlc-dev/sqlc/internal/sql/sqlpath"
|
||||
)
|
||||
|
||||
func init() {
|
||||
verifyCmd.Flags().String("against", "", "compare against this tag")
|
||||
}
|
||||
|
||||
var verifyCmd = &cobra.Command{
|
||||
Use: "verify",
|
||||
Short: "Verify schema, queries, and configuration for this project",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
stderr := cmd.ErrOrStderr()
|
||||
dir, name := getConfigPath(stderr, cmd.Flag("file"))
|
||||
against, err := cmd.Flags().GetString("against")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
opts := &Options{
|
||||
Env: ParseEnv(cmd),
|
||||
Stderr: stderr,
|
||||
Against: against,
|
||||
}
|
||||
if err := Verify(cmd.Context(), dir, name, opts); err != nil {
|
||||
fmt.Fprintf(stderr, "Error verifying queries: %s\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func Verify(ctx context.Context, dir, filename string, opts *Options) error {
|
||||
stderr := opts.Stderr
|
||||
_, conf, err := readConfig(stderr, dir, filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
client, err := quickdb.NewClientFromConfig(conf.Cloud)
|
||||
if err != nil {
|
||||
return fmt.Errorf("client init failed: %w", err)
|
||||
}
|
||||
|
||||
manager := dbmanager.NewClient(conf.Servers)
|
||||
|
||||
// Get query sets from a previous archive by tag. If no tag is provided, get
|
||||
// the latest query sets.
|
||||
previous, err := client.GetQuerySets(ctx, &pb.GetQuerySetsRequest{
|
||||
Tag: opts.Against,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create a mapping of name to query set
|
||||
existing := map[string]config.SQL{}
|
||||
for _, qs := range conf.SQL {
|
||||
existing[qs.Name] = qs
|
||||
}
|
||||
|
||||
var verr error
|
||||
for _, qs := range previous.QuerySets {
|
||||
// TODO: Create a function for this so that we can return early on errors
|
||||
|
||||
check := func() error {
|
||||
if qs.Name == "" {
|
||||
return fmt.Errorf("unnamed query set")
|
||||
}
|
||||
|
||||
current, found := existing[qs.Name]
|
||||
if !found {
|
||||
return fmt.Errorf("unknown query set: %s", qs.Name)
|
||||
}
|
||||
|
||||
// Read the schema files into memory, removing rollback statements
|
||||
var ddl []string
|
||||
files, err := sqlpath.Glob(current.Schema)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, schema := range files {
|
||||
contents, err := os.ReadFile(schema)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read file: %w", err)
|
||||
}
|
||||
ddl = append(ddl, migrations.RemoveRollbackStatements(string(contents)))
|
||||
}
|
||||
|
||||
var codegen plugin.GenerateRequest
|
||||
if err := proto.Unmarshal(qs.CodegenRequest.Contents, &codegen); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create (or re-use) a database to verify against
|
||||
resp, err := manager.CreateDatabase(ctx, &dbmanager.CreateDatabaseRequest{
|
||||
Engine: string(current.Engine),
|
||||
Migrations: ddl,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
db, err := sql.Open("pgx", resp.Uri)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
var qerr error
|
||||
for _, query := range codegen.Queries {
|
||||
stmt, err := db.PrepareContext(ctx, query.Text)
|
||||
if err != nil {
|
||||
fmt.Fprintf(stderr, "Failed to prepare the following query:\n")
|
||||
fmt.Fprintf(stderr, "%s\n", query.Text)
|
||||
fmt.Fprintf(stderr, "Error was: %s\n", err)
|
||||
qerr = err
|
||||
continue
|
||||
}
|
||||
if err := stmt.Close(); err != nil {
|
||||
slog.Error("stmt.Close failed", "err", err)
|
||||
}
|
||||
}
|
||||
|
||||
return qerr
|
||||
}
|
||||
|
||||
if err := check(); err != nil {
|
||||
verr = errors.New("errored")
|
||||
fmt.Fprintf(stderr, "FAIL\t%s\n", qs.Name)
|
||||
fmt.Fprintf(stderr, " ERROR\t%s\n", err)
|
||||
} else {
|
||||
fmt.Fprintf(stderr, "ok\t%s\n", qs.Name)
|
||||
}
|
||||
}
|
||||
|
||||
return verr
|
||||
}
|
||||
709
internal/cmd/vet.go
Normal file
709
internal/cmd/vet.go
Normal file
@@ -0,0 +1,709 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/sqlc-dev/sqlc/internal/constants"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime/trace"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
"cel.dev/cel-go/cel"
|
||||
"cel.dev/cel-go/ext"
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/spf13/cobra"
|
||||
"google.golang.org/protobuf/encoding/protojson"
|
||||
|
||||
"github.com/sqlc-dev/sqlc/internal/config"
|
||||
"github.com/sqlc-dev/sqlc/internal/dbmanager"
|
||||
"github.com/sqlc-dev/sqlc/internal/debug"
|
||||
"github.com/sqlc-dev/sqlc/internal/migrations"
|
||||
"github.com/sqlc-dev/sqlc/internal/opts"
|
||||
"github.com/sqlc-dev/sqlc/internal/plugin"
|
||||
"github.com/sqlc-dev/sqlc/internal/quickdb"
|
||||
"github.com/sqlc-dev/sqlc/internal/shfmt"
|
||||
"github.com/sqlc-dev/sqlc/internal/sql/sqlpath"
|
||||
"github.com/sqlc-dev/sqlc/internal/sqlcdebug"
|
||||
"github.com/sqlc-dev/sqlc/internal/vet"
|
||||
)
|
||||
|
||||
var (
|
||||
debugDumpExplain = sqlcdebug.New("dumpexplain")
|
||||
debugDumpVetEnv = sqlcdebug.New("dumpvetenv")
|
||||
debugDatabases = sqlcdebug.New("databases")
|
||||
)
|
||||
|
||||
var ErrFailedChecks = errors.New("failed checks")
|
||||
|
||||
var pjson = protojson.UnmarshalOptions{AllowPartial: true, DiscardUnknown: true}
|
||||
|
||||
func NewCmdVet() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "vet",
|
||||
Short: "Vet examines queries",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
defer trace.StartRegion(cmd.Context(), "vet").End()
|
||||
stderr := cmd.ErrOrStderr()
|
||||
opts := &Options{
|
||||
Env: ParseEnv(cmd),
|
||||
Stderr: stderr,
|
||||
}
|
||||
dir, name := getConfigPath(stderr, cmd.Flag("file"))
|
||||
if err := Vet(cmd.Context(), dir, name, opts); err != nil {
|
||||
if !errors.Is(err, ErrFailedChecks) {
|
||||
fmt.Fprintf(stderr, "%s\n", err)
|
||||
}
|
||||
os.Exit(1)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func Vet(ctx context.Context, dir, filename string, opts *Options) error {
|
||||
e := opts.Env
|
||||
stderr := opts.Stderr
|
||||
configPath, conf, err := readConfig(stderr, dir, filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
base := filepath.Base(configPath)
|
||||
if err := config.Validate(conf); err != nil {
|
||||
fmt.Fprintf(stderr, "error validating %s: %s\n", base, err)
|
||||
return err
|
||||
}
|
||||
|
||||
if err := e.Validate(conf); err != nil {
|
||||
fmt.Fprintf(stderr, "error validating %s: %s\n", base, err)
|
||||
return err
|
||||
}
|
||||
|
||||
env, err := cel.NewEnv(
|
||||
cel.StdLib(),
|
||||
ext.Strings(ext.StringsVersion(1)),
|
||||
cel.Types(
|
||||
&vet.Config{},
|
||||
&vet.Query{},
|
||||
&vet.PostgreSQL{},
|
||||
&vet.MySQL{},
|
||||
),
|
||||
cel.Variable("query",
|
||||
cel.ObjectType("vet.Query"),
|
||||
),
|
||||
cel.Variable("config",
|
||||
cel.ObjectType("vet.Config"),
|
||||
),
|
||||
cel.Variable("postgresql",
|
||||
cel.ObjectType("vet.PostgreSQL"),
|
||||
),
|
||||
cel.Variable("mysql",
|
||||
cel.ObjectType("vet.MySQL"),
|
||||
),
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("new CEL env error: %s", err)
|
||||
}
|
||||
|
||||
rules := map[string]rule{
|
||||
constants.QueryRuleDbPrepare: {NeedsPrepare: true},
|
||||
}
|
||||
|
||||
for _, c := range conf.Rules {
|
||||
if c.Name == "" {
|
||||
return fmt.Errorf("rules require a name")
|
||||
}
|
||||
if _, found := rules[c.Name]; found {
|
||||
return fmt.Errorf("type-check error: a rule with the name '%s' already exists", c.Name)
|
||||
}
|
||||
if c.Rule == "" {
|
||||
return fmt.Errorf("type-check error: %s is empty", c.Name)
|
||||
}
|
||||
ast, issues := env.Compile(c.Rule)
|
||||
if issues != nil && issues.Err() != nil {
|
||||
return fmt.Errorf("type-check error: %s %s", c.Name, issues.Err())
|
||||
}
|
||||
prg, err := env.Program(ast)
|
||||
if err != nil {
|
||||
return fmt.Errorf("program construction error: %s %s", c.Name, err)
|
||||
}
|
||||
rule := rule{Program: &prg, Message: c.Msg}
|
||||
|
||||
// TODO There's probably a nicer way to do this from the ast
|
||||
// https://pkg.go.dev/cel.dev/cel-go/common/ast#AllMatcher
|
||||
if strings.Contains(c.Rule, "postgresql.explain") ||
|
||||
strings.Contains(c.Rule, "mysql.explain") {
|
||||
rule.NeedsExplain = true
|
||||
}
|
||||
|
||||
rules[c.Name] = rule
|
||||
}
|
||||
|
||||
c := checker{
|
||||
Rules: rules,
|
||||
Conf: conf,
|
||||
Dir: dir,
|
||||
Env: env,
|
||||
Stderr: stderr,
|
||||
OnlyManagedDB: debugDatabases.Value() == "managed",
|
||||
Replacer: shfmt.NewReplacer(nil),
|
||||
}
|
||||
errored := false
|
||||
for _, sql := range conf.SQL {
|
||||
if err := c.checkSQL(ctx, sql); err != nil {
|
||||
if !errors.Is(err, ErrFailedChecks) {
|
||||
fmt.Fprintf(stderr, "%s\n", err)
|
||||
}
|
||||
errored = true
|
||||
}
|
||||
}
|
||||
if errored {
|
||||
return ErrFailedChecks
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type preparer interface {
|
||||
Prepare(context.Context, string, string) error
|
||||
}
|
||||
|
||||
type pgxConn struct {
|
||||
c *pgx.Conn
|
||||
}
|
||||
|
||||
func (p *pgxConn) Prepare(ctx context.Context, name, query string) error {
|
||||
_, err := p.c.Prepare(ctx, name, query)
|
||||
return err
|
||||
}
|
||||
|
||||
// Return a default value for a PostgreSQL column based on its type. Returns nil
|
||||
// if the type is unknown.
|
||||
func pgDefaultValue(col *plugin.Column) any {
|
||||
if col == nil {
|
||||
return nil
|
||||
}
|
||||
if col.Type == nil {
|
||||
return nil
|
||||
}
|
||||
typname := strings.TrimPrefix(col.Type.Name, "pg_catalog.")
|
||||
switch typname {
|
||||
case "any", "void":
|
||||
if col.IsArray {
|
||||
return []any{}
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
case "anyarray":
|
||||
return []any{}
|
||||
case "bool", "boolean":
|
||||
if col.IsArray {
|
||||
return []bool{}
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case "double", "double precision", "real":
|
||||
if col.IsArray {
|
||||
return []float32{}
|
||||
} else {
|
||||
return 0.1
|
||||
}
|
||||
case "json", "jsonb":
|
||||
if col.IsArray {
|
||||
return []string{}
|
||||
} else {
|
||||
return "{}"
|
||||
}
|
||||
case "citext", "string", "text", "varchar":
|
||||
if col.IsArray {
|
||||
return []string{}
|
||||
} else {
|
||||
return ""
|
||||
}
|
||||
case "bigint", "bigserial", "integer", "int", "int2", "int4", "int8", "serial":
|
||||
if col.IsArray {
|
||||
return []int{}
|
||||
} else {
|
||||
return 1
|
||||
}
|
||||
case "date", "time", "timestamp", "timestamptz":
|
||||
if col.IsArray {
|
||||
return []time.Time{}
|
||||
} else {
|
||||
return time.Time{}
|
||||
}
|
||||
case "uuid":
|
||||
if col.IsArray {
|
||||
return []string{}
|
||||
} else {
|
||||
return "00000000-0000-0000-0000-000000000000"
|
||||
}
|
||||
case "numeric", "decimal":
|
||||
if col.IsArray {
|
||||
return []string{}
|
||||
} else {
|
||||
return "0.1"
|
||||
}
|
||||
case "inet":
|
||||
if col.IsArray {
|
||||
return []string{}
|
||||
} else {
|
||||
return "192.168.0.1/24"
|
||||
}
|
||||
case "cidr":
|
||||
if col.IsArray {
|
||||
return []string{}
|
||||
} else {
|
||||
return "192.168.1/24"
|
||||
}
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// Return a default value for a MySQL column based on its type. Returns nil
|
||||
// if the type is unknown.
|
||||
func mysqlDefaultValue(col *plugin.Column) any {
|
||||
if col == nil {
|
||||
return nil
|
||||
}
|
||||
if col.Type == nil {
|
||||
return nil
|
||||
}
|
||||
switch col.Type.Name {
|
||||
case "any":
|
||||
return nil
|
||||
case "bool":
|
||||
return false
|
||||
case "int", "bigint", "mediumint", "smallint", "tinyint", "bit":
|
||||
return 1
|
||||
case "decimal": // "numeric", "dec", "fixed"
|
||||
// No perfect choice here to avoid "Impossible WHERE" but I think
|
||||
// 0.1 is decent. It works for all cases where `scale` > 0 which
|
||||
// should be the majority. For more information refer to
|
||||
// https://dev.mysql.com/doc/refman/8.1/en/fixed-point-types.html.
|
||||
return 0.1
|
||||
case "float", "double":
|
||||
return 0.1
|
||||
case "date":
|
||||
return "0000-00-00"
|
||||
case "datetime", "timestamp":
|
||||
return "0000-00-00 00:00:00"
|
||||
case "time":
|
||||
return "00:00:00"
|
||||
case "year":
|
||||
return "0000"
|
||||
case "char", "varchar", "binary", "varbinary", "tinyblob", "blob",
|
||||
"mediumblob", "longblob", "tinytext", "text", "mediumtext", "longtext":
|
||||
return ""
|
||||
case "json":
|
||||
return "{}"
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (p *pgxConn) Explain(ctx context.Context, query string, args ...*plugin.Parameter) (*vetEngineOutput, error) {
|
||||
eQuery := "EXPLAIN (ANALYZE false, VERBOSE, COSTS, SETTINGS, BUFFERS, FORMAT JSON) " + query
|
||||
eArgs := make([]any, len(args))
|
||||
for i, a := range args {
|
||||
eArgs[i] = pgDefaultValue(a.Column)
|
||||
}
|
||||
row := p.c.QueryRow(ctx, eQuery, eArgs...)
|
||||
var result []json.RawMessage
|
||||
if err := row.Scan(&result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if debugDumpExplain.Value() == "1" {
|
||||
fmt.Println(eQuery, "with args", eArgs)
|
||||
fmt.Println(string(result[0]))
|
||||
}
|
||||
var explain vet.PostgreSQLExplain
|
||||
if err := pjson.Unmarshal(result[0], &explain); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &vetEngineOutput{PostgreSQL: &vet.PostgreSQL{Explain: &explain}}, nil
|
||||
}
|
||||
|
||||
type dbPreparer struct {
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
func (p *dbPreparer) Prepare(ctx context.Context, name, query string) error {
|
||||
s, err := p.db.PrepareContext(ctx, query)
|
||||
if s != nil {
|
||||
s.Close()
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
type explainer interface {
|
||||
Explain(context.Context, string, ...*plugin.Parameter) (*vetEngineOutput, error)
|
||||
}
|
||||
|
||||
type mysqlExplainer struct {
|
||||
*sql.DB
|
||||
}
|
||||
|
||||
func (me *mysqlExplainer) Explain(ctx context.Context, query string, args ...*plugin.Parameter) (*vetEngineOutput, error) {
|
||||
eQuery := "EXPLAIN FORMAT=JSON " + query
|
||||
eArgs := make([]any, len(args))
|
||||
for i, a := range args {
|
||||
eArgs[i] = mysqlDefaultValue(a.Column)
|
||||
}
|
||||
row := me.QueryRowContext(ctx, eQuery, eArgs...)
|
||||
var result json.RawMessage
|
||||
if err := row.Scan(&result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if debugDumpExplain.Value() == "1" {
|
||||
fmt.Println(eQuery, "with args", eArgs)
|
||||
fmt.Println(string(result))
|
||||
}
|
||||
var explain vet.MySQLExplain
|
||||
if err := pjson.Unmarshal(result, &explain); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if explain.QueryBlock.Message != "" {
|
||||
return nil, fmt.Errorf("mysql explain: %s", explain.QueryBlock.Message)
|
||||
}
|
||||
return &vetEngineOutput{MySQL: &vet.MySQL{Explain: &explain}}, nil
|
||||
}
|
||||
|
||||
type rule struct {
|
||||
Program *cel.Program
|
||||
Message string
|
||||
NeedsPrepare bool
|
||||
NeedsExplain bool
|
||||
}
|
||||
|
||||
type checker struct {
|
||||
Rules map[string]rule
|
||||
Conf *config.Config
|
||||
Dir string
|
||||
Env *cel.Env
|
||||
Stderr io.Writer
|
||||
OnlyManagedDB bool
|
||||
Client dbmanager.Client
|
||||
clientOnce sync.Once
|
||||
Replacer *shfmt.Replacer
|
||||
}
|
||||
|
||||
// isInMemorySQLite checks if a SQLite URI refers to an in-memory database
|
||||
func isInMemorySQLite(uri string) bool {
|
||||
if uri == ":memory:" || uri == "" {
|
||||
return true
|
||||
}
|
||||
// Check for file URI with mode=memory parameter
|
||||
// e.g., "file:test?mode=memory&cache=shared"
|
||||
if strings.Contains(uri, "mode=memory") {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (c *checker) fetchDatabaseUri(ctx context.Context, s config.SQL) (string, func() error, error) {
|
||||
cleanup := func() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
if s.Database == nil {
|
||||
panic("fetch database URI called with nil database")
|
||||
}
|
||||
if !s.Database.Managed {
|
||||
uri, err := c.DSN(s.Database.URI)
|
||||
return uri, cleanup, err
|
||||
}
|
||||
|
||||
// Initialize the client exactly once, even if called concurrently
|
||||
c.clientOnce.Do(func() {
|
||||
c.Client = dbmanager.NewClient(c.Conf.Servers)
|
||||
})
|
||||
|
||||
var ddl []string
|
||||
files, err := sqlpath.Glob(s.Schema)
|
||||
if err != nil {
|
||||
return "", cleanup, err
|
||||
}
|
||||
for _, schema := range files {
|
||||
contents, err := os.ReadFile(schema)
|
||||
if err != nil {
|
||||
return "", cleanup, fmt.Errorf("read file: %w", err)
|
||||
}
|
||||
ddl = append(ddl, migrations.RemoveRollbackStatements(string(contents)))
|
||||
}
|
||||
|
||||
resp, err := c.Client.CreateDatabase(ctx, &dbmanager.CreateDatabaseRequest{
|
||||
Engine: string(s.Engine),
|
||||
Migrations: ddl,
|
||||
})
|
||||
if err != nil {
|
||||
return "", cleanup, fmt.Errorf("managed: create database: %w", err)
|
||||
}
|
||||
|
||||
var uri string
|
||||
switch s.Engine {
|
||||
case config.EngineMySQL:
|
||||
dburi, err := quickdb.MySQLReformatURI(resp.Uri)
|
||||
if err != nil {
|
||||
return "", cleanup, fmt.Errorf("reformat uri: %w", err)
|
||||
}
|
||||
uri = dburi
|
||||
default:
|
||||
uri = resp.Uri
|
||||
}
|
||||
|
||||
return uri, cleanup, nil
|
||||
}
|
||||
|
||||
func (c *checker) DSN(dsn string) (string, error) {
|
||||
return c.Replacer.Replace(dsn), nil
|
||||
}
|
||||
|
||||
func (c *checker) checkSQL(ctx context.Context, s config.SQL) error {
|
||||
// TODO: Create a separate function for this logic so we can
|
||||
combo := config.Combine(*c.Conf, s)
|
||||
|
||||
// TODO: This feels like a hack that will bite us later
|
||||
joined := make([]string, 0, len(s.Schema))
|
||||
for _, s := range s.Schema {
|
||||
joined = append(joined, filepath.Join(c.Dir, s))
|
||||
}
|
||||
s.Schema = joined
|
||||
|
||||
joined = make([]string, 0, len(s.Queries))
|
||||
for _, q := range s.Queries {
|
||||
joined = append(joined, filepath.Join(c.Dir, q))
|
||||
}
|
||||
s.Queries = joined
|
||||
|
||||
var name string
|
||||
parseOpts := opts.Parser{}
|
||||
|
||||
result, failed := parse(ctx, name, c.Dir, s, combo, parseOpts, c.Stderr)
|
||||
if failed {
|
||||
return ErrFailedChecks
|
||||
}
|
||||
|
||||
var prep preparer
|
||||
var expl explainer
|
||||
if s.Database != nil { // TODO only set up a database connection if a rule evaluation requires it
|
||||
if s.Database.URI != "" && c.OnlyManagedDB {
|
||||
return fmt.Errorf("database: connections disabled via SQLCDEBUG=databases=managed")
|
||||
}
|
||||
dburl, cleanup, err := c.fetchDatabaseUri(ctx, s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
if err := cleanup(); err != nil {
|
||||
fmt.Fprintf(c.Stderr, "error cleaning up: %s\n", err)
|
||||
}
|
||||
}()
|
||||
|
||||
switch s.Engine {
|
||||
case config.EnginePostgreSQL:
|
||||
conn, err := pgx.Connect(ctx, dburl)
|
||||
if err != nil {
|
||||
return fmt.Errorf("database: connection error: %s", err)
|
||||
}
|
||||
if err := conn.Ping(ctx); err != nil {
|
||||
return fmt.Errorf("database: connection error: %s", err)
|
||||
}
|
||||
defer conn.Close(ctx)
|
||||
pConn := &pgxConn{conn}
|
||||
prep = pConn
|
||||
expl = pConn
|
||||
case config.EngineMySQL:
|
||||
db, err := sql.Open("mysql", dburl)
|
||||
if err != nil {
|
||||
return fmt.Errorf("database: connection error: %s", err)
|
||||
}
|
||||
if err := db.PingContext(ctx); err != nil {
|
||||
return fmt.Errorf("database: connection error: %s", err)
|
||||
}
|
||||
defer db.Close()
|
||||
prep = &dbPreparer{db}
|
||||
expl = &mysqlExplainer{db}
|
||||
case config.EngineSQLite:
|
||||
db, err := sql.Open("sqlite3", dburl)
|
||||
if err != nil {
|
||||
return fmt.Errorf("database: connection error: %s", err)
|
||||
}
|
||||
if err := db.PingContext(ctx); err != nil {
|
||||
return fmt.Errorf("database: connection error: %s", err)
|
||||
}
|
||||
defer db.Close()
|
||||
// For in-memory SQLite databases, apply migrations
|
||||
if isInMemorySQLite(dburl) {
|
||||
files, err := sqlpath.Glob(s.Schema)
|
||||
if err != nil {
|
||||
return fmt.Errorf("schema: %w", err)
|
||||
}
|
||||
for _, schema := range files {
|
||||
contents, err := os.ReadFile(schema)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read schema file: %w", err)
|
||||
}
|
||||
ddl := migrations.RemoveRollbackStatements(string(contents))
|
||||
if _, err := db.ExecContext(ctx, ddl); err != nil {
|
||||
return fmt.Errorf("apply schema %s: %w", schema, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
prep = &dbPreparer{db}
|
||||
// SQLite really doesn't want us to depend on the output of EXPLAIN
|
||||
// QUERY PLAN: https://www.sqlite.org/eqp.html
|
||||
expl = nil
|
||||
default:
|
||||
return fmt.Errorf("unsupported database uri: %s", s.Engine)
|
||||
}
|
||||
}
|
||||
|
||||
errored := false
|
||||
req := codeGenRequest(result, combo)
|
||||
cfg := vetConfig(req)
|
||||
for i, query := range req.Queries {
|
||||
md := result.Queries[i].Metadata
|
||||
if md.Flags[constants.QueryFlagSqlcVetDisable] {
|
||||
// If the vet disable flag is specified without any rules listed, all rules are ignored.
|
||||
if len(md.RuleSkiplist) == 0 {
|
||||
if debug.Active {
|
||||
log.Printf("Skipping all vet rules for query: %s\n", query.Name)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Rules which are listed to be disabled but not declared in the config file are rejected.
|
||||
for r := range md.RuleSkiplist {
|
||||
if !slices.Contains(s.Rules, r) {
|
||||
fmt.Fprintf(c.Stderr, "%s: %s: rule-check error: rule %q does not exist in the config file\n", query.Filename, query.Name, r)
|
||||
errored = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
evalMap := map[string]any{
|
||||
"query": vetQuery(query),
|
||||
"config": cfg,
|
||||
}
|
||||
|
||||
for _, name := range s.Rules {
|
||||
if _, skip := md.RuleSkiplist[name]; skip {
|
||||
if debug.Active {
|
||||
log.Printf("Skipping vet rule %q for query: %s\n", name, query.Name)
|
||||
}
|
||||
} else {
|
||||
rule, ok := c.Rules[name]
|
||||
if !ok {
|
||||
return fmt.Errorf("type-check error: a rule with the name '%s' does not exist", name)
|
||||
}
|
||||
|
||||
if rule.NeedsPrepare {
|
||||
if prep == nil {
|
||||
fmt.Fprintf(c.Stderr, "%s: %s: %s: error preparing query: database connection required\n", query.Filename, query.Name, name)
|
||||
errored = true
|
||||
continue
|
||||
}
|
||||
prepName := fmt.Sprintf("sqlc_vet_%d_%d", time.Now().Unix(), i)
|
||||
if err := prep.Prepare(ctx, prepName, query.Text); err != nil {
|
||||
fmt.Fprintf(c.Stderr, "%s: %s: %s: error preparing query: %s\n", query.Filename, query.Name, name, err)
|
||||
errored = true
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// short-circuit for "sqlc/db-prepare" rule which doesn't have a CEL program
|
||||
if rule.Program == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Get explain output for this query if we need it
|
||||
_, pgsqlOK := evalMap["postgresql"]
|
||||
_, mysqlOK := evalMap["mysql"]
|
||||
if rule.NeedsExplain && !(pgsqlOK || mysqlOK) {
|
||||
if expl == nil {
|
||||
fmt.Fprintf(c.Stderr, "%s: %s: %s: error explaining query: database connection required\n", query.Filename, query.Name, name)
|
||||
errored = true
|
||||
continue
|
||||
}
|
||||
engineOutput, err := expl.Explain(ctx, query.Text, query.Params...)
|
||||
if err != nil {
|
||||
fmt.Fprintf(c.Stderr, "%s: %s: %s: error explaining query: %s\n", query.Filename, query.Name, name, err)
|
||||
errored = true
|
||||
continue
|
||||
}
|
||||
|
||||
evalMap["postgresql"] = engineOutput.PostgreSQL
|
||||
evalMap["mysql"] = engineOutput.MySQL
|
||||
}
|
||||
|
||||
if debugDumpVetEnv.Value() == "1" {
|
||||
fmt.Printf("vars for rule '%s' evaluating against query '%s':\n", name, query.Name)
|
||||
debug.DumpAsJSON(evalMap)
|
||||
}
|
||||
|
||||
out, _, err := (*rule.Program).Eval(evalMap)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tripped, ok := out.Value().(bool)
|
||||
if !ok {
|
||||
return fmt.Errorf("expression returned non-bool value: %v", out.Value())
|
||||
}
|
||||
if tripped {
|
||||
// TODO: Get line numbers in the output
|
||||
if rule.Message == "" {
|
||||
fmt.Fprintf(c.Stderr, "%s: %s: %s\n", query.Filename, query.Name, name)
|
||||
} else {
|
||||
fmt.Fprintf(c.Stderr, "%s: %s: %s: %s\n", query.Filename, query.Name, name, rule.Message)
|
||||
}
|
||||
errored = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if errored {
|
||||
return ErrFailedChecks
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func vetConfig(req *plugin.GenerateRequest) *vet.Config {
|
||||
return &vet.Config{
|
||||
Version: req.Settings.Version,
|
||||
Engine: req.Settings.Engine,
|
||||
Schema: req.Settings.Schema,
|
||||
Queries: req.Settings.Queries,
|
||||
}
|
||||
}
|
||||
|
||||
func vetQuery(q *plugin.Query) *vet.Query {
|
||||
var params []*vet.Parameter
|
||||
for _, p := range q.Params {
|
||||
params = append(params, &vet.Parameter{
|
||||
Number: p.Number,
|
||||
})
|
||||
}
|
||||
return &vet.Query{
|
||||
Sql: q.Text,
|
||||
Name: q.Name,
|
||||
Cmd: strings.TrimPrefix(q.Cmd, ":"),
|
||||
Params: params,
|
||||
}
|
||||
}
|
||||
|
||||
type vetEngineOutput struct {
|
||||
PostgreSQL *vet.PostgreSQL
|
||||
MySQL *vet.MySQL
|
||||
}
|
||||
7
internal/cmd/vet_sqlite.go
Normal file
7
internal/cmd/vet_sqlite.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
_ "github.com/ncruces/go-sqlite3/driver"
|
||||
|
||||
_ "github.com/sqlc-dev/sqlc/internal/sqlite3ext"
|
||||
)
|
||||
14
internal/codegen/golang/driver.go
Normal file
14
internal/codegen/golang/driver.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package golang
|
||||
|
||||
import "github.com/sqlc-dev/sqlc/internal/codegen/golang/opts"
|
||||
|
||||
func parseDriver(sqlPackage string) opts.SQLDriver {
|
||||
switch sqlPackage {
|
||||
case opts.SQLPackagePGXV4:
|
||||
return opts.SQLDriverPGXV4
|
||||
case opts.SQLPackagePGXV5:
|
||||
return opts.SQLDriverPGXV5
|
||||
default:
|
||||
return opts.SQLDriverLibPQ
|
||||
}
|
||||
}
|
||||
64
internal/codegen/golang/enum.go
Normal file
64
internal/codegen/golang/enum.go
Normal file
@@ -0,0 +1,64 @@
|
||||
package golang
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
type Constant struct {
|
||||
Name string
|
||||
Type string
|
||||
Value string
|
||||
}
|
||||
|
||||
type Enum struct {
|
||||
Name string
|
||||
Comment string
|
||||
Constants []Constant
|
||||
NameTags map[string]string
|
||||
ValidTags map[string]string
|
||||
}
|
||||
|
||||
func (e Enum) NameTag() string {
|
||||
return TagsToString(e.NameTags)
|
||||
}
|
||||
|
||||
func (e Enum) ValidTag() string {
|
||||
return TagsToString(e.ValidTags)
|
||||
}
|
||||
|
||||
func enumReplacer(r rune) rune {
|
||||
if strings.ContainsRune("-/:_", r) {
|
||||
return '_'
|
||||
} else if (r >= 'a' && r <= 'z') ||
|
||||
(r >= 'A' && r <= 'Z') ||
|
||||
(r >= '0' && r <= '9') {
|
||||
return r
|
||||
} else {
|
||||
return -1
|
||||
}
|
||||
}
|
||||
|
||||
// EnumReplace removes all non ident symbols (all but letters, numbers and
|
||||
// underscore) and returns valid ident name for provided name.
|
||||
func EnumReplace(value string) string {
|
||||
return strings.Map(enumReplacer, value)
|
||||
}
|
||||
|
||||
// EnumValueName removes all non ident symbols (all but letters, numbers and
|
||||
// underscore) and converts snake case ident to camel case.
|
||||
func EnumValueName(value string) string {
|
||||
parts := strings.Split(EnumReplace(value), "_")
|
||||
for i, part := range parts {
|
||||
parts[i] = titleFirst(part)
|
||||
}
|
||||
|
||||
return strings.Join(parts, "")
|
||||
}
|
||||
|
||||
func titleFirst(s string) string {
|
||||
r := []rune(s)
|
||||
r[0] = unicode.ToUpper(r[0])
|
||||
|
||||
return string(r)
|
||||
}
|
||||
143
internal/codegen/golang/field.go
Normal file
143
internal/codegen/golang/field.go
Normal file
@@ -0,0 +1,143 @@
|
||||
package golang
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/sqlc-dev/sqlc/internal/codegen/golang/opts"
|
||||
"github.com/sqlc-dev/sqlc/internal/plugin"
|
||||
)
|
||||
|
||||
type Field struct {
|
||||
Name string // CamelCased name for Go
|
||||
DBName string // Name as used in the DB
|
||||
Type string
|
||||
Tags map[string]string
|
||||
Comment string
|
||||
Column *plugin.Column
|
||||
// EmbedFields contains the embedded fields that require scanning.
|
||||
EmbedFields []Field
|
||||
}
|
||||
|
||||
func (gf Field) Tag() string {
|
||||
return TagsToString(gf.Tags)
|
||||
}
|
||||
|
||||
func (gf Field) HasSqlcSlice() bool {
|
||||
return gf.Column.IsSqlcSlice
|
||||
}
|
||||
|
||||
func TagsToString(tags map[string]string) string {
|
||||
if len(tags) == 0 {
|
||||
return ""
|
||||
}
|
||||
tagParts := make([]string, 0, len(tags))
|
||||
for key, val := range tags {
|
||||
tagParts = append(tagParts, fmt.Sprintf("%s:%q", key, val))
|
||||
}
|
||||
sort.Strings(tagParts)
|
||||
return strings.Join(tagParts, " ")
|
||||
}
|
||||
|
||||
func JSONTagName(name string, options *opts.Options) string {
|
||||
style := options.JsonTagsCaseStyle
|
||||
idUppercase := options.JsonTagsIdUppercase
|
||||
if style == "" || style == "none" {
|
||||
return name
|
||||
} else {
|
||||
return SetJSONCaseStyle(name, style, idUppercase)
|
||||
}
|
||||
}
|
||||
|
||||
func SetCaseStyle(name string, style string) string {
|
||||
switch style {
|
||||
case "camel":
|
||||
return toCamelCase(name)
|
||||
case "pascal":
|
||||
return toPascalCase(name)
|
||||
case "snake":
|
||||
return toSnakeCase(name)
|
||||
default:
|
||||
panic(fmt.Sprintf("unsupported JSON tags case style: '%s'", style))
|
||||
}
|
||||
}
|
||||
|
||||
func SetJSONCaseStyle(name string, style string, idUppercase bool) string {
|
||||
switch style {
|
||||
case "camel":
|
||||
return toJsonCamelCase(name, idUppercase)
|
||||
case "pascal":
|
||||
return toPascalCase(name)
|
||||
case "snake":
|
||||
return toSnakeCase(name)
|
||||
default:
|
||||
panic(fmt.Sprintf("unsupported JSON tags case style: '%s'", style))
|
||||
}
|
||||
}
|
||||
|
||||
var camelPattern = regexp.MustCompile("[^A-Z][A-Z]+")
|
||||
|
||||
func toSnakeCase(s string) string {
|
||||
if !strings.ContainsRune(s, '_') {
|
||||
s = camelPattern.ReplaceAllStringFunc(s, func(x string) string {
|
||||
return x[:1] + "_" + x[1:]
|
||||
})
|
||||
}
|
||||
return strings.ToLower(s)
|
||||
}
|
||||
|
||||
func toCamelCase(s string) string {
|
||||
return toCamelInitCase(s, false)
|
||||
}
|
||||
|
||||
func toPascalCase(s string) string {
|
||||
return toCamelInitCase(s, true)
|
||||
}
|
||||
|
||||
func toCamelInitCase(name string, initUpper bool) string {
|
||||
var out strings.Builder
|
||||
for i, p := range strings.Split(name, "_") {
|
||||
if !initUpper && i == 0 {
|
||||
out.WriteString(p)
|
||||
continue
|
||||
}
|
||||
if p == "id" {
|
||||
out.WriteString("ID")
|
||||
} else {
|
||||
out.WriteString(strings.Title(p))
|
||||
}
|
||||
}
|
||||
return out.String()
|
||||
}
|
||||
|
||||
func toJsonCamelCase(name string, idUppercase bool) string {
|
||||
var out strings.Builder
|
||||
idStr := "Id"
|
||||
|
||||
if idUppercase {
|
||||
idStr = "ID"
|
||||
}
|
||||
|
||||
for i, p := range strings.Split(name, "_") {
|
||||
if i == 0 {
|
||||
out.WriteString(p)
|
||||
continue
|
||||
}
|
||||
if p == "id" {
|
||||
out.WriteString(idStr)
|
||||
} else {
|
||||
out.WriteString(strings.Title(p))
|
||||
}
|
||||
}
|
||||
return out.String()
|
||||
}
|
||||
|
||||
func toLowerCase(str string) string {
|
||||
if str == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
return strings.ToLower(str[:1]) + str[1:]
|
||||
}
|
||||
426
internal/codegen/golang/gen.go
Normal file
426
internal/codegen/golang/gen.go
Normal file
@@ -0,0 +1,426 @@
|
||||
package golang
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"go/format"
|
||||
"slices"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"github.com/sqlc-dev/sqlc/internal/codegen/golang/opts"
|
||||
"github.com/sqlc-dev/sqlc/internal/codegen/sdk"
|
||||
"github.com/sqlc-dev/sqlc/internal/metadata"
|
||||
"github.com/sqlc-dev/sqlc/internal/plugin"
|
||||
)
|
||||
|
||||
type tmplCtx struct {
|
||||
Q string
|
||||
Package string
|
||||
ModelsPackage string
|
||||
SQLDriver opts.SQLDriver
|
||||
Enums []Enum
|
||||
Structs []Struct
|
||||
GoQueries []Query
|
||||
SqlcVersion string
|
||||
|
||||
// TODO: Race conditions
|
||||
SourceName string
|
||||
|
||||
EmitJSONTags bool
|
||||
JsonTagsIDUppercase bool
|
||||
EmitDBTags bool
|
||||
EmitPreparedQueries bool
|
||||
EmitInterface bool
|
||||
EmitEmptySlices bool
|
||||
EmitMethodsWithDBArgument bool
|
||||
EmitEnumValidMethod bool
|
||||
EmitAllEnumValues bool
|
||||
UsesCopyFrom bool
|
||||
UsesBatch bool
|
||||
OmitSqlcVersion bool
|
||||
BuildTags string
|
||||
WrapErrors bool
|
||||
}
|
||||
|
||||
func (t *tmplCtx) OutputQuery(sourceName string) bool {
|
||||
return t.SourceName == sourceName
|
||||
}
|
||||
|
||||
func (t *tmplCtx) codegenDbarg() string {
|
||||
if t.EmitMethodsWithDBArgument {
|
||||
return "db DBTX, "
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// Called as a global method since subtemplate queryCodeStdExec does not have
|
||||
// access to the toplevel tmplCtx
|
||||
func (t *tmplCtx) codegenEmitPreparedQueries() bool {
|
||||
return t.EmitPreparedQueries
|
||||
}
|
||||
|
||||
func (t *tmplCtx) codegenQueryMethod(q Query) string {
|
||||
db := "q.db"
|
||||
if t.EmitMethodsWithDBArgument {
|
||||
db = "db"
|
||||
}
|
||||
|
||||
switch q.Cmd {
|
||||
case ":one":
|
||||
if t.EmitPreparedQueries {
|
||||
return "q.queryRow"
|
||||
}
|
||||
return db + ".QueryRowContext"
|
||||
|
||||
case ":many":
|
||||
if t.EmitPreparedQueries {
|
||||
return "q.query"
|
||||
}
|
||||
return db + ".QueryContext"
|
||||
|
||||
default:
|
||||
if t.EmitPreparedQueries {
|
||||
return "q.exec"
|
||||
}
|
||||
return db + ".ExecContext"
|
||||
}
|
||||
}
|
||||
|
||||
func (t *tmplCtx) codegenQueryRetval(q Query) (string, error) {
|
||||
switch q.Cmd {
|
||||
case ":one":
|
||||
return "row :=", nil
|
||||
case ":many":
|
||||
return "rows, err :=", nil
|
||||
case ":exec":
|
||||
return "_, err :=", nil
|
||||
case ":execrows", ":execlastid":
|
||||
return "result, err :=", nil
|
||||
case ":execresult":
|
||||
if t.WrapErrors {
|
||||
return "result, err :=", nil
|
||||
}
|
||||
return "return", nil
|
||||
default:
|
||||
return "", fmt.Errorf("unhandled q.Cmd case %q", q.Cmd)
|
||||
}
|
||||
}
|
||||
|
||||
func Generate(ctx context.Context, req *plugin.GenerateRequest) (*plugin.GenerateResponse, error) {
|
||||
options, err := opts.Parse(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := opts.ValidateOpts(options); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
enums := buildEnums(req, options)
|
||||
structs := buildStructs(req, options)
|
||||
queries, err := buildQueries(req, options, enums, structs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if options.OmitUnusedStructs {
|
||||
enums, structs = filterUnusedStructs(enums, structs, queries, options.ModelsTypeQualifier())
|
||||
}
|
||||
|
||||
if err := validate(options, enums, structs, queries); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return generate(req, options, enums, structs, queries)
|
||||
}
|
||||
|
||||
func validate(options *opts.Options, enums []Enum, structs []Struct, queries []Query) error {
|
||||
enumNames := make(map[string]struct{})
|
||||
for _, enum := range enums {
|
||||
enumNames[enum.Name] = struct{}{}
|
||||
enumNames["Null"+enum.Name] = struct{}{}
|
||||
}
|
||||
structNames := make(map[string]struct{})
|
||||
for _, struckt := range structs {
|
||||
if _, ok := enumNames[struckt.Name]; ok {
|
||||
return fmt.Errorf("struct name conflicts with enum name: %s", struckt.Name)
|
||||
}
|
||||
structNames[struckt.Name] = struct{}{}
|
||||
}
|
||||
if !options.EmitExportedQueries {
|
||||
return nil
|
||||
}
|
||||
for _, query := range queries {
|
||||
if _, ok := enumNames[query.ConstantName]; ok {
|
||||
return fmt.Errorf("query constant name conflicts with enum name: %s", query.ConstantName)
|
||||
}
|
||||
if _, ok := structNames[query.ConstantName]; ok {
|
||||
return fmt.Errorf("query constant name conflicts with struct name: %s", query.ConstantName)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func generate(req *plugin.GenerateRequest, options *opts.Options, enums []Enum, structs []Struct, queries []Query) (*plugin.GenerateResponse, error) {
|
||||
i := &importer{
|
||||
Options: options,
|
||||
Queries: queries,
|
||||
Enums: enums,
|
||||
Structs: structs,
|
||||
}
|
||||
|
||||
tctx := tmplCtx{
|
||||
EmitInterface: options.EmitInterface,
|
||||
EmitJSONTags: options.EmitJsonTags,
|
||||
JsonTagsIDUppercase: options.JsonTagsIdUppercase,
|
||||
EmitDBTags: options.EmitDbTags,
|
||||
EmitPreparedQueries: options.EmitPreparedQueries,
|
||||
EmitEmptySlices: options.EmitEmptySlices,
|
||||
EmitMethodsWithDBArgument: options.EmitMethodsWithDbArgument,
|
||||
EmitEnumValidMethod: options.EmitEnumValidMethod,
|
||||
EmitAllEnumValues: options.EmitAllEnumValues,
|
||||
UsesCopyFrom: usesCopyFrom(queries),
|
||||
UsesBatch: usesBatch(queries),
|
||||
SQLDriver: parseDriver(options.SqlPackage),
|
||||
Q: "`",
|
||||
Package: options.Package,
|
||||
ModelsPackage: options.ModelsPackage(),
|
||||
Enums: enums,
|
||||
Structs: structs,
|
||||
SqlcVersion: req.SqlcVersion,
|
||||
BuildTags: options.BuildTags,
|
||||
OmitSqlcVersion: options.OmitSqlcVersion,
|
||||
WrapErrors: options.WrapErrors,
|
||||
}
|
||||
|
||||
if tctx.UsesCopyFrom && !tctx.SQLDriver.IsPGX() && options.SqlDriver != opts.SQLDriverGoSQLDriverMySQL {
|
||||
return nil, errors.New(":copyfrom is only supported by pgx and github.com/go-sql-driver/mysql")
|
||||
}
|
||||
|
||||
if tctx.UsesCopyFrom && options.SqlDriver == opts.SQLDriverGoSQLDriverMySQL {
|
||||
if err := checkNoTimesForMySQLCopyFrom(queries); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tctx.SQLDriver = opts.SQLDriverGoSQLDriverMySQL
|
||||
}
|
||||
|
||||
if tctx.UsesBatch && !tctx.SQLDriver.IsPGX() {
|
||||
return nil, errors.New(":batch* commands are only supported by pgx")
|
||||
}
|
||||
|
||||
funcMap := template.FuncMap{
|
||||
"lowerTitle": sdk.LowerTitle,
|
||||
"comment": sdk.DoubleSlashComment,
|
||||
"escape": sdk.EscapeBacktick,
|
||||
"imports": i.Imports,
|
||||
"hasImports": i.HasImports,
|
||||
"hasPrefix": strings.HasPrefix,
|
||||
|
||||
// These methods are Go specific, they do not belong in the codegen package
|
||||
// (as that is language independent)
|
||||
"dbarg": tctx.codegenDbarg,
|
||||
"emitPreparedQueries": tctx.codegenEmitPreparedQueries,
|
||||
"queryMethod": tctx.codegenQueryMethod,
|
||||
"queryRetval": tctx.codegenQueryRetval,
|
||||
}
|
||||
|
||||
tmpl := template.Must(
|
||||
template.New("table").
|
||||
Funcs(funcMap).
|
||||
ParseFS(
|
||||
templates,
|
||||
"templates/*.tmpl",
|
||||
"templates/*/*.tmpl",
|
||||
),
|
||||
)
|
||||
|
||||
output := map[string]string{}
|
||||
|
||||
execute := func(name, templateName string) error {
|
||||
imports := i.Imports(name)
|
||||
replacedQueries := replaceConflictedArg(imports, queries)
|
||||
|
||||
var b bytes.Buffer
|
||||
w := bufio.NewWriter(&b)
|
||||
tctx.SourceName = name
|
||||
tctx.GoQueries = replacedQueries
|
||||
err := tmpl.ExecuteTemplate(w, templateName, &tctx)
|
||||
w.Flush()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
code, err := format.Source(b.Bytes())
|
||||
if err != nil {
|
||||
fmt.Println(b.String())
|
||||
return fmt.Errorf("source error: %w", err)
|
||||
}
|
||||
|
||||
if templateName == "queryFile" && options.OutputFilesSuffix != "" {
|
||||
name += options.OutputFilesSuffix
|
||||
}
|
||||
|
||||
if !strings.HasSuffix(name, ".go") {
|
||||
name += ".go"
|
||||
}
|
||||
output[name] = string(code)
|
||||
return nil
|
||||
}
|
||||
|
||||
dbFileName := "db.go"
|
||||
if options.OutputDbFileName != "" {
|
||||
dbFileName = options.OutputDbFileName
|
||||
}
|
||||
modelsFileName := "models.go"
|
||||
if options.OutputModelsFileName != "" {
|
||||
modelsFileName = options.OutputModelsFileName
|
||||
}
|
||||
querierFileName := "querier.go"
|
||||
if options.OutputQuerierFileName != "" {
|
||||
querierFileName = options.OutputQuerierFileName
|
||||
}
|
||||
copyfromFileName := "copyfrom.go"
|
||||
if options.OutputCopyfromFileName != "" {
|
||||
copyfromFileName = options.OutputCopyfromFileName
|
||||
}
|
||||
|
||||
batchFileName := "batch.go"
|
||||
if options.OutputBatchFileName != "" {
|
||||
batchFileName = options.OutputBatchFileName
|
||||
}
|
||||
|
||||
if err := execute(dbFileName, "dbFile"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if options.ModelsEmitEnabled() {
|
||||
if err := execute(modelsFileName, "modelsFile"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if options.EmitInterface {
|
||||
if err := execute(querierFileName, "interfaceFile"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if tctx.UsesCopyFrom {
|
||||
if err := execute(copyfromFileName, "copyfromFile"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if tctx.UsesBatch {
|
||||
if err := execute(batchFileName, "batchFile"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
files := map[string]struct{}{}
|
||||
for _, gq := range queries {
|
||||
files[gq.SourceName] = struct{}{}
|
||||
}
|
||||
|
||||
for source := range files {
|
||||
if err := execute(source, "queryFile"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
resp := plugin.GenerateResponse{}
|
||||
|
||||
for filename, code := range output {
|
||||
resp.Files = append(resp.Files, &plugin.File{
|
||||
Name: filename,
|
||||
Contents: []byte(code),
|
||||
})
|
||||
}
|
||||
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
func usesCopyFrom(queries []Query) bool {
|
||||
for _, q := range queries {
|
||||
if q.Cmd == metadata.CmdCopyFrom {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func usesBatch(queries []Query) bool {
|
||||
for _, q := range queries {
|
||||
if slices.Contains([]string{metadata.CmdBatchExec, metadata.CmdBatchMany, metadata.CmdBatchOne}, q.Cmd) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func checkNoTimesForMySQLCopyFrom(queries []Query) error {
|
||||
for _, q := range queries {
|
||||
if q.Cmd != metadata.CmdCopyFrom {
|
||||
continue
|
||||
}
|
||||
for _, f := range q.Arg.CopyFromMySQLFields() {
|
||||
if f.Type == "time.Time" {
|
||||
return fmt.Errorf("values with a timezone are not yet supported")
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func filterUnusedStructs(enums []Enum, structs []Struct, queries []Query, qualifier string) ([]Enum, []Struct) {
|
||||
keepTypes := make(map[string]struct{})
|
||||
|
||||
keep := func(t string) {
|
||||
keepTypes[t] = struct{}{}
|
||||
// Also store the bare type name so that lookups against
|
||||
// bare struct/enum names match even when types have been
|
||||
// qualified with the models package prefix (e.g. "model.User").
|
||||
if bare := stripQualifier(t, qualifier); bare != t {
|
||||
keepTypes[bare] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
for _, query := range queries {
|
||||
if !query.Arg.isEmpty() {
|
||||
keep(query.Arg.Type())
|
||||
if query.Arg.IsStruct() {
|
||||
for _, field := range query.Arg.Struct.Fields {
|
||||
keep(field.Type)
|
||||
}
|
||||
}
|
||||
}
|
||||
if query.hasRetType() {
|
||||
keep(query.Ret.Type())
|
||||
if query.Ret.IsStruct() {
|
||||
for _, field := range query.Ret.Struct.Fields {
|
||||
keep(strings.TrimPrefix(field.Type, "[]"))
|
||||
for _, embedField := range field.EmbedFields {
|
||||
keep(embedField.Type)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
keepEnums := make([]Enum, 0, len(enums))
|
||||
for _, enum := range enums {
|
||||
_, keep := keepTypes[enum.Name]
|
||||
_, keepNull := keepTypes["Null"+enum.Name]
|
||||
_, keepPointer := keepTypes["*"+enum.Name]
|
||||
if keep || keepNull || keepPointer {
|
||||
keepEnums = append(keepEnums, enum)
|
||||
}
|
||||
}
|
||||
|
||||
keepStructs := make([]Struct, 0, len(structs))
|
||||
for _, st := range structs {
|
||||
if _, ok := keepTypes[st.Name]; ok {
|
||||
keepStructs = append(keepStructs, st)
|
||||
}
|
||||
}
|
||||
|
||||
return keepEnums, keepStructs
|
||||
}
|
||||
92
internal/codegen/golang/go_type.go
Normal file
92
internal/codegen/golang/go_type.go
Normal file
@@ -0,0 +1,92 @@
|
||||
package golang
|
||||
|
||||
import (
|
||||
"maps"
|
||||
"strings"
|
||||
|
||||
"github.com/sqlc-dev/sqlc/internal/codegen/golang/opts"
|
||||
"github.com/sqlc-dev/sqlc/internal/codegen/sdk"
|
||||
"github.com/sqlc-dev/sqlc/internal/plugin"
|
||||
)
|
||||
|
||||
func addExtraGoStructTags(tags map[string]string, req *plugin.GenerateRequest, options *opts.Options, col *plugin.Column) {
|
||||
for _, override := range options.Overrides {
|
||||
oride := override.ShimOverride
|
||||
if oride.GoType.StructTags == nil {
|
||||
continue
|
||||
}
|
||||
if override.MatchesColumn(col) {
|
||||
maps.Copy(tags, oride.GoType.StructTags)
|
||||
continue
|
||||
}
|
||||
if !override.Matches(col.Table, req.Catalog.DefaultSchema) {
|
||||
// Different table.
|
||||
continue
|
||||
}
|
||||
cname := col.Name
|
||||
if col.OriginalName != "" {
|
||||
cname = col.OriginalName
|
||||
}
|
||||
if !sdk.MatchString(oride.ColumnName, cname) {
|
||||
// Different column.
|
||||
continue
|
||||
}
|
||||
// Add the extra tags.
|
||||
maps.Copy(tags, oride.GoType.StructTags)
|
||||
}
|
||||
}
|
||||
|
||||
func goType(req *plugin.GenerateRequest, options *opts.Options, col *plugin.Column) string {
|
||||
// Check if the column's type has been overridden
|
||||
for _, override := range options.Overrides {
|
||||
oride := override.ShimOverride
|
||||
|
||||
if oride.GoType.TypeName == "" {
|
||||
continue
|
||||
}
|
||||
cname := col.Name
|
||||
if col.OriginalName != "" {
|
||||
cname = col.OriginalName
|
||||
}
|
||||
sameTable := override.Matches(col.Table, req.Catalog.DefaultSchema)
|
||||
if oride.Column != "" && sdk.MatchString(oride.ColumnName, cname) && sameTable {
|
||||
if col.IsSqlcSlice {
|
||||
return "[]" + oride.GoType.TypeName
|
||||
}
|
||||
return oride.GoType.TypeName
|
||||
}
|
||||
}
|
||||
typ := goInnerType(req, options, col)
|
||||
if col.IsSqlcSlice {
|
||||
return "[]" + typ
|
||||
}
|
||||
if col.IsArray {
|
||||
return strings.Repeat("[]", int(col.ArrayDims)) + typ
|
||||
}
|
||||
return typ
|
||||
}
|
||||
|
||||
func goInnerType(req *plugin.GenerateRequest, options *opts.Options, col *plugin.Column) string {
|
||||
// package overrides have a higher precedence
|
||||
for _, override := range options.Overrides {
|
||||
oride := override.ShimOverride
|
||||
if oride.GoType.TypeName == "" {
|
||||
continue
|
||||
}
|
||||
if override.MatchesColumn(col) {
|
||||
return oride.GoType.TypeName
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Extend the engine interface to handle types
|
||||
switch req.Settings.Engine {
|
||||
case "mysql":
|
||||
return mysqlType(req, options, col)
|
||||
case "postgresql":
|
||||
return postgresType(req, options, col)
|
||||
case "sqlite":
|
||||
return sqliteType(req, options, col)
|
||||
default:
|
||||
return "any"
|
||||
}
|
||||
}
|
||||
537
internal/codegen/golang/imports.go
Normal file
537
internal/codegen/golang/imports.go
Normal file
@@ -0,0 +1,537 @@
|
||||
package golang
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/sqlc-dev/sqlc/internal/codegen/golang/opts"
|
||||
"github.com/sqlc-dev/sqlc/internal/metadata"
|
||||
)
|
||||
|
||||
type fileImports struct {
|
||||
Std []ImportSpec
|
||||
Dep []ImportSpec
|
||||
}
|
||||
|
||||
type ImportSpec struct {
|
||||
ID string
|
||||
Path string
|
||||
}
|
||||
|
||||
func (s ImportSpec) String() string {
|
||||
if s.ID != "" {
|
||||
return fmt.Sprintf("%s %q", s.ID, s.Path)
|
||||
} else {
|
||||
return fmt.Sprintf("%q", s.Path)
|
||||
}
|
||||
}
|
||||
|
||||
func mergeImports(imps ...fileImports) [][]ImportSpec {
|
||||
if len(imps) == 1 {
|
||||
return [][]ImportSpec{
|
||||
imps[0].Std,
|
||||
imps[0].Dep,
|
||||
}
|
||||
}
|
||||
|
||||
var stds, pkgs []ImportSpec
|
||||
seenStd := map[string]struct{}{}
|
||||
seenPkg := map[string]struct{}{}
|
||||
for i := range imps {
|
||||
for _, spec := range imps[i].Std {
|
||||
if _, ok := seenStd[spec.Path]; ok {
|
||||
continue
|
||||
}
|
||||
stds = append(stds, spec)
|
||||
seenStd[spec.Path] = struct{}{}
|
||||
}
|
||||
for _, spec := range imps[i].Dep {
|
||||
if _, ok := seenPkg[spec.Path]; ok {
|
||||
continue
|
||||
}
|
||||
pkgs = append(pkgs, spec)
|
||||
seenPkg[spec.Path] = struct{}{}
|
||||
}
|
||||
}
|
||||
return [][]ImportSpec{stds, pkgs}
|
||||
}
|
||||
|
||||
type importer struct {
|
||||
Options *opts.Options
|
||||
Queries []Query
|
||||
Enums []Enum
|
||||
Structs []Struct
|
||||
}
|
||||
|
||||
func (i *importer) usesType(typ string) bool {
|
||||
for _, strct := range i.Structs {
|
||||
for _, f := range strct.Fields {
|
||||
if hasPrefixIgnoringSliceAndPointerPrefix(f.Type, typ) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (i *importer) HasImports(filename string) bool {
|
||||
imports := i.Imports(filename)
|
||||
return len(imports[0]) != 0 || len(imports[1]) != 0
|
||||
}
|
||||
|
||||
func (i *importer) Imports(filename string) [][]ImportSpec {
|
||||
dbFileName := "db.go"
|
||||
if i.Options.OutputDbFileName != "" {
|
||||
dbFileName = i.Options.OutputDbFileName
|
||||
}
|
||||
modelsFileName := "models.go"
|
||||
if i.Options.OutputModelsFileName != "" {
|
||||
modelsFileName = i.Options.OutputModelsFileName
|
||||
}
|
||||
querierFileName := "querier.go"
|
||||
if i.Options.OutputQuerierFileName != "" {
|
||||
querierFileName = i.Options.OutputQuerierFileName
|
||||
}
|
||||
copyfromFileName := "copyfrom.go"
|
||||
if i.Options.OutputCopyfromFileName != "" {
|
||||
copyfromFileName = i.Options.OutputCopyfromFileName
|
||||
}
|
||||
batchFileName := "batch.go"
|
||||
if i.Options.OutputBatchFileName != "" {
|
||||
batchFileName = i.Options.OutputBatchFileName
|
||||
}
|
||||
|
||||
switch filename {
|
||||
case dbFileName:
|
||||
return mergeImports(i.dbImports())
|
||||
case modelsFileName:
|
||||
return mergeImports(i.modelImports())
|
||||
case querierFileName:
|
||||
return mergeImports(i.interfaceImports())
|
||||
case copyfromFileName:
|
||||
return mergeImports(i.copyfromImports())
|
||||
case batchFileName:
|
||||
return mergeImports(i.batchImports())
|
||||
default:
|
||||
return mergeImports(i.queryImports(filename))
|
||||
}
|
||||
}
|
||||
|
||||
func (i *importer) dbImports() fileImports {
|
||||
var pkg []ImportSpec
|
||||
std := []ImportSpec{
|
||||
{Path: "context"},
|
||||
}
|
||||
|
||||
sqlpkg := parseDriver(i.Options.SqlPackage)
|
||||
switch sqlpkg {
|
||||
case opts.SQLDriverPGXV4:
|
||||
pkg = append(pkg, ImportSpec{Path: "github.com/jackc/pgconn"})
|
||||
pkg = append(pkg, ImportSpec{Path: "github.com/jackc/pgx/v4"})
|
||||
case opts.SQLDriverPGXV5:
|
||||
pkg = append(pkg, ImportSpec{Path: "github.com/jackc/pgx/v5/pgconn"})
|
||||
pkg = append(pkg, ImportSpec{Path: "github.com/jackc/pgx/v5"})
|
||||
default:
|
||||
std = append(std, ImportSpec{Path: "database/sql"})
|
||||
if i.Options.EmitPreparedQueries {
|
||||
std = append(std, ImportSpec{Path: "fmt"})
|
||||
}
|
||||
}
|
||||
|
||||
sort.Slice(std, func(i, j int) bool { return std[i].Path < std[j].Path })
|
||||
sort.Slice(pkg, func(i, j int) bool { return pkg[i].Path < pkg[j].Path })
|
||||
return fileImports{Std: std, Dep: pkg}
|
||||
}
|
||||
|
||||
var stdlibTypes = map[string]string{
|
||||
"json.RawMessage": "encoding/json",
|
||||
"time.Time": "time",
|
||||
"net.IP": "net",
|
||||
"net.HardwareAddr": "net",
|
||||
"netip.Addr": "net/netip",
|
||||
"netip.Prefix": "net/netip",
|
||||
}
|
||||
|
||||
var pqtypeTypes = map[string]struct{}{
|
||||
"pqtype.CIDR": {},
|
||||
"pqtype.Inet": {},
|
||||
"pqtype.Macaddr": {},
|
||||
"pqtype.NullRawMessage": {},
|
||||
}
|
||||
|
||||
func buildImports(options *opts.Options, queries []Query, uses func(string) bool) (map[string]struct{}, map[ImportSpec]struct{}) {
|
||||
pkg := make(map[ImportSpec]struct{})
|
||||
std := make(map[string]struct{})
|
||||
|
||||
if uses("sql.Null") {
|
||||
std["database/sql"] = struct{}{}
|
||||
}
|
||||
|
||||
sqlpkg := parseDriver(options.SqlPackage)
|
||||
for _, q := range queries {
|
||||
if q.Cmd == metadata.CmdExecResult {
|
||||
switch sqlpkg {
|
||||
case opts.SQLDriverPGXV4:
|
||||
pkg[ImportSpec{Path: "github.com/jackc/pgconn"}] = struct{}{}
|
||||
case opts.SQLDriverPGXV5:
|
||||
pkg[ImportSpec{Path: "github.com/jackc/pgx/v5/pgconn"}] = struct{}{}
|
||||
default:
|
||||
std["database/sql"] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for typeName, pkg := range stdlibTypes {
|
||||
if uses(typeName) {
|
||||
std[pkg] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
if uses("pgtype.") {
|
||||
if sqlpkg == opts.SQLDriverPGXV5 {
|
||||
pkg[ImportSpec{Path: "github.com/jackc/pgx/v5/pgtype"}] = struct{}{}
|
||||
} else {
|
||||
pkg[ImportSpec{Path: "github.com/jackc/pgtype"}] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
for typeName := range pqtypeTypes {
|
||||
if uses(typeName) {
|
||||
pkg[ImportSpec{Path: "github.com/sqlc-dev/pqtype"}] = struct{}{}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
overrideTypes := map[string]string{}
|
||||
for _, override := range options.Overrides {
|
||||
o := override.ShimOverride
|
||||
if o.GoType.BasicType || o.GoType.TypeName == "" {
|
||||
continue
|
||||
}
|
||||
overrideTypes[o.GoType.TypeName] = o.GoType.ImportPath
|
||||
}
|
||||
|
||||
_, overrideNullTime := overrideTypes["pq.NullTime"]
|
||||
if uses("pq.NullTime") && !overrideNullTime {
|
||||
pkg[ImportSpec{Path: "github.com/lib/pq"}] = struct{}{}
|
||||
}
|
||||
_, overrideUUID := overrideTypes["uuid.UUID"]
|
||||
if uses("uuid.UUID") && !overrideUUID {
|
||||
pkg[ImportSpec{Path: "github.com/google/uuid"}] = struct{}{}
|
||||
}
|
||||
_, overrideNullUUID := overrideTypes["uuid.NullUUID"]
|
||||
if uses("uuid.NullUUID") && !overrideNullUUID {
|
||||
pkg[ImportSpec{Path: "github.com/google/uuid"}] = struct{}{}
|
||||
}
|
||||
_, overrideVector := overrideTypes["pgvector.Vector"]
|
||||
if uses("pgvector.Vector") && !overrideVector {
|
||||
pkg[ImportSpec{Path: "github.com/pgvector/pgvector-go"}] = struct{}{}
|
||||
}
|
||||
|
||||
// Custom imports
|
||||
for _, override := range options.Overrides {
|
||||
o := override.ShimOverride
|
||||
|
||||
if o.GoType.BasicType || o.GoType.TypeName == "" {
|
||||
continue
|
||||
}
|
||||
_, alreadyImported := std[o.GoType.ImportPath]
|
||||
hasPackageAlias := o.GoType.Package != ""
|
||||
if (!alreadyImported || hasPackageAlias) && uses(o.GoType.TypeName) {
|
||||
pkg[ImportSpec{Path: o.GoType.ImportPath, ID: o.GoType.Package}] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
// Models package import. When models live in a separate Go package and
|
||||
// any type in this file references a qualified model type, import the
|
||||
// models package under a fixed `models` alias so query files always
|
||||
// reference types as `models.User` regardless of how the actual
|
||||
// package is named.
|
||||
if options.ModelsAreExternal() {
|
||||
if uses(options.ModelsTypeQualifier()) {
|
||||
pkg[ImportSpec{Path: options.OutputModelsImport, ID: opts.ModelsImportAlias}] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
return std, pkg
|
||||
}
|
||||
|
||||
func (i *importer) interfaceImports() fileImports {
|
||||
std, pkg := buildImports(i.Options, i.Queries, func(name string) bool {
|
||||
for _, q := range i.Queries {
|
||||
if q.hasRetType() {
|
||||
if usesBatch([]Query{q}) {
|
||||
continue
|
||||
}
|
||||
if hasPrefixIgnoringSliceAndPointerPrefix(q.Ret.Type(), name) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
for _, f := range q.Arg.Pairs() {
|
||||
if hasPrefixIgnoringSliceAndPointerPrefix(f.Type, name) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
std["context"] = struct{}{}
|
||||
|
||||
return sortedImports(std, pkg)
|
||||
}
|
||||
|
||||
func (i *importer) modelImports() fileImports {
|
||||
std, pkg := buildImports(i.Options, nil, i.usesType)
|
||||
|
||||
if len(i.Enums) > 0 {
|
||||
std["fmt"] = struct{}{}
|
||||
std["database/sql/driver"] = struct{}{}
|
||||
}
|
||||
|
||||
return sortedImports(std, pkg)
|
||||
}
|
||||
|
||||
func sortedImports(std map[string]struct{}, pkg map[ImportSpec]struct{}) fileImports {
|
||||
pkgs := make([]ImportSpec, 0, len(pkg))
|
||||
for spec := range pkg {
|
||||
pkgs = append(pkgs, spec)
|
||||
}
|
||||
stds := make([]ImportSpec, 0, len(std))
|
||||
for path := range std {
|
||||
stds = append(stds, ImportSpec{Path: path})
|
||||
}
|
||||
sort.Slice(stds, func(i, j int) bool { return stds[i].Path < stds[j].Path })
|
||||
sort.Slice(pkgs, func(i, j int) bool { return pkgs[i].Path < pkgs[j].Path })
|
||||
return fileImports{stds, pkgs}
|
||||
}
|
||||
|
||||
func (i *importer) queryImports(filename string) fileImports {
|
||||
var gq []Query
|
||||
anyNonCopyFrom := false
|
||||
for _, query := range i.Queries {
|
||||
if usesBatch([]Query{query}) {
|
||||
continue
|
||||
}
|
||||
if query.SourceName == filename {
|
||||
gq = append(gq, query)
|
||||
if query.Cmd != metadata.CmdCopyFrom {
|
||||
anyNonCopyFrom = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std, pkg := buildImports(i.Options, gq, func(name string) bool {
|
||||
for _, q := range gq {
|
||||
if q.hasRetType() {
|
||||
if q.Ret.EmitStruct() {
|
||||
for _, f := range q.Ret.Struct.Fields {
|
||||
if hasPrefixIgnoringSliceAndPointerPrefix(f.Type, name) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
if hasPrefixIgnoringSliceAndPointerPrefix(q.Ret.Type(), name) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
// Check the fields of the argument struct if it's emitted
|
||||
if q.Arg.EmitStruct() {
|
||||
for _, f := range q.Arg.Struct.Fields {
|
||||
if hasPrefixIgnoringSliceAndPointerPrefix(f.Type, name) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
// Check the argument pairs inside the method definition
|
||||
for _, f := range q.Arg.Pairs() {
|
||||
if hasPrefixIgnoringSliceAndPointerPrefix(f.Type, name) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
sliceScan := func() bool {
|
||||
for _, q := range gq {
|
||||
if q.hasRetType() {
|
||||
if q.Ret.IsStruct() {
|
||||
for _, f := range q.Ret.Struct.Fields {
|
||||
if strings.HasPrefix(f.Type, "[]") && f.Type != "[]byte" {
|
||||
return true
|
||||
}
|
||||
for _, embed := range f.EmbedFields {
|
||||
if strings.HasPrefix(embed.Type, "[]") && embed.Type != "[]byte" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if strings.HasPrefix(q.Ret.Type(), "[]") && q.Ret.Type() != "[]byte" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
if !q.Arg.isEmpty() {
|
||||
if q.Arg.IsStruct() {
|
||||
for _, f := range q.Arg.Struct.Fields {
|
||||
if strings.HasPrefix(f.Type, "[]") && f.Type != "[]byte" && !f.HasSqlcSlice() {
|
||||
return true
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if strings.HasPrefix(q.Arg.Type(), "[]") && q.Arg.Type() != "[]byte" && !q.Arg.HasSqlcSlices() {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Search for sqlc.slice() calls
|
||||
sqlcSliceScan := func() bool {
|
||||
for _, q := range gq {
|
||||
if q.Arg.HasSqlcSlices() {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
if anyNonCopyFrom {
|
||||
std["context"] = struct{}{}
|
||||
}
|
||||
|
||||
sqlpkg := parseDriver(i.Options.SqlPackage)
|
||||
if sqlcSliceScan() && !sqlpkg.IsPGX() {
|
||||
std["strings"] = struct{}{}
|
||||
}
|
||||
if sliceScan() && !sqlpkg.IsPGX() {
|
||||
pkg[ImportSpec{Path: "github.com/lib/pq"}] = struct{}{}
|
||||
}
|
||||
|
||||
if i.Options.WrapErrors {
|
||||
std["fmt"] = struct{}{}
|
||||
}
|
||||
|
||||
return sortedImports(std, pkg)
|
||||
}
|
||||
|
||||
func (i *importer) copyfromImports() fileImports {
|
||||
copyFromQueries := make([]Query, 0, len(i.Queries))
|
||||
for _, q := range i.Queries {
|
||||
if q.Cmd == metadata.CmdCopyFrom {
|
||||
copyFromQueries = append(copyFromQueries, q)
|
||||
}
|
||||
}
|
||||
std, pkg := buildImports(i.Options, copyFromQueries, func(name string) bool {
|
||||
for _, q := range copyFromQueries {
|
||||
if q.hasRetType() {
|
||||
if strings.HasPrefix(q.Ret.Type(), name) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
if !q.Arg.isEmpty() {
|
||||
if strings.HasPrefix(q.Arg.Type(), name) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
std["context"] = struct{}{}
|
||||
if i.Options.SqlDriver == opts.SQLDriverGoSQLDriverMySQL {
|
||||
std["io"] = struct{}{}
|
||||
std["fmt"] = struct{}{}
|
||||
std["sync/atomic"] = struct{}{}
|
||||
pkg[ImportSpec{Path: "github.com/go-sql-driver/mysql"}] = struct{}{}
|
||||
pkg[ImportSpec{Path: "github.com/hexon/mysqltsv"}] = struct{}{}
|
||||
}
|
||||
|
||||
return sortedImports(std, pkg)
|
||||
}
|
||||
|
||||
func (i *importer) batchImports() fileImports {
|
||||
batchQueries := make([]Query, 0, len(i.Queries))
|
||||
for _, q := range i.Queries {
|
||||
if usesBatch([]Query{q}) {
|
||||
batchQueries = append(batchQueries, q)
|
||||
}
|
||||
}
|
||||
std, pkg := buildImports(i.Options, batchQueries, func(name string) bool {
|
||||
for _, q := range batchQueries {
|
||||
if q.hasRetType() {
|
||||
if q.Ret.EmitStruct() {
|
||||
for _, f := range q.Ret.Struct.Fields {
|
||||
if hasPrefixIgnoringSliceAndPointerPrefix(f.Type, name) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
if hasPrefixIgnoringSliceAndPointerPrefix(q.Ret.Type(), name) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
if q.Arg.EmitStruct() {
|
||||
for _, f := range q.Arg.Struct.Fields {
|
||||
if hasPrefixIgnoringSliceAndPointerPrefix(f.Type, name) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, f := range q.Arg.Pairs() {
|
||||
if hasPrefixIgnoringSliceAndPointerPrefix(f.Type, name) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
std["context"] = struct{}{}
|
||||
std["errors"] = struct{}{}
|
||||
sqlpkg := parseDriver(i.Options.SqlPackage)
|
||||
switch sqlpkg {
|
||||
case opts.SQLDriverPGXV4:
|
||||
pkg[ImportSpec{Path: "github.com/jackc/pgx/v4"}] = struct{}{}
|
||||
case opts.SQLDriverPGXV5:
|
||||
pkg[ImportSpec{Path: "github.com/jackc/pgx/v5"}] = struct{}{}
|
||||
}
|
||||
|
||||
return sortedImports(std, pkg)
|
||||
}
|
||||
|
||||
func trimSliceAndPointerPrefix(v string) string {
|
||||
v = strings.TrimPrefix(v, "[]")
|
||||
v = strings.TrimPrefix(v, "*")
|
||||
return v
|
||||
}
|
||||
|
||||
func hasPrefixIgnoringSliceAndPointerPrefix(s, prefix string) bool {
|
||||
trimmedS := trimSliceAndPointerPrefix(s)
|
||||
trimmedPrefix := trimSliceAndPointerPrefix(prefix)
|
||||
return strings.HasPrefix(trimmedS, trimmedPrefix)
|
||||
}
|
||||
|
||||
func replaceConflictedArg(imports [][]ImportSpec, queries []Query) []Query {
|
||||
m := make(map[string]struct{})
|
||||
for _, is := range imports {
|
||||
for _, i := range is {
|
||||
paths := strings.Split(i.Path, "/")
|
||||
m[paths[len(paths)-1]] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
replacedQueries := make([]Query, 0, len(queries))
|
||||
for _, query := range queries {
|
||||
if _, exist := m[query.Arg.Name]; exist {
|
||||
query.Arg.Name = toCamelCase(fmt.Sprintf("arg_%s", query.Arg.Name))
|
||||
}
|
||||
replacedQueries = append(replacedQueries, query)
|
||||
}
|
||||
return replacedQueries
|
||||
}
|
||||
144
internal/codegen/golang/mysql_type.go
Normal file
144
internal/codegen/golang/mysql_type.go
Normal file
@@ -0,0 +1,144 @@
|
||||
package golang
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"github.com/sqlc-dev/sqlc/internal/codegen/golang/opts"
|
||||
"github.com/sqlc-dev/sqlc/internal/codegen/sdk"
|
||||
"github.com/sqlc-dev/sqlc/internal/debug"
|
||||
"github.com/sqlc-dev/sqlc/internal/plugin"
|
||||
)
|
||||
|
||||
func mysqlType(req *plugin.GenerateRequest, options *opts.Options, col *plugin.Column) string {
|
||||
columnType := sdk.DataType(col.Type)
|
||||
notNull := col.NotNull || col.IsArray
|
||||
unsigned := col.Unsigned
|
||||
|
||||
switch columnType {
|
||||
|
||||
case "varchar", "text", "char", "tinytext", "mediumtext", "longtext":
|
||||
if notNull {
|
||||
return "string"
|
||||
}
|
||||
return "sql.NullString"
|
||||
|
||||
case "tinyint":
|
||||
if col.Length == 1 {
|
||||
if notNull {
|
||||
return "bool"
|
||||
}
|
||||
return "sql.NullBool"
|
||||
} else {
|
||||
if notNull {
|
||||
if unsigned {
|
||||
return "uint8"
|
||||
}
|
||||
return "int8"
|
||||
}
|
||||
// The database/sql package does not have a sql.NullInt8 type, so we
|
||||
// use the smallest type they have which is NullInt16
|
||||
return "sql.NullInt16"
|
||||
}
|
||||
|
||||
case "year":
|
||||
if notNull {
|
||||
return "int16"
|
||||
}
|
||||
return "sql.NullInt16"
|
||||
|
||||
case "smallint":
|
||||
if notNull {
|
||||
if unsigned {
|
||||
return "uint16"
|
||||
}
|
||||
return "int16"
|
||||
}
|
||||
return "sql.NullInt16"
|
||||
|
||||
case "int", "integer", "mediumint":
|
||||
if notNull {
|
||||
if unsigned {
|
||||
return "uint32"
|
||||
}
|
||||
return "int32"
|
||||
}
|
||||
return "sql.NullInt32"
|
||||
|
||||
case "bigint", "bigint unsigned", "bigint signed":
|
||||
// "bigint unsigned" and "bigint signed" are MySQL CAST types
|
||||
// Note: We use int64 for CAST AS UNSIGNED to match original behavior,
|
||||
// even though uint64 would be more semantically correct.
|
||||
// The Unsigned flag on columns (from table schema) still uses uint64.
|
||||
if notNull {
|
||||
if unsigned {
|
||||
return "uint64"
|
||||
}
|
||||
return "int64"
|
||||
}
|
||||
return "sql.NullInt64"
|
||||
|
||||
case "blob", "binary", "varbinary", "tinyblob", "mediumblob", "longblob":
|
||||
if notNull {
|
||||
return "[]byte"
|
||||
}
|
||||
return "sql.NullString"
|
||||
|
||||
case "double", "double precision", "real", "float":
|
||||
if notNull {
|
||||
return "float64"
|
||||
}
|
||||
return "sql.NullFloat64"
|
||||
|
||||
case "decimal", "dec", "fixed":
|
||||
if notNull {
|
||||
return "string"
|
||||
}
|
||||
return "sql.NullString"
|
||||
|
||||
case "enum":
|
||||
// TODO: Proper Enum support
|
||||
return "string"
|
||||
|
||||
case "date", "timestamp", "datetime", "time":
|
||||
if notNull {
|
||||
return "time.Time"
|
||||
}
|
||||
return "sql.NullTime"
|
||||
|
||||
case "boolean", "bool":
|
||||
if notNull {
|
||||
return "bool"
|
||||
}
|
||||
return "sql.NullBool"
|
||||
|
||||
case "json":
|
||||
return "json.RawMessage"
|
||||
|
||||
case "any":
|
||||
return "any"
|
||||
|
||||
default:
|
||||
for _, schema := range req.Catalog.Schemas {
|
||||
for _, enum := range schema.Enums {
|
||||
if enum.Name == columnType {
|
||||
if notNull {
|
||||
if schema.Name == req.Catalog.DefaultSchema {
|
||||
return StructName(enum.Name, options)
|
||||
}
|
||||
return StructName(schema.Name+"_"+enum.Name, options)
|
||||
} else {
|
||||
if schema.Name == req.Catalog.DefaultSchema {
|
||||
return "Null" + StructName(enum.Name, options)
|
||||
}
|
||||
return "Null" + StructName(schema.Name+"_"+enum.Name, options)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if debug.Active {
|
||||
log.Printf("Unknown MySQL type: %s\n", columnType)
|
||||
}
|
||||
return "any"
|
||||
|
||||
}
|
||||
}
|
||||
64
internal/codegen/golang/opts/enum.go
Normal file
64
internal/codegen/golang/opts/enum.go
Normal file
@@ -0,0 +1,64 @@
|
||||
package opts
|
||||
|
||||
import "fmt"
|
||||
|
||||
type SQLDriver string
|
||||
|
||||
const (
|
||||
SQLPackagePGXV4 string = "pgx/v4"
|
||||
SQLPackagePGXV5 string = "pgx/v5"
|
||||
SQLPackageStandard string = "database/sql"
|
||||
)
|
||||
|
||||
var validPackages = map[string]struct{}{
|
||||
string(SQLPackagePGXV4): {},
|
||||
string(SQLPackagePGXV5): {},
|
||||
string(SQLPackageStandard): {},
|
||||
}
|
||||
|
||||
func validatePackage(sqlPackage string) error {
|
||||
if _, found := validPackages[sqlPackage]; !found {
|
||||
return fmt.Errorf("unknown SQL package: %s", sqlPackage)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
const (
|
||||
SQLDriverPGXV4 SQLDriver = "github.com/jackc/pgx/v4"
|
||||
SQLDriverPGXV5 = "github.com/jackc/pgx/v5"
|
||||
SQLDriverLibPQ = "github.com/lib/pq"
|
||||
SQLDriverGoSQLDriverMySQL = "github.com/go-sql-driver/mysql"
|
||||
)
|
||||
|
||||
var validDrivers = map[string]struct{}{
|
||||
string(SQLDriverPGXV4): {},
|
||||
string(SQLDriverPGXV5): {},
|
||||
string(SQLDriverLibPQ): {},
|
||||
string(SQLDriverGoSQLDriverMySQL): {},
|
||||
}
|
||||
|
||||
func validateDriver(sqlDriver string) error {
|
||||
if _, found := validDrivers[sqlDriver]; !found {
|
||||
return fmt.Errorf("unknown SQL driver: %s", sqlDriver)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d SQLDriver) IsPGX() bool {
|
||||
return d == SQLDriverPGXV4 || d == SQLDriverPGXV5
|
||||
}
|
||||
|
||||
func (d SQLDriver) IsGoSQLDriverMySQL() bool {
|
||||
return d == SQLDriverGoSQLDriverMySQL
|
||||
}
|
||||
|
||||
func (d SQLDriver) Package() string {
|
||||
switch d {
|
||||
case SQLDriverPGXV4:
|
||||
return SQLPackagePGXV4
|
||||
case SQLDriverPGXV5:
|
||||
return SQLPackagePGXV5
|
||||
default:
|
||||
return SQLPackageStandard
|
||||
}
|
||||
}
|
||||
192
internal/codegen/golang/opts/go_type.go
Normal file
192
internal/codegen/golang/opts/go_type.go
Normal file
@@ -0,0 +1,192 @@
|
||||
package opts
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"go/types"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/fatih/structtag"
|
||||
)
|
||||
|
||||
type GoType struct {
|
||||
Path string `json:"import" yaml:"import"`
|
||||
Package string `json:"package" yaml:"package"`
|
||||
Name string `json:"type" yaml:"type"`
|
||||
Pointer bool `json:"pointer" yaml:"pointer"`
|
||||
Slice bool `json:"slice" yaml:"slice"`
|
||||
Spec string `json:"-"`
|
||||
BuiltIn bool `json:"-"`
|
||||
}
|
||||
|
||||
type ParsedGoType struct {
|
||||
ImportPath string
|
||||
Package string
|
||||
TypeName string
|
||||
BasicType bool
|
||||
StructTag string
|
||||
}
|
||||
|
||||
func (o *GoType) MarshalJSON() ([]byte, error) {
|
||||
if o.Spec != "" {
|
||||
return json.Marshal(o.Spec)
|
||||
}
|
||||
type alias GoType
|
||||
return json.Marshal(alias(*o))
|
||||
}
|
||||
|
||||
func (o *GoType) UnmarshalJSON(data []byte) error {
|
||||
var spec string
|
||||
if err := json.Unmarshal(data, &spec); err == nil {
|
||||
*o = GoType{Spec: spec}
|
||||
return nil
|
||||
}
|
||||
type alias GoType
|
||||
var a alias
|
||||
if err := json.Unmarshal(data, &a); err != nil {
|
||||
return err
|
||||
}
|
||||
*o = GoType(a)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *GoType) UnmarshalYAML(unmarshal func(any) error) error {
|
||||
var spec string
|
||||
if err := unmarshal(&spec); err == nil {
|
||||
*o = GoType{Spec: spec}
|
||||
return nil
|
||||
}
|
||||
type alias GoType
|
||||
var a alias
|
||||
if err := unmarshal(&a); err != nil {
|
||||
return err
|
||||
}
|
||||
*o = GoType(a)
|
||||
return nil
|
||||
}
|
||||
|
||||
var validIdentifier = regexp.MustCompile(`^[a-zA-Z0-9_]+$`)
|
||||
var versionNumber = regexp.MustCompile(`^v[0-9]+$`)
|
||||
var invalidIdentifier = regexp.MustCompile(`[^a-zA-Z0-9_]`)
|
||||
|
||||
func generatePackageID(importPath string) (string, bool) {
|
||||
parts := strings.Split(importPath, "/")
|
||||
name := parts[len(parts)-1]
|
||||
// If the last part of the import path is a valid identifier, assume that's the package name
|
||||
if versionNumber.MatchString(name) && len(parts) >= 2 {
|
||||
name = parts[len(parts)-2]
|
||||
return invalidIdentifier.ReplaceAllString(strings.ToLower(name), "_"), true
|
||||
}
|
||||
if validIdentifier.MatchString(name) {
|
||||
return name, false
|
||||
}
|
||||
return invalidIdentifier.ReplaceAllString(strings.ToLower(name), "_"), true
|
||||
}
|
||||
|
||||
// validate GoType
|
||||
func (gt GoType) parse() (*ParsedGoType, error) {
|
||||
var o ParsedGoType
|
||||
|
||||
if gt.Spec == "" {
|
||||
// TODO: Validation
|
||||
if gt.Path == "" && gt.Package != "" {
|
||||
return nil, fmt.Errorf("Package override `go_type`: package name requires an import path")
|
||||
}
|
||||
var pkg string
|
||||
var pkgNeedsAlias bool
|
||||
|
||||
if gt.Package == "" && gt.Path != "" {
|
||||
pkg, pkgNeedsAlias = generatePackageID(gt.Path)
|
||||
if pkgNeedsAlias {
|
||||
o.Package = pkg
|
||||
}
|
||||
} else {
|
||||
pkg = gt.Package
|
||||
o.Package = gt.Package
|
||||
}
|
||||
|
||||
o.ImportPath = gt.Path
|
||||
o.TypeName = gt.Name
|
||||
o.BasicType = gt.Path == "" && gt.Package == ""
|
||||
if pkg != "" {
|
||||
o.TypeName = pkg + "." + o.TypeName
|
||||
}
|
||||
if gt.Pointer {
|
||||
o.TypeName = "*" + o.TypeName
|
||||
}
|
||||
if gt.Slice {
|
||||
o.TypeName = "[]" + o.TypeName
|
||||
}
|
||||
return &o, nil
|
||||
}
|
||||
|
||||
input := gt.Spec
|
||||
lastDot := strings.LastIndex(input, ".")
|
||||
lastSlash := strings.LastIndex(input, "/")
|
||||
typename := input
|
||||
if lastDot == -1 && lastSlash == -1 {
|
||||
// if the type name has no slash and no dot, validate that the type is a basic Go type
|
||||
var found bool
|
||||
for _, typ := range types.Typ {
|
||||
info := typ.Info()
|
||||
if info == 0 {
|
||||
continue
|
||||
}
|
||||
if info&types.IsUntyped != 0 {
|
||||
continue
|
||||
}
|
||||
if typename == typ.Name() {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return nil, fmt.Errorf("Package override `go_type` specifier %q is not a Go basic type e.g. 'string'", input)
|
||||
}
|
||||
o.BasicType = true
|
||||
} else {
|
||||
// assume the type lives in a Go package
|
||||
if lastDot == -1 {
|
||||
return nil, fmt.Errorf("Package override `go_type` specifier %q is not the proper format, expected 'package.type', e.g. 'github.com/segmentio/ksuid.KSUID'", input)
|
||||
}
|
||||
typename = input[lastSlash+1:]
|
||||
// a package name beginning with "go-" will give syntax errors in
|
||||
// generated code. We should do the right thing and get the actual
|
||||
// import name, but in lieu of that, stripping the leading "go-" may get
|
||||
// us what we want.
|
||||
typename = strings.TrimPrefix(typename, "go-")
|
||||
typename = strings.TrimSuffix(typename, "-go")
|
||||
o.ImportPath = input[:lastDot]
|
||||
}
|
||||
o.TypeName = typename
|
||||
isPointer := input[0] == '*'
|
||||
if isPointer {
|
||||
o.ImportPath = o.ImportPath[1:]
|
||||
o.TypeName = "*" + o.TypeName
|
||||
}
|
||||
return &o, nil
|
||||
}
|
||||
|
||||
// GoStructTag is a raw Go struct tag.
|
||||
type GoStructTag string
|
||||
|
||||
// Parse parses and validates a GoStructTag.
|
||||
// The output is in a form convenient for codegen.
|
||||
//
|
||||
// Sample valid inputs/outputs:
|
||||
//
|
||||
// In Out
|
||||
// empty string {}
|
||||
// `a:"b"` {"a": "b"}
|
||||
// `a:"b" x:"y,z"` {"a": "b", "x": "y,z"}
|
||||
func (s GoStructTag) parse() (map[string]string, error) {
|
||||
m := make(map[string]string)
|
||||
tags, err := structtag.Parse(string(s))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, tag := range tags.Tags() {
|
||||
m[tag.Key] = tag.Value()
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
243
internal/codegen/golang/opts/options.go
Normal file
243
internal/codegen/golang/opts/options.go
Normal file
@@ -0,0 +1,243 @@
|
||||
package opts
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"maps"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/sqlc-dev/sqlc/internal/plugin"
|
||||
)
|
||||
|
||||
type Options struct {
|
||||
EmitInterface bool `json:"emit_interface" yaml:"emit_interface"`
|
||||
EmitJsonTags bool `json:"emit_json_tags" yaml:"emit_json_tags"`
|
||||
JsonTagsIdUppercase bool `json:"json_tags_id_uppercase" yaml:"json_tags_id_uppercase"`
|
||||
EmitDbTags bool `json:"emit_db_tags" yaml:"emit_db_tags"`
|
||||
EmitPreparedQueries bool `json:"emit_prepared_queries" yaml:"emit_prepared_queries"`
|
||||
EmitExactTableNames bool `json:"emit_exact_table_names,omitempty" yaml:"emit_exact_table_names"`
|
||||
EmitEmptySlices bool `json:"emit_empty_slices,omitempty" yaml:"emit_empty_slices"`
|
||||
EmitExportedQueries bool `json:"emit_exported_queries" yaml:"emit_exported_queries"`
|
||||
EmitResultStructPointers bool `json:"emit_result_struct_pointers" yaml:"emit_result_struct_pointers"`
|
||||
EmitParamsStructPointers bool `json:"emit_params_struct_pointers" yaml:"emit_params_struct_pointers"`
|
||||
EmitMethodsWithDbArgument bool `json:"emit_methods_with_db_argument,omitempty" yaml:"emit_methods_with_db_argument"`
|
||||
EmitPointersForNullTypes bool `json:"emit_pointers_for_null_types" yaml:"emit_pointers_for_null_types"`
|
||||
// nil inherits EmitPointersForNullTypes; non-nil overrides for enums only.
|
||||
EmitPointersForNullEnumTypes *bool `json:"emit_pointers_for_null_enum_types,omitempty" yaml:"emit_pointers_for_null_enum_types"`
|
||||
EmitEnumValidMethod bool `json:"emit_enum_valid_method,omitempty" yaml:"emit_enum_valid_method"`
|
||||
EmitAllEnumValues bool `json:"emit_all_enum_values,omitempty" yaml:"emit_all_enum_values"`
|
||||
EmitSqlAsComment bool `json:"emit_sql_as_comment,omitempty" yaml:"emit_sql_as_comment"`
|
||||
JsonTagsCaseStyle string `json:"json_tags_case_style,omitempty" yaml:"json_tags_case_style"`
|
||||
Package string `json:"package" yaml:"package"`
|
||||
Out string `json:"out" yaml:"out"`
|
||||
Overrides []Override `json:"overrides,omitempty" yaml:"overrides"`
|
||||
Rename map[string]string `json:"rename,omitempty" yaml:"rename"`
|
||||
SqlPackage string `json:"sql_package" yaml:"sql_package"`
|
||||
SqlDriver string `json:"sql_driver" yaml:"sql_driver"`
|
||||
OutputBatchFileName string `json:"output_batch_file_name,omitempty" yaml:"output_batch_file_name"`
|
||||
OutputDbFileName string `json:"output_db_file_name,omitempty" yaml:"output_db_file_name"`
|
||||
OutputModelsFileName string `json:"output_models_file_name,omitempty" yaml:"output_models_file_name"`
|
||||
OutputModelsPath string `json:"output_models_path,omitempty" yaml:"output_models_path"`
|
||||
OutputModelsPackage string `json:"output_models_package,omitempty" yaml:"output_models_package"`
|
||||
OutputModelsImport string `json:"output_models_import,omitempty" yaml:"output_models_import"`
|
||||
OutputModelsEmit *bool `json:"output_models_emit,omitempty" yaml:"output_models_emit"`
|
||||
OutputQuerierFileName string `json:"output_querier_file_name,omitempty" yaml:"output_querier_file_name"`
|
||||
OutputCopyfromFileName string `json:"output_copyfrom_file_name,omitempty" yaml:"output_copyfrom_file_name"`
|
||||
OutputFilesSuffix string `json:"output_files_suffix,omitempty" yaml:"output_files_suffix"`
|
||||
InflectionExcludeTableNames []string `json:"inflection_exclude_table_names,omitempty" yaml:"inflection_exclude_table_names"`
|
||||
WrapErrors bool `json:"wrap_errors,omitempty" yaml:"wrap_errors"`
|
||||
QueryParameterLimit *int32 `json:"query_parameter_limit,omitempty" yaml:"query_parameter_limit"`
|
||||
OmitSqlcVersion bool `json:"omit_sqlc_version,omitempty" yaml:"omit_sqlc_version"`
|
||||
OmitUnusedStructs bool `json:"omit_unused_structs,omitempty" yaml:"omit_unused_structs"`
|
||||
BuildTags string `json:"build_tags,omitempty" yaml:"build_tags"`
|
||||
Initialisms *[]string `json:"initialisms,omitempty" yaml:"initialisms"`
|
||||
|
||||
InitialismsMap map[string]struct{} `json:"-" yaml:"-"`
|
||||
}
|
||||
|
||||
type GlobalOptions struct {
|
||||
Overrides []Override `json:"overrides,omitempty" yaml:"overrides"`
|
||||
Rename map[string]string `json:"rename,omitempty" yaml:"rename"`
|
||||
}
|
||||
|
||||
func Parse(req *plugin.GenerateRequest) (*Options, error) {
|
||||
options, err := parseOpts(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
global, err := parseGlobalOpts(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(global.Overrides) > 0 {
|
||||
options.Overrides = append(global.Overrides, options.Overrides...)
|
||||
}
|
||||
if len(global.Rename) > 0 {
|
||||
if options.Rename == nil {
|
||||
options.Rename = map[string]string{}
|
||||
}
|
||||
maps.Copy(options.Rename, global.Rename)
|
||||
}
|
||||
return options, nil
|
||||
}
|
||||
|
||||
func parseOpts(req *plugin.GenerateRequest) (*Options, error) {
|
||||
var options Options
|
||||
if len(req.PluginOptions) == 0 {
|
||||
return &options, nil
|
||||
}
|
||||
if err := json.Unmarshal(req.PluginOptions, &options); err != nil {
|
||||
return nil, fmt.Errorf("unmarshalling plugin options: %w", err)
|
||||
}
|
||||
|
||||
if options.Package == "" {
|
||||
if options.Out != "" {
|
||||
options.Package = filepath.Base(options.Out)
|
||||
} else {
|
||||
return nil, fmt.Errorf("invalid options: missing package name")
|
||||
}
|
||||
}
|
||||
|
||||
// Default the models package name to the base of the models path. When
|
||||
// the user only configures output_models_emit: false (no path), fall
|
||||
// back to the base of the import path.
|
||||
if options.OutputModelsPackage == "" {
|
||||
if options.OutputModelsPath != "" {
|
||||
options.OutputModelsPackage = filepath.Base(options.OutputModelsPath)
|
||||
} else if options.OutputModelsImport != "" {
|
||||
options.OutputModelsPackage = filepath.Base(options.OutputModelsImport)
|
||||
}
|
||||
}
|
||||
|
||||
for i := range options.Overrides {
|
||||
if err := options.Overrides[i].parse(req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if options.SqlPackage != "" {
|
||||
if err := validatePackage(options.SqlPackage); err != nil {
|
||||
return nil, fmt.Errorf("invalid options: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
if options.SqlDriver != "" {
|
||||
if err := validateDriver(options.SqlDriver); err != nil {
|
||||
return nil, fmt.Errorf("invalid options: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
if options.QueryParameterLimit == nil {
|
||||
options.QueryParameterLimit = new(int32)
|
||||
*options.QueryParameterLimit = 1
|
||||
}
|
||||
|
||||
if options.Initialisms == nil {
|
||||
options.Initialisms = new([]string)
|
||||
*options.Initialisms = []string{"id"}
|
||||
}
|
||||
|
||||
options.InitialismsMap = map[string]struct{}{}
|
||||
for _, initial := range *options.Initialisms {
|
||||
options.InitialismsMap[initial] = struct{}{}
|
||||
}
|
||||
|
||||
return &options, nil
|
||||
}
|
||||
|
||||
func parseGlobalOpts(req *plugin.GenerateRequest) (*GlobalOptions, error) {
|
||||
var options GlobalOptions
|
||||
if len(req.GlobalOptions) == 0 {
|
||||
return &options, nil
|
||||
}
|
||||
if err := json.Unmarshal(req.GlobalOptions, &options); err != nil {
|
||||
return nil, fmt.Errorf("unmarshalling global options: %w", err)
|
||||
}
|
||||
for i := range options.Overrides {
|
||||
if err := options.Overrides[i].parse(req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return &options, nil
|
||||
}
|
||||
|
||||
func ValidateOpts(opts *Options) error {
|
||||
if opts.EmitMethodsWithDbArgument && opts.EmitPreparedQueries {
|
||||
return fmt.Errorf("invalid options: emit_methods_with_db_argument and emit_prepared_queries options are mutually exclusive")
|
||||
}
|
||||
if *opts.QueryParameterLimit < 0 {
|
||||
return fmt.Errorf("invalid options: query parameter limit must not be negative")
|
||||
}
|
||||
|
||||
if err := validateModelsOptions(opts); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ModelsEmitEnabled reports whether this codegen block should write the
|
||||
// models file. Defaults to true when the option is unset.
|
||||
func (o *Options) ModelsEmitEnabled() bool {
|
||||
if o.OutputModelsEmit == nil {
|
||||
return true
|
||||
}
|
||||
return *o.OutputModelsEmit
|
||||
}
|
||||
|
||||
// ModelsImportAlias is the fixed Go import alias used for the models
|
||||
// package in query files. Using a constant alias keeps the type qualifier
|
||||
// consistent regardless of how the user names the actual package.
|
||||
const ModelsImportAlias = "models"
|
||||
|
||||
// ModelsPackage returns the Go package name to use in the models file
|
||||
// itself (i.e. the `package X` declaration). When the caller has not
|
||||
// configured a separate models package, this is the same as Package.
|
||||
func (o *Options) ModelsPackage() string {
|
||||
if o.OutputModelsPackage != "" {
|
||||
return o.OutputModelsPackage
|
||||
}
|
||||
return o.Package
|
||||
}
|
||||
|
||||
// ModelsAreExternal reports whether model types live in a different Go
|
||||
// package than the queries package. When true, query files must import the
|
||||
// models package and reference types as `models.Type`.
|
||||
func (o *Options) ModelsAreExternal() bool {
|
||||
return o.OutputModelsImport != ""
|
||||
}
|
||||
|
||||
// ModelsTypeQualifier returns the prefix to use when referencing a model
|
||||
// type from a query file ("models."). Empty string when no qualifier is
|
||||
// needed.
|
||||
func (o *Options) ModelsTypeQualifier() string {
|
||||
if o.ModelsAreExternal() {
|
||||
return ModelsImportAlias + "."
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func validateModelsOptions(opts *Options) error {
|
||||
hasAnyModelsOpt := opts.OutputModelsPath != "" ||
|
||||
opts.OutputModelsPackage != "" ||
|
||||
opts.OutputModelsImport != "" ||
|
||||
opts.OutputModelsEmit != nil
|
||||
|
||||
if !hasAnyModelsOpt {
|
||||
return nil
|
||||
}
|
||||
|
||||
if opts.OutputModelsImport == "" {
|
||||
return fmt.Errorf("invalid options: output_models_import is required when any output_models_* option is set")
|
||||
}
|
||||
|
||||
if opts.ModelsEmitEnabled() && opts.OutputModelsPath == "" {
|
||||
return fmt.Errorf("invalid options: output_models_path is required when emitting models to a separate package")
|
||||
}
|
||||
|
||||
if opts.ModelsEmitEnabled() && opts.OutputModelsPath == opts.Out {
|
||||
return fmt.Errorf("invalid options: output_models_path matches out; models would overwrite the queries package")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
176
internal/codegen/golang/opts/override.go
Normal file
176
internal/codegen/golang/opts/override.go
Normal file
@@ -0,0 +1,176 @@
|
||||
package opts
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/sqlc-dev/sqlc/internal/codegen/sdk"
|
||||
"github.com/sqlc-dev/sqlc/internal/pattern"
|
||||
"github.com/sqlc-dev/sqlc/internal/plugin"
|
||||
)
|
||||
|
||||
type Override struct {
|
||||
// name of the golang type to use, e.g. `github.com/segmentio/ksuid.KSUID`
|
||||
GoType GoType `json:"go_type" yaml:"go_type"`
|
||||
|
||||
// additional Go struct tags to add to this field, in raw Go struct tag form, e.g. `validate:"required" x:"y,z"`
|
||||
// see https://github.com/sqlc-dev/sqlc/issues/534
|
||||
GoStructTag GoStructTag `json:"go_struct_tag" yaml:"go_struct_tag"`
|
||||
|
||||
// fully qualified name of the Go type, e.g. `github.com/segmentio/ksuid.KSUID`
|
||||
DBType string `json:"db_type" yaml:"db_type"`
|
||||
Deprecated_PostgresType string `json:"postgres_type" yaml:"postgres_type"`
|
||||
|
||||
// for global overrides only when two different engines are in use
|
||||
Engine string `json:"engine,omitempty" yaml:"engine"`
|
||||
|
||||
// True if the GoType should override if the matching type is nullable
|
||||
Nullable bool `json:"nullable" yaml:"nullable"`
|
||||
|
||||
// True if the GoType should override if the matching type is unsiged.
|
||||
Unsigned bool `json:"unsigned" yaml:"unsigned"`
|
||||
|
||||
// Deprecated. Use the `nullable` property instead
|
||||
Deprecated_Null bool `json:"null" yaml:"null"`
|
||||
|
||||
// fully qualified name of the column, e.g. `accounts.id`
|
||||
Column string `json:"column" yaml:"column"`
|
||||
|
||||
ColumnName *pattern.Match `json:"-"`
|
||||
TableCatalog *pattern.Match `json:"-"`
|
||||
TableSchema *pattern.Match `json:"-"`
|
||||
TableRel *pattern.Match `json:"-"`
|
||||
GoImportPath string `json:"-"`
|
||||
GoPackage string `json:"-"`
|
||||
GoTypeName string `json:"-"`
|
||||
GoBasicType bool `json:"-"`
|
||||
|
||||
// Parsed form of GoStructTag, e.g. {"validate:", "required"}
|
||||
GoStructTags map[string]string `json:"-"`
|
||||
ShimOverride *ShimOverride `json:"-"`
|
||||
}
|
||||
|
||||
func (o *Override) Matches(n *plugin.Identifier, defaultSchema string) bool {
|
||||
if n == nil {
|
||||
return false
|
||||
}
|
||||
schema := n.Schema
|
||||
if n.Schema == "" {
|
||||
schema = defaultSchema
|
||||
}
|
||||
if o.TableCatalog != nil && !o.TableCatalog.MatchString(n.Catalog) {
|
||||
return false
|
||||
}
|
||||
if o.TableSchema == nil && schema != "" {
|
||||
return false
|
||||
}
|
||||
if o.TableSchema != nil && !o.TableSchema.MatchString(schema) {
|
||||
return false
|
||||
}
|
||||
if o.TableRel == nil && n.Name != "" {
|
||||
return false
|
||||
}
|
||||
if o.TableRel != nil && !o.TableRel.MatchString(n.Name) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (o *Override) MatchesColumn(col *plugin.Column) bool {
|
||||
columnType := sdk.DataType(col.Type)
|
||||
notNull := col.NotNull || col.IsArray
|
||||
return o.DBType != "" && o.DBType == columnType && o.Nullable != notNull && o.Unsigned == col.Unsigned
|
||||
}
|
||||
|
||||
func (o *Override) parse(req *plugin.GenerateRequest) (err error) {
|
||||
// validate deprecated postgres_type field
|
||||
if o.Deprecated_PostgresType != "" {
|
||||
fmt.Fprintf(os.Stderr, "WARNING: \"postgres_type\" is deprecated. Instead, use \"db_type\" to specify a type override.\n")
|
||||
if o.DBType != "" {
|
||||
return fmt.Errorf(`Type override configurations cannot have "db_type" and "postres_type" together. Use "db_type" alone`)
|
||||
}
|
||||
o.DBType = o.Deprecated_PostgresType
|
||||
}
|
||||
|
||||
// validate deprecated null field
|
||||
if o.Deprecated_Null {
|
||||
fmt.Fprintf(os.Stderr, "WARNING: \"null\" is deprecated. Instead, use the \"nullable\" field.\n")
|
||||
o.Nullable = true
|
||||
}
|
||||
|
||||
schema := "public"
|
||||
if req != nil && req.Catalog != nil {
|
||||
schema = req.Catalog.DefaultSchema
|
||||
}
|
||||
|
||||
// validate option combinations
|
||||
switch {
|
||||
case o.Column != "" && o.DBType != "":
|
||||
return fmt.Errorf("Override specifying both `column` (%q) and `db_type` (%q) is not valid.", o.Column, o.DBType)
|
||||
case o.Column == "" && o.DBType == "":
|
||||
return fmt.Errorf("Override must specify one of either `column` or `db_type`")
|
||||
}
|
||||
|
||||
// validate Column
|
||||
if o.Column != "" {
|
||||
colParts := strings.Split(o.Column, ".")
|
||||
switch len(colParts) {
|
||||
case 2:
|
||||
if o.ColumnName, err = pattern.MatchCompile(colParts[1]); err != nil {
|
||||
return err
|
||||
}
|
||||
if o.TableRel, err = pattern.MatchCompile(colParts[0]); err != nil {
|
||||
return err
|
||||
}
|
||||
if o.TableSchema, err = pattern.MatchCompile(schema); err != nil {
|
||||
return err
|
||||
}
|
||||
case 3:
|
||||
if o.ColumnName, err = pattern.MatchCompile(colParts[2]); err != nil {
|
||||
return err
|
||||
}
|
||||
if o.TableRel, err = pattern.MatchCompile(colParts[1]); err != nil {
|
||||
return err
|
||||
}
|
||||
if o.TableSchema, err = pattern.MatchCompile(colParts[0]); err != nil {
|
||||
return err
|
||||
}
|
||||
case 4:
|
||||
if o.ColumnName, err = pattern.MatchCompile(colParts[3]); err != nil {
|
||||
return err
|
||||
}
|
||||
if o.TableRel, err = pattern.MatchCompile(colParts[2]); err != nil {
|
||||
return err
|
||||
}
|
||||
if o.TableSchema, err = pattern.MatchCompile(colParts[1]); err != nil {
|
||||
return err
|
||||
}
|
||||
if o.TableCatalog, err = pattern.MatchCompile(colParts[0]); err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("Override `column` specifier %q is not the proper format, expected '[catalog.][schema.]tablename.colname'", o.Column)
|
||||
}
|
||||
}
|
||||
|
||||
// validate GoType
|
||||
parsed, err := o.GoType.parse()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
o.GoImportPath = parsed.ImportPath
|
||||
o.GoPackage = parsed.Package
|
||||
o.GoTypeName = parsed.TypeName
|
||||
o.GoBasicType = parsed.BasicType
|
||||
|
||||
// validate GoStructTag
|
||||
tags, err := o.GoStructTag.parse()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
o.GoStructTags = tags
|
||||
|
||||
o.ShimOverride = shimOverride(req, o)
|
||||
return nil
|
||||
}
|
||||
117
internal/codegen/golang/opts/override_test.go
Normal file
117
internal/codegen/golang/opts/override_test.go
Normal file
@@ -0,0 +1,117 @@
|
||||
package opts
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
)
|
||||
|
||||
func TestTypeOverrides(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
override Override
|
||||
pkg string
|
||||
typeName string
|
||||
basic bool
|
||||
}{
|
||||
{
|
||||
Override{
|
||||
DBType: "uuid",
|
||||
GoType: GoType{Spec: "github.com/segmentio/ksuid.KSUID"},
|
||||
},
|
||||
"github.com/segmentio/ksuid",
|
||||
"ksuid.KSUID",
|
||||
false,
|
||||
},
|
||||
// TODO: Add test for struct pointers
|
||||
//
|
||||
// {
|
||||
// Override{
|
||||
// DBType: "uuid",
|
||||
// GoType: "github.com/segmentio/*ksuid.KSUID",
|
||||
// },
|
||||
// "github.com/segmentio/ksuid",
|
||||
// "*ksuid.KSUID",
|
||||
// false,
|
||||
// },
|
||||
{
|
||||
Override{
|
||||
DBType: "citext",
|
||||
GoType: GoType{Spec: "string"},
|
||||
},
|
||||
"",
|
||||
"string",
|
||||
true,
|
||||
},
|
||||
{
|
||||
Override{
|
||||
DBType: "timestamp",
|
||||
GoType: GoType{Spec: "time.Time"},
|
||||
},
|
||||
"time",
|
||||
"time.Time",
|
||||
false,
|
||||
},
|
||||
} {
|
||||
tt := test
|
||||
t.Run(tt.override.GoType.Spec, func(t *testing.T) {
|
||||
if err := tt.override.parse(nil); err != nil {
|
||||
t.Fatalf("override parsing failed; %s", err)
|
||||
}
|
||||
if diff := cmp.Diff(tt.pkg, tt.override.GoImportPath); diff != "" {
|
||||
t.Errorf("package mismatch;\n%s", diff)
|
||||
}
|
||||
if diff := cmp.Diff(tt.typeName, tt.override.GoTypeName); diff != "" {
|
||||
t.Errorf("type name mismatch;\n%s", diff)
|
||||
}
|
||||
if diff := cmp.Diff(tt.basic, tt.override.GoBasicType); diff != "" {
|
||||
t.Errorf("basic mismatch;\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
for _, test := range []struct {
|
||||
override Override
|
||||
err string
|
||||
}{
|
||||
{
|
||||
Override{
|
||||
DBType: "uuid",
|
||||
GoType: GoType{Spec: "Pointer"},
|
||||
},
|
||||
"Package override `go_type` specifier \"Pointer\" is not a Go basic type e.g. 'string'",
|
||||
},
|
||||
{
|
||||
Override{
|
||||
DBType: "uuid",
|
||||
GoType: GoType{Spec: "untyped rune"},
|
||||
},
|
||||
"Package override `go_type` specifier \"untyped rune\" is not a Go basic type e.g. 'string'",
|
||||
},
|
||||
} {
|
||||
tt := test
|
||||
t.Run(tt.override.GoType.Spec, func(t *testing.T) {
|
||||
err := tt.override.parse(nil)
|
||||
if err == nil {
|
||||
t.Fatalf("expected parse to fail; got nil")
|
||||
}
|
||||
if diff := cmp.Diff(tt.err, err.Error()); diff != "" {
|
||||
t.Errorf("error mismatch;\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func FuzzOverride(f *testing.F) {
|
||||
for _, spec := range []string{
|
||||
"string",
|
||||
"github.com/gofrs/uuid.UUID",
|
||||
"github.com/segmentio/ksuid.KSUID",
|
||||
} {
|
||||
f.Add(spec)
|
||||
}
|
||||
f.Fuzz(func(t *testing.T, s string) {
|
||||
o := Override{
|
||||
GoType: GoType{Spec: s},
|
||||
}
|
||||
o.parse(nil)
|
||||
})
|
||||
}
|
||||
77
internal/codegen/golang/opts/shim.go
Normal file
77
internal/codegen/golang/opts/shim.go
Normal file
@@ -0,0 +1,77 @@
|
||||
package opts
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/sqlc-dev/sqlc/internal/plugin"
|
||||
)
|
||||
|
||||
// The ShimOverride struct exists to bridge the gap between the Override struct
|
||||
// and the previous Override struct defined in codegen.proto. Eventually these
|
||||
// shim structs should be removed in favor of using the existing Override and
|
||||
// GoType structs, but it's easier to provide these shim structs to not change
|
||||
// the existing, working code.
|
||||
type ShimOverride struct {
|
||||
DbType string
|
||||
Nullable bool
|
||||
Column string
|
||||
Table *plugin.Identifier
|
||||
ColumnName string
|
||||
Unsigned bool
|
||||
GoType *ShimGoType
|
||||
}
|
||||
|
||||
func shimOverride(req *plugin.GenerateRequest, o *Override) *ShimOverride {
|
||||
var column string
|
||||
var table plugin.Identifier
|
||||
|
||||
if o.Column != "" {
|
||||
colParts := strings.Split(o.Column, ".")
|
||||
switch len(colParts) {
|
||||
case 2:
|
||||
table.Schema = req.Catalog.DefaultSchema
|
||||
table.Name = colParts[0]
|
||||
column = colParts[1]
|
||||
case 3:
|
||||
table.Schema = colParts[0]
|
||||
table.Name = colParts[1]
|
||||
column = colParts[2]
|
||||
case 4:
|
||||
table.Catalog = colParts[0]
|
||||
table.Schema = colParts[1]
|
||||
table.Name = colParts[2]
|
||||
column = colParts[3]
|
||||
}
|
||||
}
|
||||
return &ShimOverride{
|
||||
DbType: o.DBType,
|
||||
Nullable: o.Nullable,
|
||||
Unsigned: o.Unsigned,
|
||||
Column: o.Column,
|
||||
ColumnName: column,
|
||||
Table: &table,
|
||||
GoType: shimGoType(o),
|
||||
}
|
||||
}
|
||||
|
||||
type ShimGoType struct {
|
||||
ImportPath string
|
||||
Package string
|
||||
TypeName string
|
||||
BasicType bool
|
||||
StructTags map[string]string
|
||||
}
|
||||
|
||||
func shimGoType(o *Override) *ShimGoType {
|
||||
// Note that there is a slight mismatch between this and the
|
||||
// proto api. The GoType on the override is the unparsed type,
|
||||
// which could be a qualified path or an object, as per
|
||||
// https://docs.sqlc.dev/en/v1.18.0/reference/config.html#type-overriding
|
||||
return &ShimGoType{
|
||||
ImportPath: o.GoImportPath,
|
||||
Package: o.GoPackage,
|
||||
TypeName: o.GoTypeName,
|
||||
BasicType: o.GoBasicType,
|
||||
StructTags: o.GoStructTags,
|
||||
}
|
||||
}
|
||||
619
internal/codegen/golang/postgresql_type.go
Normal file
619
internal/codegen/golang/postgresql_type.go
Normal file
@@ -0,0 +1,619 @@
|
||||
package golang
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
|
||||
"github.com/sqlc-dev/sqlc/internal/codegen/golang/opts"
|
||||
"github.com/sqlc-dev/sqlc/internal/codegen/sdk"
|
||||
"github.com/sqlc-dev/sqlc/internal/debug"
|
||||
"github.com/sqlc-dev/sqlc/internal/plugin"
|
||||
)
|
||||
|
||||
func parseIdentifierString(name string) (*plugin.Identifier, error) {
|
||||
parts := strings.Split(name, ".")
|
||||
switch len(parts) {
|
||||
case 1:
|
||||
return &plugin.Identifier{
|
||||
Name: parts[0],
|
||||
}, nil
|
||||
case 2:
|
||||
return &plugin.Identifier{
|
||||
Schema: parts[0],
|
||||
Name: parts[1],
|
||||
}, nil
|
||||
case 3:
|
||||
return &plugin.Identifier{
|
||||
Catalog: parts[0],
|
||||
Schema: parts[1],
|
||||
Name: parts[2],
|
||||
}, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid name: %s", name)
|
||||
}
|
||||
}
|
||||
|
||||
func postgresType(req *plugin.GenerateRequest, options *opts.Options, col *plugin.Column) string {
|
||||
columnType := sdk.DataType(col.Type)
|
||||
notNull := col.NotNull || col.IsArray
|
||||
driver := parseDriver(options.SqlPackage)
|
||||
emitPointersForNull := driver.IsPGX() && options.EmitPointersForNullTypes
|
||||
emitPointersForNullEnums := emitPointersForNull
|
||||
if options.EmitPointersForNullEnumTypes != nil {
|
||||
emitPointersForNullEnums = driver.IsPGX() && *options.EmitPointersForNullEnumTypes
|
||||
}
|
||||
|
||||
switch columnType {
|
||||
case "serial", "serial4", "pg_catalog.serial4":
|
||||
if notNull {
|
||||
return "int32"
|
||||
}
|
||||
if emitPointersForNull {
|
||||
return "*int32"
|
||||
}
|
||||
if driver == opts.SQLDriverPGXV5 {
|
||||
return "pgtype.Int4"
|
||||
}
|
||||
return "sql.NullInt32"
|
||||
|
||||
case "bigserial", "serial8", "pg_catalog.serial8":
|
||||
if notNull {
|
||||
return "int64"
|
||||
}
|
||||
if emitPointersForNull {
|
||||
return "*int64"
|
||||
}
|
||||
if driver == opts.SQLDriverPGXV5 {
|
||||
return "pgtype.Int8"
|
||||
}
|
||||
return "sql.NullInt64"
|
||||
|
||||
case "smallserial", "serial2", "pg_catalog.serial2":
|
||||
if notNull {
|
||||
return "int16"
|
||||
}
|
||||
if emitPointersForNull {
|
||||
return "*int16"
|
||||
}
|
||||
if driver == opts.SQLDriverPGXV5 {
|
||||
return "pgtype.Int2"
|
||||
}
|
||||
return "sql.NullInt16"
|
||||
|
||||
case "integer", "int", "int4", "pg_catalog.int4":
|
||||
if notNull {
|
||||
return "int32"
|
||||
}
|
||||
if emitPointersForNull {
|
||||
return "*int32"
|
||||
}
|
||||
if driver == opts.SQLDriverPGXV5 {
|
||||
return "pgtype.Int4"
|
||||
}
|
||||
return "sql.NullInt32"
|
||||
|
||||
case "bigint", "int8", "pg_catalog.int8":
|
||||
if notNull {
|
||||
return "int64"
|
||||
}
|
||||
if emitPointersForNull {
|
||||
return "*int64"
|
||||
}
|
||||
if driver == opts.SQLDriverPGXV5 {
|
||||
return "pgtype.Int8"
|
||||
}
|
||||
return "sql.NullInt64"
|
||||
|
||||
case "smallint", "int2", "pg_catalog.int2":
|
||||
if notNull {
|
||||
return "int16"
|
||||
}
|
||||
if emitPointersForNull {
|
||||
return "*int16"
|
||||
}
|
||||
if driver == opts.SQLDriverPGXV5 {
|
||||
return "pgtype.Int2"
|
||||
}
|
||||
return "sql.NullInt16"
|
||||
|
||||
case "float", "double precision", "float8", "pg_catalog.float8":
|
||||
if notNull {
|
||||
return "float64"
|
||||
}
|
||||
if emitPointersForNull {
|
||||
return "*float64"
|
||||
}
|
||||
if driver == opts.SQLDriverPGXV5 {
|
||||
return "pgtype.Float8"
|
||||
}
|
||||
return "sql.NullFloat64"
|
||||
|
||||
case "real", "float4", "pg_catalog.float4":
|
||||
if notNull {
|
||||
return "float32"
|
||||
}
|
||||
if emitPointersForNull {
|
||||
return "*float32"
|
||||
}
|
||||
if driver == opts.SQLDriverPGXV5 {
|
||||
return "pgtype.Float4"
|
||||
}
|
||||
return "sql.NullFloat64" // TODO: Change to sql.NullFloat32 after updating the go.mod file
|
||||
|
||||
case "numeric", "pg_catalog.numeric", "money":
|
||||
if driver.IsPGX() {
|
||||
return "pgtype.Numeric"
|
||||
}
|
||||
// Since the Go standard library does not have a decimal type, lib/pq
|
||||
// returns numerics as strings.
|
||||
//
|
||||
// https://github.com/lib/pq/issues/648
|
||||
if notNull {
|
||||
return "string"
|
||||
}
|
||||
if emitPointersForNull {
|
||||
return "*string"
|
||||
}
|
||||
return "sql.NullString"
|
||||
|
||||
case "boolean", "bool", "pg_catalog.bool":
|
||||
if notNull {
|
||||
return "bool"
|
||||
}
|
||||
if emitPointersForNull {
|
||||
return "*bool"
|
||||
}
|
||||
if driver == opts.SQLDriverPGXV5 {
|
||||
return "pgtype.Bool"
|
||||
}
|
||||
return "sql.NullBool"
|
||||
|
||||
case "json", "pg_catalog.json":
|
||||
switch driver {
|
||||
case opts.SQLDriverPGXV5:
|
||||
return "[]byte"
|
||||
case opts.SQLDriverPGXV4:
|
||||
return "pgtype.JSON"
|
||||
case opts.SQLDriverLibPQ:
|
||||
if notNull {
|
||||
return "json.RawMessage"
|
||||
} else {
|
||||
return "pqtype.NullRawMessage"
|
||||
}
|
||||
default:
|
||||
return "any"
|
||||
}
|
||||
|
||||
case "jsonb", "pg_catalog.jsonb":
|
||||
switch driver {
|
||||
case opts.SQLDriverPGXV5:
|
||||
return "[]byte"
|
||||
case opts.SQLDriverPGXV4:
|
||||
return "pgtype.JSONB"
|
||||
case opts.SQLDriverLibPQ:
|
||||
if notNull {
|
||||
return "json.RawMessage"
|
||||
} else {
|
||||
return "pqtype.NullRawMessage"
|
||||
}
|
||||
default:
|
||||
return "any"
|
||||
}
|
||||
|
||||
case "bytea", "blob", "pg_catalog.bytea":
|
||||
return "[]byte"
|
||||
|
||||
case "date":
|
||||
if driver == opts.SQLDriverPGXV5 {
|
||||
return "pgtype.Date"
|
||||
}
|
||||
if notNull {
|
||||
return "time.Time"
|
||||
}
|
||||
if emitPointersForNull {
|
||||
return "*time.Time"
|
||||
}
|
||||
return "sql.NullTime"
|
||||
|
||||
case "pg_catalog.time", "time", "time without time zone":
|
||||
if driver == opts.SQLDriverPGXV5 {
|
||||
return "pgtype.Time"
|
||||
}
|
||||
if notNull {
|
||||
return "time.Time"
|
||||
}
|
||||
if emitPointersForNull {
|
||||
return "*time.Time"
|
||||
}
|
||||
return "sql.NullTime"
|
||||
|
||||
case "pg_catalog.timetz", "timetz", "time with time zone":
|
||||
if notNull {
|
||||
return "time.Time"
|
||||
}
|
||||
if emitPointersForNull {
|
||||
return "*time.Time"
|
||||
}
|
||||
return "sql.NullTime"
|
||||
|
||||
case "pg_catalog.timestamp", "timestamp", "timestamp without time zone":
|
||||
if driver == opts.SQLDriverPGXV5 {
|
||||
return "pgtype.Timestamp"
|
||||
}
|
||||
if notNull {
|
||||
return "time.Time"
|
||||
}
|
||||
if emitPointersForNull {
|
||||
return "*time.Time"
|
||||
}
|
||||
return "sql.NullTime"
|
||||
|
||||
case "pg_catalog.timestamptz", "timestamptz", "timestamp with time zone":
|
||||
if driver == opts.SQLDriverPGXV5 {
|
||||
return "pgtype.Timestamptz"
|
||||
}
|
||||
if notNull {
|
||||
return "time.Time"
|
||||
}
|
||||
if emitPointersForNull {
|
||||
return "*time.Time"
|
||||
}
|
||||
return "sql.NullTime"
|
||||
|
||||
case "text", "pg_catalog.varchar", "varchar", "character varying", "pg_catalog.bpchar", "bpchar", "character", "string", "citext", "name":
|
||||
if notNull {
|
||||
return "string"
|
||||
}
|
||||
if emitPointersForNull {
|
||||
return "*string"
|
||||
}
|
||||
if driver == opts.SQLDriverPGXV5 {
|
||||
return "pgtype.Text"
|
||||
}
|
||||
return "sql.NullString"
|
||||
|
||||
case "uuid":
|
||||
if driver == opts.SQLDriverPGXV5 {
|
||||
return "pgtype.UUID"
|
||||
}
|
||||
if notNull {
|
||||
return "uuid.UUID"
|
||||
}
|
||||
if emitPointersForNull {
|
||||
return "*uuid.UUID"
|
||||
}
|
||||
return "uuid.NullUUID"
|
||||
|
||||
case "inet":
|
||||
switch driver {
|
||||
case opts.SQLDriverPGXV5:
|
||||
if notNull {
|
||||
return "netip.Addr"
|
||||
}
|
||||
return "*netip.Addr"
|
||||
case opts.SQLDriverPGXV4:
|
||||
return "pgtype.Inet"
|
||||
case opts.SQLDriverLibPQ:
|
||||
return "pqtype.Inet"
|
||||
default:
|
||||
return "any"
|
||||
}
|
||||
|
||||
case "cidr":
|
||||
switch driver {
|
||||
case opts.SQLDriverPGXV5:
|
||||
if notNull {
|
||||
return "netip.Prefix"
|
||||
}
|
||||
return "*netip.Prefix"
|
||||
case opts.SQLDriverPGXV4:
|
||||
return "pgtype.CIDR"
|
||||
case opts.SQLDriverLibPQ:
|
||||
return "pqtype.CIDR"
|
||||
default:
|
||||
return "any"
|
||||
}
|
||||
|
||||
case "macaddr", "macaddr8":
|
||||
switch driver {
|
||||
case opts.SQLDriverPGXV5:
|
||||
return "net.HardwareAddr"
|
||||
case opts.SQLDriverPGXV4:
|
||||
return "pgtype.Macaddr"
|
||||
case opts.SQLDriverLibPQ:
|
||||
return "pqtype.Macaddr"
|
||||
default:
|
||||
return "any"
|
||||
}
|
||||
|
||||
case "ltree", "lquery", "ltxtquery":
|
||||
// This module implements a data type ltree for representing labels
|
||||
// of data stored in a hierarchical tree-like structure. Extensive
|
||||
// facilities for searching through label trees are provided.
|
||||
//
|
||||
// https://www.postgresql.org/docs/current/ltree.html
|
||||
if notNull {
|
||||
return "string"
|
||||
}
|
||||
if emitPointersForNull {
|
||||
return "*string"
|
||||
}
|
||||
if driver == opts.SQLDriverPGXV5 {
|
||||
return "pgtype.Text"
|
||||
}
|
||||
return "sql.NullString"
|
||||
|
||||
case "interval", "pg_catalog.interval":
|
||||
if driver == opts.SQLDriverPGXV5 {
|
||||
return "pgtype.Interval"
|
||||
}
|
||||
if notNull {
|
||||
return "int64"
|
||||
}
|
||||
if emitPointersForNull {
|
||||
return "*int64"
|
||||
}
|
||||
return "sql.NullInt64"
|
||||
|
||||
case "daterange":
|
||||
switch driver {
|
||||
case opts.SQLDriverPGXV4:
|
||||
return "pgtype.Daterange"
|
||||
case opts.SQLDriverPGXV5:
|
||||
return "pgtype.Range[pgtype.Date]"
|
||||
default:
|
||||
return "any"
|
||||
}
|
||||
|
||||
case "datemultirange":
|
||||
switch driver {
|
||||
case opts.SQLDriverPGXV5:
|
||||
return "pgtype.Multirange[pgtype.Range[pgtype.Date]]"
|
||||
default:
|
||||
return "any"
|
||||
}
|
||||
|
||||
case "tsrange":
|
||||
switch driver {
|
||||
case opts.SQLDriverPGXV4:
|
||||
return "pgtype.Tsrange"
|
||||
case opts.SQLDriverPGXV5:
|
||||
return "pgtype.Range[pgtype.Timestamp]"
|
||||
default:
|
||||
return "any"
|
||||
}
|
||||
|
||||
case "tsmultirange":
|
||||
switch driver {
|
||||
case opts.SQLDriverPGXV5:
|
||||
return "pgtype.Multirange[pgtype.Range[pgtype.Timestamp]]"
|
||||
default:
|
||||
return "any"
|
||||
}
|
||||
|
||||
case "tstzrange":
|
||||
switch driver {
|
||||
case opts.SQLDriverPGXV4:
|
||||
return "pgtype.Tstzrange"
|
||||
case opts.SQLDriverPGXV5:
|
||||
return "pgtype.Range[pgtype.Timestamptz]"
|
||||
default:
|
||||
return "any"
|
||||
}
|
||||
|
||||
case "tstzmultirange":
|
||||
switch driver {
|
||||
case opts.SQLDriverPGXV5:
|
||||
return "pgtype.Multirange[pgtype.Range[pgtype.Timestamptz]]"
|
||||
default:
|
||||
return "any"
|
||||
}
|
||||
|
||||
case "numrange":
|
||||
switch driver {
|
||||
case opts.SQLDriverPGXV4:
|
||||
return "pgtype.Numrange"
|
||||
case opts.SQLDriverPGXV5:
|
||||
return "pgtype.Range[pgtype.Numeric]"
|
||||
default:
|
||||
return "any"
|
||||
}
|
||||
|
||||
case "nummultirange":
|
||||
switch driver {
|
||||
case opts.SQLDriverPGXV5:
|
||||
return "pgtype.Multirange[pgtype.Range[pgtype.Numeric]]"
|
||||
default:
|
||||
return "any"
|
||||
}
|
||||
|
||||
case "int4range":
|
||||
switch driver {
|
||||
case opts.SQLDriverPGXV4:
|
||||
return "pgtype.Int4range"
|
||||
case opts.SQLDriverPGXV5:
|
||||
return "pgtype.Range[pgtype.Int4]"
|
||||
default:
|
||||
return "any"
|
||||
}
|
||||
|
||||
case "int4multirange":
|
||||
switch driver {
|
||||
case opts.SQLDriverPGXV5:
|
||||
return "pgtype.Multirange[pgtype.Range[pgtype.Int4]]"
|
||||
default:
|
||||
return "any"
|
||||
}
|
||||
|
||||
case "int8range":
|
||||
switch driver {
|
||||
case opts.SQLDriverPGXV4:
|
||||
return "pgtype.Int8range"
|
||||
case opts.SQLDriverPGXV5:
|
||||
return "pgtype.Range[pgtype.Int8]"
|
||||
default:
|
||||
return "any"
|
||||
}
|
||||
|
||||
case "int8multirange":
|
||||
switch driver {
|
||||
case opts.SQLDriverPGXV5:
|
||||
return "pgtype.Multirange[pgtype.Range[pgtype.Int8]]"
|
||||
default:
|
||||
return "any"
|
||||
}
|
||||
|
||||
case "hstore":
|
||||
if driver.IsPGX() {
|
||||
return "pgtype.Hstore"
|
||||
}
|
||||
return "any"
|
||||
|
||||
case "bit", "varbit", "bit varying", "pg_catalog.bit", "pg_catalog.varbit":
|
||||
if driver == opts.SQLDriverPGXV5 {
|
||||
return "pgtype.Bits"
|
||||
}
|
||||
if driver == opts.SQLDriverPGXV4 {
|
||||
return "pgtype.Varbit"
|
||||
}
|
||||
|
||||
case "cid":
|
||||
if driver == opts.SQLDriverPGXV5 {
|
||||
return "pgtype.Uint32"
|
||||
}
|
||||
if driver == opts.SQLDriverPGXV4 {
|
||||
return "pgtype.CID"
|
||||
}
|
||||
|
||||
case "oid":
|
||||
if driver == opts.SQLDriverPGXV5 {
|
||||
return "pgtype.Uint32"
|
||||
}
|
||||
if driver == opts.SQLDriverPGXV4 {
|
||||
return "pgtype.OID"
|
||||
}
|
||||
|
||||
case "tid":
|
||||
if driver.IsPGX() {
|
||||
return "pgtype.TID"
|
||||
}
|
||||
|
||||
case "xid":
|
||||
if driver == opts.SQLDriverPGXV5 {
|
||||
return "pgtype.Uint32"
|
||||
}
|
||||
if driver == opts.SQLDriverPGXV4 {
|
||||
return "pgtype.XID"
|
||||
}
|
||||
|
||||
case "xid8":
|
||||
if driver == opts.SQLDriverPGXV5 {
|
||||
return "pgtype.Uint64"
|
||||
}
|
||||
|
||||
case "box":
|
||||
if driver.IsPGX() {
|
||||
return "pgtype.Box"
|
||||
}
|
||||
|
||||
case "circle":
|
||||
if driver.IsPGX() {
|
||||
return "pgtype.Circle"
|
||||
}
|
||||
|
||||
case "line":
|
||||
if driver.IsPGX() {
|
||||
return "pgtype.Line"
|
||||
}
|
||||
|
||||
case "lseg":
|
||||
if driver.IsPGX() {
|
||||
return "pgtype.Lseg"
|
||||
}
|
||||
|
||||
case "path":
|
||||
if driver.IsPGX() {
|
||||
return "pgtype.Path"
|
||||
}
|
||||
|
||||
case "point":
|
||||
if driver.IsPGX() {
|
||||
return "pgtype.Point"
|
||||
}
|
||||
|
||||
case "polygon":
|
||||
if driver.IsPGX() {
|
||||
return "pgtype.Polygon"
|
||||
}
|
||||
|
||||
case "vector":
|
||||
if driver == opts.SQLDriverPGXV5 {
|
||||
if emitPointersForNull {
|
||||
return "*pgvector.Vector"
|
||||
} else {
|
||||
return "pgvector.Vector"
|
||||
}
|
||||
}
|
||||
|
||||
case "void":
|
||||
// A void value can only be scanned into an empty interface.
|
||||
return "any"
|
||||
|
||||
case "any":
|
||||
return "any"
|
||||
|
||||
default:
|
||||
rel, err := parseIdentifierString(columnType)
|
||||
if err != nil {
|
||||
// TODO: Should this actually return an error here?
|
||||
return "any"
|
||||
}
|
||||
if rel.Schema == "" {
|
||||
rel.Schema = req.Catalog.DefaultSchema
|
||||
}
|
||||
|
||||
for _, schema := range req.Catalog.Schemas {
|
||||
if schema.Name == "pg_catalog" || schema.Name == "information_schema" {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, enum := range schema.Enums {
|
||||
if rel.Name == enum.Name && rel.Schema == schema.Name {
|
||||
if notNull {
|
||||
if schema.Name == req.Catalog.DefaultSchema {
|
||||
return StructName(enum.Name, options)
|
||||
}
|
||||
return StructName(schema.Name+"_"+enum.Name, options)
|
||||
} else {
|
||||
nullPrefix := "Null"
|
||||
if emitPointersForNullEnums {
|
||||
nullPrefix = "*"
|
||||
}
|
||||
if schema.Name == req.Catalog.DefaultSchema {
|
||||
return nullPrefix + StructName(enum.Name, options)
|
||||
}
|
||||
return nullPrefix + StructName(schema.Name+"_"+enum.Name, options)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, ct := range schema.CompositeTypes {
|
||||
if rel.Name == ct.Name && rel.Schema == schema.Name {
|
||||
if notNull {
|
||||
return "string"
|
||||
}
|
||||
if emitPointersForNull {
|
||||
return "*string"
|
||||
}
|
||||
return "sql.NullString"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if debug.Active {
|
||||
log.Printf("unknown PostgreSQL type: %s\n", columnType)
|
||||
}
|
||||
return "any"
|
||||
}
|
||||
79
internal/codegen/golang/qualify.go
Normal file
79
internal/codegen/golang/qualify.go
Normal file
@@ -0,0 +1,79 @@
|
||||
package golang
|
||||
|
||||
import "strings"
|
||||
|
||||
// stripQualifier removes a leading slice/pointer prefix and the given
|
||||
// `pkg.` qualifier from a Go type expression. When the qualifier is empty
|
||||
// or absent from the type, the input is returned unchanged.
|
||||
func stripQualifier(t, qualifier string) string {
|
||||
if qualifier == "" {
|
||||
return t
|
||||
}
|
||||
var prefix strings.Builder
|
||||
rest := t
|
||||
for {
|
||||
if strings.HasPrefix(rest, "[]") {
|
||||
prefix.WriteString("[]")
|
||||
rest = rest[2:]
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(rest, "*") {
|
||||
prefix.WriteString("*")
|
||||
rest = rest[1:]
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
if strings.HasPrefix(rest, qualifier) {
|
||||
return prefix.String() + rest[len(qualifier):]
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
// modelTypeSet is the set of Go type names that live in the models file.
|
||||
type modelTypeSet map[string]struct{}
|
||||
|
||||
// buildModelTypeSet returns the set of type names that are declared in
|
||||
// models.go for the current codegen invocation.
|
||||
func buildModelTypeSet(enums []Enum, structs []Struct) modelTypeSet {
|
||||
set := make(modelTypeSet, len(enums)*4+len(structs))
|
||||
for _, e := range enums {
|
||||
set[e.Name] = struct{}{}
|
||||
set["Null"+e.Name] = struct{}{}
|
||||
}
|
||||
for _, s := range structs {
|
||||
if s.IsModel {
|
||||
set[s.Name] = struct{}{}
|
||||
}
|
||||
}
|
||||
return set
|
||||
}
|
||||
|
||||
// qualifyType prefixes a Go type expression with `qualifier` when the bare
|
||||
// type name belongs to `models`. Slice and pointer prefixes are preserved.
|
||||
// When qualifier is empty (i.e. models live in the queries package), the
|
||||
// input is returned unchanged.
|
||||
func qualifyType(t string, models modelTypeSet, qualifier string) string {
|
||||
if qualifier == "" || t == "" || len(models) == 0 {
|
||||
return t
|
||||
}
|
||||
var prefix strings.Builder
|
||||
rest := t
|
||||
for {
|
||||
if strings.HasPrefix(rest, "[]") {
|
||||
prefix.WriteString("[]")
|
||||
rest = rest[2:]
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(rest, "*") {
|
||||
prefix.WriteString("*")
|
||||
rest = rest[1:]
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
if _, ok := models[rest]; ok {
|
||||
return prefix.String() + qualifier + rest
|
||||
}
|
||||
return t
|
||||
}
|
||||
308
internal/codegen/golang/query.go
Normal file
308
internal/codegen/golang/query.go
Normal file
@@ -0,0 +1,308 @@
|
||||
package golang
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/sqlc-dev/sqlc/internal/codegen/golang/opts"
|
||||
"github.com/sqlc-dev/sqlc/internal/metadata"
|
||||
"github.com/sqlc-dev/sqlc/internal/plugin"
|
||||
)
|
||||
|
||||
type QueryValue struct {
|
||||
Emit bool
|
||||
EmitPointer bool
|
||||
Name string
|
||||
DBName string // The name of the field in the database. Only set if Struct==nil.
|
||||
Struct *Struct
|
||||
Typ string
|
||||
SQLDriver opts.SQLDriver
|
||||
|
||||
// ModelQualifier prefixes references to model types when the models file
|
||||
// lives in a different Go package (e.g. "model."). Empty otherwise.
|
||||
ModelQualifier string
|
||||
|
||||
// Column is kept so late in the generation process around to differentiate
|
||||
// between mysql slices and pg arrays
|
||||
Column *plugin.Column
|
||||
}
|
||||
|
||||
func (v QueryValue) EmitStruct() bool {
|
||||
return v.Emit
|
||||
}
|
||||
|
||||
func (v QueryValue) IsStruct() bool {
|
||||
return v.Struct != nil
|
||||
}
|
||||
|
||||
func (v QueryValue) IsPointer() bool {
|
||||
return v.EmitPointer && v.Struct != nil
|
||||
}
|
||||
|
||||
func (v QueryValue) isEmpty() bool {
|
||||
return v.Typ == "" && v.Name == "" && v.Struct == nil
|
||||
}
|
||||
|
||||
type Argument struct {
|
||||
Name string
|
||||
Type string
|
||||
}
|
||||
|
||||
func (v QueryValue) Pair() string {
|
||||
var out []string
|
||||
for _, arg := range v.Pairs() {
|
||||
out = append(out, arg.Name+" "+arg.Type)
|
||||
}
|
||||
return strings.Join(out, ",")
|
||||
}
|
||||
|
||||
// Return the argument name and type for query methods. Should only be used in
|
||||
// the context of method arguments.
|
||||
func (v QueryValue) Pairs() []Argument {
|
||||
if v.isEmpty() {
|
||||
return nil
|
||||
}
|
||||
if !v.EmitStruct() && v.IsStruct() {
|
||||
var out []Argument
|
||||
for _, f := range v.Struct.Fields {
|
||||
out = append(out, Argument{
|
||||
Name: escape(toLowerCase(f.Name)),
|
||||
Type: f.Type,
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
return []Argument{
|
||||
{
|
||||
Name: escape(v.Name),
|
||||
Type: v.DefineType(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (v QueryValue) SlicePair() string {
|
||||
if v.isEmpty() {
|
||||
return ""
|
||||
}
|
||||
return v.Name + " []" + v.DefineType()
|
||||
}
|
||||
|
||||
func (v QueryValue) Type() string {
|
||||
if v.Typ != "" {
|
||||
return v.Typ
|
||||
}
|
||||
if v.Struct != nil {
|
||||
// Model structs (table-derived) live in the models file. When that
|
||||
// file is generated into a different Go package, references from
|
||||
// query files must be qualified. Synthetic structs (Params/Row)
|
||||
// are defined in the same query file as their use, so they stay
|
||||
// bare.
|
||||
if v.Struct.IsModel && v.ModelQualifier != "" {
|
||||
return v.ModelQualifier + v.Struct.Name
|
||||
}
|
||||
return v.Struct.Name
|
||||
}
|
||||
panic("no type for QueryValue: " + v.Name)
|
||||
}
|
||||
|
||||
func (v *QueryValue) DefineType() string {
|
||||
t := v.Type()
|
||||
if v.IsPointer() {
|
||||
return "*" + t
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
func (v *QueryValue) ReturnName() string {
|
||||
if v.IsPointer() {
|
||||
return "&" + escape(v.Name)
|
||||
}
|
||||
return escape(v.Name)
|
||||
}
|
||||
|
||||
func (v QueryValue) UniqueFields() []Field {
|
||||
seen := map[string]struct{}{}
|
||||
fields := make([]Field, 0, len(v.Struct.Fields))
|
||||
|
||||
for _, field := range v.Struct.Fields {
|
||||
if _, found := seen[field.Name]; found {
|
||||
continue
|
||||
}
|
||||
seen[field.Name] = struct{}{}
|
||||
fields = append(fields, field)
|
||||
}
|
||||
|
||||
return fields
|
||||
}
|
||||
|
||||
func (v QueryValue) Params() string {
|
||||
if v.isEmpty() {
|
||||
return ""
|
||||
}
|
||||
var out []string
|
||||
if v.Struct == nil {
|
||||
if !v.Column.IsSqlcSlice && strings.HasPrefix(v.Typ, "[]") && v.Typ != "[]byte" && !v.SQLDriver.IsPGX() {
|
||||
out = append(out, "pq.Array("+escape(v.Name)+")")
|
||||
} else {
|
||||
out = append(out, escape(v.Name))
|
||||
}
|
||||
} else {
|
||||
for _, f := range v.Struct.Fields {
|
||||
if !f.HasSqlcSlice() && strings.HasPrefix(f.Type, "[]") && f.Type != "[]byte" && !v.SQLDriver.IsPGX() {
|
||||
out = append(out, "pq.Array("+escape(v.VariableForField(f))+")")
|
||||
} else {
|
||||
out = append(out, escape(v.VariableForField(f)))
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(out) <= 3 {
|
||||
return strings.Join(out, ",")
|
||||
}
|
||||
out = append(out, "")
|
||||
return "\n" + strings.Join(out, ",\n")
|
||||
}
|
||||
|
||||
func (v QueryValue) ColumnNames() []string {
|
||||
if v.Struct == nil {
|
||||
return []string{v.DBName}
|
||||
}
|
||||
names := make([]string, len(v.Struct.Fields))
|
||||
for i, f := range v.Struct.Fields {
|
||||
names[i] = f.DBName
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
func (v QueryValue) ColumnNamesAsGoSlice() string {
|
||||
if v.Struct == nil {
|
||||
return fmt.Sprintf("[]string{%q}", v.DBName)
|
||||
}
|
||||
escapedNames := make([]string, len(v.Struct.Fields))
|
||||
for i, f := range v.Struct.Fields {
|
||||
if f.Column != nil && f.Column.OriginalName != "" {
|
||||
escapedNames[i] = fmt.Sprintf("%q", f.Column.OriginalName)
|
||||
} else {
|
||||
escapedNames[i] = fmt.Sprintf("%q", f.DBName)
|
||||
}
|
||||
}
|
||||
return "[]string{" + strings.Join(escapedNames, ", ") + "}"
|
||||
}
|
||||
|
||||
// When true, we have to build the arguments to q.db.QueryContext in addition to
|
||||
// munging the SQL
|
||||
func (v QueryValue) HasSqlcSlices() bool {
|
||||
if v.Struct == nil {
|
||||
return v.Column != nil && v.Column.IsSqlcSlice
|
||||
}
|
||||
for _, v := range v.Struct.Fields {
|
||||
if v.Column.IsSqlcSlice {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (v QueryValue) Scan() string {
|
||||
var out []string
|
||||
if v.Struct == nil {
|
||||
if strings.HasPrefix(v.Typ, "[]") && v.Typ != "[]byte" && !v.SQLDriver.IsPGX() {
|
||||
out = append(out, "pq.Array(&"+v.Name+")")
|
||||
} else {
|
||||
out = append(out, "&"+v.Name)
|
||||
}
|
||||
} else {
|
||||
for _, f := range v.Struct.Fields {
|
||||
|
||||
// append any embedded fields
|
||||
if len(f.EmbedFields) > 0 {
|
||||
for _, embed := range f.EmbedFields {
|
||||
if strings.HasPrefix(embed.Type, "[]") && embed.Type != "[]byte" && !v.SQLDriver.IsPGX() {
|
||||
out = append(out, "pq.Array(&"+v.Name+"."+f.Name+"."+embed.Name+")")
|
||||
} else {
|
||||
out = append(out, "&"+v.Name+"."+f.Name+"."+embed.Name)
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.HasPrefix(f.Type, "[]") && f.Type != "[]byte" && !v.SQLDriver.IsPGX() {
|
||||
out = append(out, "pq.Array(&"+v.Name+"."+f.Name+")")
|
||||
} else {
|
||||
out = append(out, "&"+v.Name+"."+f.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(out) <= 3 {
|
||||
return strings.Join(out, ",")
|
||||
}
|
||||
out = append(out, "")
|
||||
return "\n" + strings.Join(out, ",\n")
|
||||
}
|
||||
|
||||
// Deprecated: This method does not respect the Emit field set on the
|
||||
// QueryValue. It's used by the go-sql-driver-mysql/copyfromCopy.tmpl and should
|
||||
// not be used other places.
|
||||
func (v QueryValue) CopyFromMySQLFields() []Field {
|
||||
// fmt.Printf("%#v\n", v)
|
||||
if v.Struct != nil {
|
||||
return v.Struct.Fields
|
||||
}
|
||||
return []Field{
|
||||
{
|
||||
Name: v.Name,
|
||||
DBName: v.DBName,
|
||||
Type: v.Typ,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (v QueryValue) VariableForField(f Field) string {
|
||||
if !v.IsStruct() {
|
||||
return v.Name
|
||||
}
|
||||
if !v.EmitStruct() {
|
||||
return toLowerCase(f.Name)
|
||||
}
|
||||
return v.Name + "." + f.Name
|
||||
}
|
||||
|
||||
// A struct used to generate methods and fields on the Queries struct
|
||||
type Query struct {
|
||||
Cmd string
|
||||
Comments []string
|
||||
MethodName string
|
||||
FieldName string
|
||||
ConstantName string
|
||||
SQL string
|
||||
SourceName string
|
||||
Ret QueryValue
|
||||
Arg QueryValue
|
||||
// Used for :copyfrom
|
||||
Table *plugin.Identifier
|
||||
}
|
||||
|
||||
func (q Query) hasRetType() bool {
|
||||
scanned := q.Cmd == metadata.CmdOne || q.Cmd == metadata.CmdMany ||
|
||||
q.Cmd == metadata.CmdBatchMany || q.Cmd == metadata.CmdBatchOne
|
||||
return scanned && !q.Ret.isEmpty()
|
||||
}
|
||||
|
||||
func (q Query) TableIdentifierAsGoSlice() string {
|
||||
escapedNames := make([]string, 0, 3)
|
||||
for _, p := range []string{q.Table.Catalog, q.Table.Schema, q.Table.Name} {
|
||||
if p != "" {
|
||||
escapedNames = append(escapedNames, fmt.Sprintf("%q", p))
|
||||
}
|
||||
}
|
||||
return "[]string{" + strings.Join(escapedNames, ", ") + "}"
|
||||
}
|
||||
|
||||
func (q Query) TableIdentifierForMySQL() string {
|
||||
escapedNames := make([]string, 0, 3)
|
||||
for _, p := range []string{q.Table.Catalog, q.Table.Schema, q.Table.Name} {
|
||||
if p != "" {
|
||||
escapedNames = append(escapedNames, fmt.Sprintf("`%s`", p))
|
||||
}
|
||||
}
|
||||
return strings.Join(escapedNames, ".")
|
||||
}
|
||||
71
internal/codegen/golang/reserved.go
Normal file
71
internal/codegen/golang/reserved.go
Normal file
@@ -0,0 +1,71 @@
|
||||
package golang
|
||||
|
||||
func escape(s string) string {
|
||||
if IsReserved(s) {
|
||||
return s + "_"
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func IsReserved(s string) bool {
|
||||
switch s {
|
||||
// Not a keyword, but generated code refers to the predeclared type
|
||||
// `any`, so a package-scope identifier with that name would shadow it.
|
||||
case "any":
|
||||
return true
|
||||
case "break":
|
||||
return true
|
||||
case "default":
|
||||
return true
|
||||
case "func":
|
||||
return true
|
||||
case "interface":
|
||||
return true
|
||||
case "select":
|
||||
return true
|
||||
case "case":
|
||||
return true
|
||||
case "defer":
|
||||
return true
|
||||
case "go":
|
||||
return true
|
||||
case "map":
|
||||
return true
|
||||
case "struct":
|
||||
return true
|
||||
case "chan":
|
||||
return true
|
||||
case "else":
|
||||
return true
|
||||
case "goto":
|
||||
return true
|
||||
case "package":
|
||||
return true
|
||||
case "switch":
|
||||
return true
|
||||
case "const":
|
||||
return true
|
||||
case "fallthrough":
|
||||
return true
|
||||
case "if":
|
||||
return true
|
||||
case "range":
|
||||
return true
|
||||
case "type":
|
||||
return true
|
||||
case "continue":
|
||||
return true
|
||||
case "for":
|
||||
return true
|
||||
case "import":
|
||||
return true
|
||||
case "return":
|
||||
return true
|
||||
case "var":
|
||||
return true
|
||||
case "q":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
473
internal/codegen/golang/result.go
Normal file
473
internal/codegen/golang/result.go
Normal file
@@ -0,0 +1,473 @@
|
||||
package golang
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/sqlc-dev/sqlc/internal/codegen/golang/opts"
|
||||
"github.com/sqlc-dev/sqlc/internal/codegen/sdk"
|
||||
"github.com/sqlc-dev/sqlc/internal/inflection"
|
||||
"github.com/sqlc-dev/sqlc/internal/metadata"
|
||||
"github.com/sqlc-dev/sqlc/internal/plugin"
|
||||
)
|
||||
|
||||
func buildEnums(req *plugin.GenerateRequest, options *opts.Options) []Enum {
|
||||
var enums []Enum
|
||||
for _, schema := range req.Catalog.Schemas {
|
||||
if schema.Name == "pg_catalog" || schema.Name == "information_schema" {
|
||||
continue
|
||||
}
|
||||
for _, enum := range schema.Enums {
|
||||
var enumName string
|
||||
if schema.Name == req.Catalog.DefaultSchema {
|
||||
enumName = enum.Name
|
||||
} else {
|
||||
enumName = schema.Name + "_" + enum.Name
|
||||
}
|
||||
|
||||
e := Enum{
|
||||
Name: StructName(enumName, options),
|
||||
Comment: enum.Comment,
|
||||
NameTags: map[string]string{},
|
||||
ValidTags: map[string]string{},
|
||||
}
|
||||
if options.EmitJsonTags {
|
||||
e.NameTags["json"] = JSONTagName(enumName, options)
|
||||
e.ValidTags["json"] = JSONTagName("valid", options)
|
||||
}
|
||||
|
||||
seen := make(map[string]struct{}, len(enum.Vals))
|
||||
for i, v := range enum.Vals {
|
||||
value := EnumReplace(v)
|
||||
if _, found := seen[value]; found || value == "" {
|
||||
value = fmt.Sprintf("value_%d", i)
|
||||
}
|
||||
e.Constants = append(e.Constants, Constant{
|
||||
Name: StructName(enumName+"_"+value, options),
|
||||
Value: v,
|
||||
Type: e.Name,
|
||||
})
|
||||
seen[value] = struct{}{}
|
||||
}
|
||||
enums = append(enums, e)
|
||||
}
|
||||
}
|
||||
if len(enums) > 0 {
|
||||
sort.Slice(enums, func(i, j int) bool { return enums[i].Name < enums[j].Name })
|
||||
}
|
||||
return enums
|
||||
}
|
||||
|
||||
func buildStructs(req *plugin.GenerateRequest, options *opts.Options) []Struct {
|
||||
var structs []Struct
|
||||
for _, schema := range req.Catalog.Schemas {
|
||||
if schema.Name == "pg_catalog" || schema.Name == "information_schema" {
|
||||
continue
|
||||
}
|
||||
for _, table := range schema.Tables {
|
||||
var tableName string
|
||||
if schema.Name == req.Catalog.DefaultSchema {
|
||||
tableName = table.Rel.Name
|
||||
} else {
|
||||
tableName = schema.Name + "_" + table.Rel.Name
|
||||
}
|
||||
structName := tableName
|
||||
if !options.EmitExactTableNames {
|
||||
structName = inflection.Singular(inflection.SingularParams{
|
||||
Name: structName,
|
||||
Exclusions: options.InflectionExcludeTableNames,
|
||||
})
|
||||
}
|
||||
s := Struct{
|
||||
Table: &plugin.Identifier{Schema: schema.Name, Name: table.Rel.Name},
|
||||
Name: StructName(structName, options),
|
||||
Comment: table.Comment,
|
||||
IsModel: true,
|
||||
}
|
||||
for _, column := range table.Columns {
|
||||
tags := map[string]string{}
|
||||
if options.EmitDbTags {
|
||||
tags["db"] = column.Name
|
||||
}
|
||||
if options.EmitJsonTags {
|
||||
tags["json"] = JSONTagName(column.Name, options)
|
||||
}
|
||||
addExtraGoStructTags(tags, req, options, column)
|
||||
s.Fields = append(s.Fields, Field{
|
||||
Name: StructName(column.Name, options),
|
||||
Type: goType(req, options, column),
|
||||
Tags: tags,
|
||||
Comment: column.Comment,
|
||||
})
|
||||
}
|
||||
structs = append(structs, s)
|
||||
}
|
||||
}
|
||||
if len(structs) > 0 {
|
||||
sort.Slice(structs, func(i, j int) bool { return structs[i].Name < structs[j].Name })
|
||||
}
|
||||
return structs
|
||||
}
|
||||
|
||||
type goColumn struct {
|
||||
id int
|
||||
*plugin.Column
|
||||
embed *goEmbed
|
||||
}
|
||||
|
||||
type goEmbed struct {
|
||||
modelType string
|
||||
modelName string
|
||||
fields []Field
|
||||
}
|
||||
|
||||
// look through all the structs and attempt to find a matching one to embed
|
||||
// We need the name of the struct and its field names.
|
||||
func newGoEmbed(embed *plugin.Identifier, structs []Struct, defaultSchema string) *goEmbed {
|
||||
if embed == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, s := range structs {
|
||||
embedSchema := defaultSchema
|
||||
if embed.Schema != "" {
|
||||
embedSchema = embed.Schema
|
||||
}
|
||||
|
||||
// compare the other attributes
|
||||
if embed.Catalog != s.Table.Catalog || embed.Name != s.Table.Name || embedSchema != s.Table.Schema {
|
||||
continue
|
||||
}
|
||||
|
||||
fields := make([]Field, len(s.Fields))
|
||||
copy(fields, s.Fields)
|
||||
|
||||
return &goEmbed{
|
||||
modelType: s.Name,
|
||||
modelName: s.Name,
|
||||
fields: fields,
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func columnName(c *plugin.Column, pos int) string {
|
||||
if c.Name != "" {
|
||||
return c.Name
|
||||
}
|
||||
return fmt.Sprintf("column_%d", pos+1)
|
||||
}
|
||||
|
||||
func paramName(p *plugin.Parameter) string {
|
||||
if p.Column.Name != "" {
|
||||
return argName(p.Column.Name)
|
||||
}
|
||||
return fmt.Sprintf("dollar_%d", p.Number)
|
||||
}
|
||||
|
||||
func argName(name string) string {
|
||||
var out strings.Builder
|
||||
for i, p := range strings.Split(name, "_") {
|
||||
if i == 0 {
|
||||
out.WriteString(strings.ToLower(p))
|
||||
} else if p == "id" {
|
||||
out.WriteString("ID")
|
||||
} else {
|
||||
out.WriteString(strings.Title(p))
|
||||
}
|
||||
}
|
||||
return out.String()
|
||||
}
|
||||
|
||||
func buildQueries(req *plugin.GenerateRequest, options *opts.Options, enums []Enum, structs []Struct) ([]Query, error) {
|
||||
models := buildModelTypeSet(enums, structs)
|
||||
qualifier := options.ModelsTypeQualifier()
|
||||
qs := make([]Query, 0, len(req.Queries))
|
||||
for _, query := range req.Queries {
|
||||
if query.Name == "" {
|
||||
continue
|
||||
}
|
||||
if query.Cmd == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
var constantName string
|
||||
if options.EmitExportedQueries {
|
||||
constantName = sdk.Title(query.Name)
|
||||
} else {
|
||||
constantName = escape(sdk.LowerTitle(query.Name))
|
||||
}
|
||||
|
||||
comments := query.Comments
|
||||
if options.EmitSqlAsComment {
|
||||
if len(comments) == 0 {
|
||||
comments = append(comments, query.Name)
|
||||
}
|
||||
comments = append(comments, " ")
|
||||
scanner := bufio.NewScanner(strings.NewReader(query.Text))
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
comments = append(comments, " "+line)
|
||||
}
|
||||
if err := scanner.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
gq := Query{
|
||||
Cmd: query.Cmd,
|
||||
ConstantName: constantName,
|
||||
FieldName: sdk.LowerTitle(query.Name) + "Stmt",
|
||||
MethodName: query.Name,
|
||||
SourceName: query.Filename,
|
||||
SQL: query.Text,
|
||||
Comments: comments,
|
||||
Table: query.InsertIntoTable,
|
||||
}
|
||||
sqlpkg := parseDriver(options.SqlPackage)
|
||||
|
||||
qpl := int(*options.QueryParameterLimit)
|
||||
|
||||
if len(query.Params) == 1 && qpl != 0 {
|
||||
p := query.Params[0]
|
||||
gq.Arg = QueryValue{
|
||||
Name: escape(paramName(p)),
|
||||
DBName: p.Column.GetName(),
|
||||
Typ: qualifyType(goType(req, options, p.Column), models, qualifier),
|
||||
SQLDriver: sqlpkg,
|
||||
ModelQualifier: qualifier,
|
||||
Column: p.Column,
|
||||
}
|
||||
} else if len(query.Params) >= 1 {
|
||||
var cols []goColumn
|
||||
for _, p := range query.Params {
|
||||
cols = append(cols, goColumn{
|
||||
id: int(p.Number),
|
||||
Column: p.Column,
|
||||
})
|
||||
}
|
||||
s, err := columnsToStruct(req, options, gq.MethodName+"Params", cols, false, models, qualifier)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
gq.Arg = QueryValue{
|
||||
Emit: true,
|
||||
Name: "arg",
|
||||
Struct: s,
|
||||
SQLDriver: sqlpkg,
|
||||
EmitPointer: options.EmitParamsStructPointers,
|
||||
ModelQualifier: qualifier,
|
||||
}
|
||||
|
||||
// if query params is 2, and query params limit is 4 AND this is a copyfrom, we still want to emit the query's model
|
||||
// otherwise we end up with a copyfrom using a struct without the struct definition
|
||||
if len(query.Params) <= qpl && query.Cmd != ":copyfrom" {
|
||||
gq.Arg.Emit = false
|
||||
}
|
||||
}
|
||||
|
||||
if len(query.Columns) == 1 && query.Columns[0].EmbedTable == nil {
|
||||
c := query.Columns[0]
|
||||
name := columnName(c, 0)
|
||||
name = strings.Replace(name, "$", "_", -1)
|
||||
retName := escape(name)
|
||||
// For :one queries the scan destination lives in the same scope as
|
||||
// the query parameters, so reusing a parameter's name would cause
|
||||
// Scan to overwrite the input and leak it back to the caller on
|
||||
// sql.ErrNoRows (see sqlc-dev/sqlc#4354). Rename the return
|
||||
// variable when it would collide.
|
||||
if query.Cmd == metadata.CmdOne {
|
||||
argNames := map[string]struct{}{}
|
||||
for _, p := range gq.Arg.Pairs() {
|
||||
argNames[p.Name] = struct{}{}
|
||||
}
|
||||
for {
|
||||
if _, conflict := argNames[retName]; !conflict {
|
||||
break
|
||||
}
|
||||
retName += "_2"
|
||||
}
|
||||
}
|
||||
gq.Ret = QueryValue{
|
||||
Name: retName,
|
||||
DBName: name,
|
||||
Typ: qualifyType(goType(req, options, c), models, qualifier),
|
||||
SQLDriver: sqlpkg,
|
||||
ModelQualifier: qualifier,
|
||||
}
|
||||
} else if putOutColumns(query) {
|
||||
var gs *Struct
|
||||
var emit bool
|
||||
|
||||
for _, s := range structs {
|
||||
if len(s.Fields) != len(query.Columns) {
|
||||
continue
|
||||
}
|
||||
same := true
|
||||
for i, f := range s.Fields {
|
||||
c := query.Columns[i]
|
||||
sameName := f.Name == StructName(columnName(c, i), options)
|
||||
sameType := f.Type == goType(req, options, c)
|
||||
sameTable := sdk.SameTableName(c.Table, s.Table, req.Catalog.DefaultSchema)
|
||||
if !sameName || !sameType || !sameTable {
|
||||
same = false
|
||||
}
|
||||
}
|
||||
if same {
|
||||
gs = &s
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if gs == nil {
|
||||
var columns []goColumn
|
||||
for i, c := range query.Columns {
|
||||
columns = append(columns, goColumn{
|
||||
id: i,
|
||||
Column: c,
|
||||
embed: newGoEmbed(c.EmbedTable, structs, req.Catalog.DefaultSchema),
|
||||
})
|
||||
}
|
||||
var err error
|
||||
gs, err = columnsToStruct(req, options, gq.MethodName+"Row", columns, true, models, qualifier)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
emit = true
|
||||
}
|
||||
gq.Ret = QueryValue{
|
||||
Emit: emit,
|
||||
Name: "i",
|
||||
Struct: gs,
|
||||
SQLDriver: sqlpkg,
|
||||
EmitPointer: options.EmitResultStructPointers,
|
||||
ModelQualifier: qualifier,
|
||||
}
|
||||
}
|
||||
|
||||
qs = append(qs, gq)
|
||||
}
|
||||
sort.Slice(qs, func(i, j int) bool { return qs[i].MethodName < qs[j].MethodName })
|
||||
return qs, nil
|
||||
}
|
||||
|
||||
var cmdReturnsData = map[string]struct{}{
|
||||
metadata.CmdBatchMany: {},
|
||||
metadata.CmdBatchOne: {},
|
||||
metadata.CmdMany: {},
|
||||
metadata.CmdOne: {},
|
||||
}
|
||||
|
||||
func putOutColumns(query *plugin.Query) bool {
|
||||
_, found := cmdReturnsData[query.Cmd]
|
||||
return found
|
||||
}
|
||||
|
||||
// It's possible that this method will generate duplicate JSON tag values
|
||||
//
|
||||
// Columns: count, count, count_2
|
||||
// Fields: Count, Count_2, Count2
|
||||
//
|
||||
// JSON tags: count, count_2, count_2
|
||||
//
|
||||
// This is unlikely to happen, so don't fix it yet
|
||||
func columnsToStruct(req *plugin.GenerateRequest, options *opts.Options, name string, columns []goColumn, useID bool, models modelTypeSet, qualifier string) (*Struct, error) {
|
||||
gs := Struct{
|
||||
Name: name,
|
||||
}
|
||||
seen := map[string][]int{}
|
||||
suffixes := map[int]int{}
|
||||
for i, c := range columns {
|
||||
colName := columnName(c.Column, i)
|
||||
tagName := colName
|
||||
|
||||
// override col/tag with expected model name
|
||||
if c.embed != nil {
|
||||
colName = c.embed.modelName
|
||||
tagName = SetCaseStyle(colName, "snake")
|
||||
}
|
||||
|
||||
fieldName := StructName(colName, options)
|
||||
baseFieldName := fieldName
|
||||
// Track suffixes by the ID of the column, so that columns referring to the same numbered parameter can be
|
||||
// reused.
|
||||
suffix := 0
|
||||
if o, ok := suffixes[c.id]; ok && useID {
|
||||
suffix = o
|
||||
} else if v := len(seen[fieldName]); v > 0 && !c.IsNamedParam {
|
||||
suffix = v + 1
|
||||
}
|
||||
suffixes[c.id] = suffix
|
||||
if suffix > 0 {
|
||||
tagName = fmt.Sprintf("%s_%d", tagName, suffix)
|
||||
fieldName = fmt.Sprintf("%s_%d", fieldName, suffix)
|
||||
}
|
||||
tags := map[string]string{}
|
||||
if options.EmitDbTags {
|
||||
tags["db"] = tagName
|
||||
}
|
||||
if options.EmitJsonTags {
|
||||
tags["json"] = JSONTagName(tagName, options)
|
||||
}
|
||||
addExtraGoStructTags(tags, req, options, c.Column)
|
||||
f := Field{
|
||||
Name: fieldName,
|
||||
DBName: colName,
|
||||
Tags: tags,
|
||||
Column: c.Column,
|
||||
}
|
||||
if c.embed == nil {
|
||||
f.Type = qualifyType(goType(req, options, c.Column), models, qualifier)
|
||||
} else {
|
||||
f.Type = qualifyType(c.embed.modelType, models, qualifier)
|
||||
f.EmbedFields = c.embed.fields
|
||||
}
|
||||
|
||||
gs.Fields = append(gs.Fields, f)
|
||||
if _, found := seen[baseFieldName]; !found {
|
||||
seen[baseFieldName] = []int{i}
|
||||
} else {
|
||||
seen[baseFieldName] = append(seen[baseFieldName], i)
|
||||
}
|
||||
}
|
||||
|
||||
// If a field does not have a known type, but another
|
||||
// field with the same name has a known type, assign
|
||||
// the known type to the field without a known type
|
||||
for i, field := range gs.Fields {
|
||||
if len(seen[field.Name]) > 1 && field.Type == "any" {
|
||||
for _, j := range seen[field.Name] {
|
||||
if i == j {
|
||||
continue
|
||||
}
|
||||
otherField := gs.Fields[j]
|
||||
if otherField.Type != field.Type {
|
||||
field.Type = otherField.Type
|
||||
}
|
||||
gs.Fields[i] = field
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
err := checkIncompatibleFieldTypes(gs.Fields)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &gs, nil
|
||||
}
|
||||
|
||||
func checkIncompatibleFieldTypes(fields []Field) error {
|
||||
fieldTypes := map[string]string{}
|
||||
for _, field := range fields {
|
||||
if fieldType, found := fieldTypes[field.Name]; !found {
|
||||
fieldTypes[field.Name] = field.Type
|
||||
} else if field.Type != fieldType {
|
||||
return fmt.Errorf("named param %s has incompatible types: %s, %s", field.Name, field.Type, fieldType)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
78
internal/codegen/golang/result_test.go
Normal file
78
internal/codegen/golang/result_test.go
Normal file
@@ -0,0 +1,78 @@
|
||||
package golang
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/sqlc-dev/sqlc/internal/metadata"
|
||||
"github.com/sqlc-dev/sqlc/internal/plugin"
|
||||
)
|
||||
|
||||
func TestPutOutColumns_ForZeroColumns(t *testing.T) {
|
||||
tests := []struct {
|
||||
cmd string
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
cmd: metadata.CmdExec,
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
cmd: metadata.CmdExecResult,
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
cmd: metadata.CmdExecRows,
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
cmd: metadata.CmdExecLastId,
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
cmd: metadata.CmdMany,
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
cmd: metadata.CmdOne,
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
cmd: metadata.CmdCopyFrom,
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
cmd: metadata.CmdBatchExec,
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
cmd: metadata.CmdBatchMany,
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
cmd: metadata.CmdBatchOne,
|
||||
want: true,
|
||||
},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.cmd, func(t *testing.T) {
|
||||
query := &plugin.Query{
|
||||
Cmd: tc.cmd,
|
||||
Columns: []*plugin.Column{},
|
||||
}
|
||||
got := putOutColumns(query)
|
||||
if got != tc.want {
|
||||
t.Errorf("putOutColumns failed. want %v, got %v", tc.want, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPutOutColumns_AlwaysTrueWhenQueryHasColumns(t *testing.T) {
|
||||
query := &plugin.Query{
|
||||
Cmd: metadata.CmdMany,
|
||||
Columns: []*plugin.Column{{}},
|
||||
}
|
||||
if putOutColumns(query) != true {
|
||||
t.Error("should be true when we have columns")
|
||||
}
|
||||
}
|
||||
102
internal/codegen/golang/sqlite_type.go
Normal file
102
internal/codegen/golang/sqlite_type.go
Normal file
@@ -0,0 +1,102 @@
|
||||
package golang
|
||||
|
||||
import (
|
||||
"log"
|
||||
"strings"
|
||||
|
||||
"github.com/sqlc-dev/sqlc/internal/codegen/golang/opts"
|
||||
"github.com/sqlc-dev/sqlc/internal/codegen/sdk"
|
||||
"github.com/sqlc-dev/sqlc/internal/debug"
|
||||
"github.com/sqlc-dev/sqlc/internal/plugin"
|
||||
)
|
||||
|
||||
func sqliteType(req *plugin.GenerateRequest, options *opts.Options, col *plugin.Column) string {
|
||||
dt := strings.ToLower(sdk.DataType(col.Type))
|
||||
notNull := col.NotNull || col.IsArray
|
||||
emitPointersForNull := options.EmitPointersForNullTypes
|
||||
|
||||
switch dt {
|
||||
|
||||
case "int", "integer", "tinyint", "smallint", "mediumint", "bigint", "unsignedbigint", "int2", "int8":
|
||||
if notNull {
|
||||
return "int64"
|
||||
}
|
||||
if emitPointersForNull {
|
||||
return "*int64"
|
||||
}
|
||||
return "sql.NullInt64"
|
||||
|
||||
case "blob":
|
||||
return "[]byte"
|
||||
|
||||
case "real", "double", "doubleprecision", "float":
|
||||
if notNull {
|
||||
return "float64"
|
||||
}
|
||||
if emitPointersForNull {
|
||||
return "*float64"
|
||||
}
|
||||
return "sql.NullFloat64"
|
||||
|
||||
case "boolean", "bool":
|
||||
if notNull {
|
||||
return "bool"
|
||||
}
|
||||
if emitPointersForNull {
|
||||
return "*bool"
|
||||
}
|
||||
return "sql.NullBool"
|
||||
|
||||
case "date", "datetime", "timestamp":
|
||||
if notNull {
|
||||
return "time.Time"
|
||||
}
|
||||
if emitPointersForNull {
|
||||
return "*time.Time"
|
||||
}
|
||||
return "sql.NullTime"
|
||||
|
||||
case "json", "jsonb":
|
||||
return "json.RawMessage"
|
||||
|
||||
case "any":
|
||||
return "any"
|
||||
|
||||
}
|
||||
|
||||
switch {
|
||||
|
||||
case strings.HasPrefix(dt, "character"),
|
||||
strings.HasPrefix(dt, "varchar"),
|
||||
strings.HasPrefix(dt, "varyingcharacter"),
|
||||
strings.HasPrefix(dt, "nchar"),
|
||||
strings.HasPrefix(dt, "nativecharacter"),
|
||||
strings.HasPrefix(dt, "nvarchar"),
|
||||
dt == "text",
|
||||
dt == "clob":
|
||||
if notNull {
|
||||
return "string"
|
||||
}
|
||||
if emitPointersForNull {
|
||||
return "*string"
|
||||
}
|
||||
return "sql.NullString"
|
||||
|
||||
case strings.HasPrefix(dt, "decimal"), dt == "numeric":
|
||||
if notNull {
|
||||
return "float64"
|
||||
}
|
||||
if emitPointersForNull {
|
||||
return "*float64"
|
||||
}
|
||||
return "sql.NullFloat64"
|
||||
|
||||
default:
|
||||
if debug.Active {
|
||||
log.Printf("unknown SQLite type: %s\n", dt)
|
||||
}
|
||||
|
||||
return "any"
|
||||
|
||||
}
|
||||
}
|
||||
53
internal/codegen/golang/struct.go
Normal file
53
internal/codegen/golang/struct.go
Normal file
@@ -0,0 +1,53 @@
|
||||
package golang
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/sqlc-dev/sqlc/internal/codegen/golang/opts"
|
||||
"github.com/sqlc-dev/sqlc/internal/plugin"
|
||||
)
|
||||
|
||||
type Struct struct {
|
||||
Table *plugin.Identifier
|
||||
Name string
|
||||
Fields []Field
|
||||
Comment string
|
||||
// IsModel is true for table structs that live in the models file. When
|
||||
// the models file is generated into a different Go package, references
|
||||
// to these types from query files must be qualified.
|
||||
IsModel bool
|
||||
}
|
||||
|
||||
func StructName(name string, options *opts.Options) string {
|
||||
if rename := options.Rename[name]; rename != "" {
|
||||
return rename
|
||||
}
|
||||
var out strings.Builder
|
||||
name = strings.Map(func(r rune) rune {
|
||||
if unicode.IsLetter(r) {
|
||||
return r
|
||||
}
|
||||
if unicode.IsDigit(r) {
|
||||
return r
|
||||
}
|
||||
return rune('_')
|
||||
}, name)
|
||||
|
||||
for p := range strings.SplitSeq(name, "_") {
|
||||
if _, found := options.InitialismsMap[p]; found {
|
||||
out.WriteString(strings.ToUpper(p))
|
||||
} else {
|
||||
out.WriteString(strings.Title(p))
|
||||
}
|
||||
}
|
||||
|
||||
// If a name has a digit as its first char, prepand an underscore to make it a valid Go name.
|
||||
r, _ := utf8.DecodeRuneInString(out.String())
|
||||
if unicode.IsDigit(r) {
|
||||
return "_" + out.String()
|
||||
} else {
|
||||
return out.String()
|
||||
}
|
||||
}
|
||||
7
internal/codegen/golang/template.go
Normal file
7
internal/codegen/golang/template.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package golang
|
||||
|
||||
import "embed"
|
||||
|
||||
//go:embed templates/*
|
||||
//go:embed templates/*/*
|
||||
var templates embed.FS
|
||||
@@ -0,0 +1,52 @@
|
||||
{{define "copyfromCodeGoSqlDriver"}}
|
||||
{{range .GoQueries}}
|
||||
{{if eq .Cmd ":copyfrom" }}
|
||||
var readerHandlerSequenceFor{{.MethodName}} uint32 = 1
|
||||
|
||||
func convertRowsFor{{.MethodName}}(w *io.PipeWriter, {{.Arg.SlicePair}}) {
|
||||
e := mysqltsv.NewEncoder(w, {{ len .Arg.CopyFromMySQLFields }}, nil)
|
||||
for _, row := range {{.Arg.Name}} {
|
||||
{{- with $arg := .Arg }}
|
||||
{{- range $arg.CopyFromMySQLFields}}
|
||||
{{- if eq .Type "string"}}
|
||||
e.AppendString({{if $arg.Struct}}row.{{.Name}}{{else}}row{{end}})
|
||||
{{- else if or (eq .Type "[]byte") (eq .Type "json.RawMessage")}}
|
||||
e.AppendBytes({{if $arg.Struct}}row.{{.Name}}{{else}}row{{end}})
|
||||
{{- else}}
|
||||
e.AppendValue({{if $arg.Struct}}row.{{.Name}}{{else}}row{{end}})
|
||||
{{- end}}
|
||||
{{- end}}
|
||||
{{- end}}
|
||||
}
|
||||
w.CloseWithError(e.Close())
|
||||
}
|
||||
|
||||
{{range .Comments}}//{{.}}
|
||||
{{end -}}
|
||||
// {{.MethodName}} uses MySQL's LOAD DATA LOCAL INFILE and is not atomic.
|
||||
//
|
||||
// Errors and duplicate keys are treated as warnings and insertion will
|
||||
// continue, even without an error for some cases. Use this in a transaction
|
||||
// and use SHOW WARNINGS to check for any problems and roll back if you want to.
|
||||
//
|
||||
// Check the documentation for more information:
|
||||
// https://dev.mysql.com/doc/refman/8.0/en/load-data.html#load-data-error-handling
|
||||
func (q *Queries) {{.MethodName}}(ctx context.Context{{if $.EmitMethodsWithDBArgument}}, db DBTX{{end}}, {{.Arg.SlicePair}}) (int64, error) {
|
||||
pr, pw := io.Pipe()
|
||||
defer pr.Close()
|
||||
rh := fmt.Sprintf("{{.MethodName}}_%d", atomic.AddUint32(&readerHandlerSequenceFor{{.MethodName}}, 1))
|
||||
mysql.RegisterReaderHandler(rh, func() io.Reader { return pr })
|
||||
defer mysql.DeregisterReaderHandler(rh)
|
||||
go convertRowsFor{{.MethodName}}(pw, {{.Arg.Name}})
|
||||
// The string interpolation is necessary because LOAD DATA INFILE requires
|
||||
// the file name to be given as a literal string.
|
||||
result, err := {{if (not $.EmitMethodsWithDBArgument)}}q.{{end}}db.ExecContext(ctx, fmt.Sprintf("LOAD DATA LOCAL INFILE '%s' INTO TABLE {{.TableIdentifierForMySQL}} %s ({{range $index, $name := .Arg.ColumnNames}}{{if gt $index 0}}, {{end}}{{$name}}{{end}})", "Reader::" + rh, mysqltsv.Escaping))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return result.RowsAffected()
|
||||
}
|
||||
|
||||
{{end}}
|
||||
{{end}}
|
||||
{{end}}
|
||||
134
internal/codegen/golang/templates/pgx/batchCode.tmpl
Normal file
134
internal/codegen/golang/templates/pgx/batchCode.tmpl
Normal file
@@ -0,0 +1,134 @@
|
||||
{{define "batchCodePgx"}}
|
||||
|
||||
var (
|
||||
ErrBatchAlreadyClosed = errors.New("batch already closed")
|
||||
)
|
||||
|
||||
{{range .GoQueries}}
|
||||
{{if eq (hasPrefix .Cmd ":batch") true }}
|
||||
const {{.ConstantName}} = {{$.Q}}-- name: {{.MethodName}} {{.Cmd}}
|
||||
{{escape .SQL}}
|
||||
{{$.Q}}
|
||||
|
||||
type {{.MethodName}}BatchResults struct {
|
||||
br pgx.BatchResults
|
||||
tot int
|
||||
closed bool
|
||||
}
|
||||
|
||||
{{if .Arg.Struct}}
|
||||
type {{.Arg.Type}} struct { {{- range .Arg.Struct.Fields}}
|
||||
{{.Name}} {{.Type}} {{if .Tag}}{{$.Q}}{{.Tag}}{{$.Q}}{{end}}
|
||||
{{- end}}
|
||||
}
|
||||
{{end}}
|
||||
|
||||
{{if .Ret.EmitStruct}}
|
||||
type {{.Ret.Type}} struct { {{- range .Ret.Struct.Fields}}
|
||||
{{.Name}} {{.Type}} {{if .Tag}}{{$.Q}}{{.Tag}}{{$.Q}}{{end}}
|
||||
{{- end}}
|
||||
}
|
||||
{{end}}
|
||||
|
||||
{{range .Comments}}//{{.}}
|
||||
{{end -}}
|
||||
func (q *Queries) {{.MethodName}}(ctx context.Context, {{ if $.EmitMethodsWithDBArgument}}db DBTX,{{end}} {{.Arg.SlicePair}}) *{{.MethodName}}BatchResults {
|
||||
batch := &pgx.Batch{}
|
||||
for _, a := range {{index .Arg.Name}} {
|
||||
vals := []any{
|
||||
{{- if .Arg.Struct }}
|
||||
{{- range .Arg.Struct.Fields }}
|
||||
a.{{.Name}},
|
||||
{{- end }}
|
||||
{{- else }}
|
||||
a,
|
||||
{{- end }}
|
||||
}
|
||||
batch.Queue({{.ConstantName}}, vals...)
|
||||
}
|
||||
br := {{if not $.EmitMethodsWithDBArgument}}q.{{end}}db.SendBatch(ctx, batch)
|
||||
return &{{.MethodName}}BatchResults{br,len({{.Arg.Name}}),false}
|
||||
}
|
||||
|
||||
{{if eq .Cmd ":batchexec"}}
|
||||
func (b *{{.MethodName}}BatchResults) Exec(f func(int, error)) {
|
||||
defer b.br.Close()
|
||||
for t := 0; t < b.tot; t++ {
|
||||
if b.closed {
|
||||
if f != nil {
|
||||
f(t, ErrBatchAlreadyClosed)
|
||||
}
|
||||
continue
|
||||
}
|
||||
_, err := b.br.Exec()
|
||||
if f != nil {
|
||||
f(t, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
{{end}}
|
||||
|
||||
{{if eq .Cmd ":batchmany"}}
|
||||
func (b *{{.MethodName}}BatchResults) Query(f func(int, []{{.Ret.DefineType}}, error)) {
|
||||
defer b.br.Close()
|
||||
for t := 0; t < b.tot; t++ {
|
||||
{{- if $.EmitEmptySlices}}
|
||||
items := []{{.Ret.DefineType}}{}
|
||||
{{else}}
|
||||
var items []{{.Ret.DefineType}}
|
||||
{{end -}}
|
||||
if b.closed {
|
||||
if f != nil {
|
||||
f(t, items, ErrBatchAlreadyClosed)
|
||||
}
|
||||
continue
|
||||
}
|
||||
err := func() error {
|
||||
rows, err := b.br.Query()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var {{.Ret.Name}} {{.Ret.Type}}
|
||||
if err := rows.Scan({{.Ret.Scan}}); err != nil {
|
||||
return err
|
||||
}
|
||||
items = append(items, {{.Ret.ReturnName}})
|
||||
}
|
||||
return rows.Err()
|
||||
}()
|
||||
if f != nil {
|
||||
f(t, items, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
{{end}}
|
||||
|
||||
{{if eq .Cmd ":batchone"}}
|
||||
func (b *{{.MethodName}}BatchResults) QueryRow(f func(int, {{.Ret.DefineType}}, error)) {
|
||||
defer b.br.Close()
|
||||
for t := 0; t < b.tot; t++ {
|
||||
var {{.Ret.Name}} {{.Ret.Type}}
|
||||
if b.closed {
|
||||
if f != nil {
|
||||
f(t, {{if .Ret.IsPointer}}nil{{else}}{{.Ret.Name}}{{end}}, ErrBatchAlreadyClosed)
|
||||
}
|
||||
continue
|
||||
}
|
||||
row := b.br.QueryRow()
|
||||
err := row.Scan({{.Ret.Scan}})
|
||||
if f != nil {
|
||||
f(t, {{.Ret.ReturnName}}, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
{{end}}
|
||||
|
||||
func (b *{{.MethodName}}BatchResults) Close() error {
|
||||
b.closed = true
|
||||
return b.br.Close()
|
||||
}
|
||||
{{end}}
|
||||
{{end}}
|
||||
{{end}}
|
||||
51
internal/codegen/golang/templates/pgx/copyfromCopy.tmpl
Normal file
51
internal/codegen/golang/templates/pgx/copyfromCopy.tmpl
Normal file
@@ -0,0 +1,51 @@
|
||||
{{define "copyfromCodePgx"}}
|
||||
{{range .GoQueries}}
|
||||
{{if eq .Cmd ":copyfrom" }}
|
||||
// iteratorFor{{.MethodName}} implements pgx.CopyFromSource.
|
||||
type iteratorFor{{.MethodName}} struct {
|
||||
rows []{{.Arg.DefineType}}
|
||||
skippedFirstNextCall bool
|
||||
}
|
||||
|
||||
func (r *iteratorFor{{.MethodName}}) Next() bool {
|
||||
if len(r.rows) == 0 {
|
||||
return false
|
||||
}
|
||||
if !r.skippedFirstNextCall {
|
||||
r.skippedFirstNextCall = true
|
||||
return true
|
||||
}
|
||||
r.rows = r.rows[1:]
|
||||
return len(r.rows) > 0
|
||||
}
|
||||
|
||||
func (r iteratorFor{{.MethodName}}) Values() ([]any, error) {
|
||||
return []any{
|
||||
{{- if .Arg.Struct }}
|
||||
{{- range .Arg.Struct.Fields }}
|
||||
r.rows[0].{{.Name}},
|
||||
{{- end }}
|
||||
{{- else }}
|
||||
r.rows[0],
|
||||
{{- end }}
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r iteratorFor{{.MethodName}}) Err() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
{{range .Comments}}//{{.}}
|
||||
{{end -}}
|
||||
{{- if $.EmitMethodsWithDBArgument -}}
|
||||
func (q *Queries) {{.MethodName}}(ctx context.Context, db DBTX, {{.Arg.SlicePair}}) (int64, error) {
|
||||
return db.CopyFrom(ctx, {{.TableIdentifierAsGoSlice}}, {{.Arg.ColumnNamesAsGoSlice}}, &iteratorFor{{.MethodName}}{rows: {{.Arg.Name}}})
|
||||
{{- else -}}
|
||||
func (q *Queries) {{.MethodName}}(ctx context.Context, {{.Arg.SlicePair}}) (int64, error) {
|
||||
return q.db.CopyFrom(ctx, {{.TableIdentifierAsGoSlice}}, {{.Arg.ColumnNamesAsGoSlice}}, &iteratorFor{{.MethodName}}{rows: {{.Arg.Name}}})
|
||||
{{- end}}
|
||||
}
|
||||
|
||||
{{end}}
|
||||
{{end}}
|
||||
{{end}}
|
||||
37
internal/codegen/golang/templates/pgx/dbCode.tmpl
Normal file
37
internal/codegen/golang/templates/pgx/dbCode.tmpl
Normal file
@@ -0,0 +1,37 @@
|
||||
{{define "dbCodeTemplatePgx"}}
|
||||
|
||||
type DBTX interface {
|
||||
Exec(context.Context, string, ...any) (pgconn.CommandTag, error)
|
||||
Query(context.Context, string, ...any) (pgx.Rows, error)
|
||||
QueryRow(context.Context, string, ...any) pgx.Row
|
||||
{{- if .UsesCopyFrom }}
|
||||
CopyFrom(ctx context.Context, tableName pgx.Identifier, columnNames []string, rowSrc pgx.CopyFromSource) (int64, error)
|
||||
{{- end }}
|
||||
{{- if .UsesBatch }}
|
||||
SendBatch(context.Context, *pgx.Batch) pgx.BatchResults
|
||||
{{- end }}
|
||||
}
|
||||
|
||||
{{ if .EmitMethodsWithDBArgument}}
|
||||
func New() *Queries {
|
||||
return &Queries{}
|
||||
{{- else -}}
|
||||
func New(db DBTX) *Queries {
|
||||
return &Queries{db: db}
|
||||
{{- end}}
|
||||
}
|
||||
|
||||
type Queries struct {
|
||||
{{if not .EmitMethodsWithDBArgument}}
|
||||
db DBTX
|
||||
{{end}}
|
||||
}
|
||||
|
||||
{{if not .EmitMethodsWithDBArgument}}
|
||||
func (q *Queries) WithTx(tx pgx.Tx) *Queries {
|
||||
return &Queries{
|
||||
db: tx,
|
||||
}
|
||||
}
|
||||
{{end}}
|
||||
{{end}}
|
||||
73
internal/codegen/golang/templates/pgx/interfaceCode.tmpl
Normal file
73
internal/codegen/golang/templates/pgx/interfaceCode.tmpl
Normal file
@@ -0,0 +1,73 @@
|
||||
{{define "interfaceCodePgx"}}
|
||||
type Querier interface {
|
||||
{{- $dbtxParam := .EmitMethodsWithDBArgument -}}
|
||||
{{- range .GoQueries}}
|
||||
{{- if and (eq .Cmd ":one") ($dbtxParam) }}
|
||||
{{range .Comments}}//{{.}}
|
||||
{{end -}}
|
||||
{{.MethodName}}(ctx context.Context, db DBTX, {{.Arg.Pair}}) ({{.Ret.DefineType}}, error)
|
||||
{{- else if eq .Cmd ":one" }}
|
||||
{{range .Comments}}//{{.}}
|
||||
{{end -}}
|
||||
{{.MethodName}}(ctx context.Context, {{.Arg.Pair}}) ({{.Ret.DefineType}}, error)
|
||||
{{- end}}
|
||||
{{- if and (eq .Cmd ":many") ($dbtxParam) }}
|
||||
{{range .Comments}}//{{.}}
|
||||
{{end -}}
|
||||
{{.MethodName}}(ctx context.Context, db DBTX, {{.Arg.Pair}}) ([]{{.Ret.DefineType}}, error)
|
||||
{{- else if eq .Cmd ":many" }}
|
||||
{{range .Comments}}//{{.}}
|
||||
{{end -}}
|
||||
{{.MethodName}}(ctx context.Context, {{.Arg.Pair}}) ([]{{.Ret.DefineType}}, error)
|
||||
{{- end}}
|
||||
{{- if and (eq .Cmd ":exec") ($dbtxParam) }}
|
||||
{{range .Comments}}//{{.}}
|
||||
{{end -}}
|
||||
{{.MethodName}}(ctx context.Context, db DBTX, {{.Arg.Pair}}) error
|
||||
{{- else if eq .Cmd ":exec" }}
|
||||
{{range .Comments}}//{{.}}
|
||||
{{end -}}
|
||||
{{.MethodName}}(ctx context.Context, {{.Arg.Pair}}) error
|
||||
{{- end}}
|
||||
{{- if and (eq .Cmd ":execrows") ($dbtxParam) }}
|
||||
{{range .Comments}}//{{.}}
|
||||
{{end -}}
|
||||
{{.MethodName}}(ctx context.Context, db DBTX, {{.Arg.Pair}}) (int64, error)
|
||||
{{- else if eq .Cmd ":execrows" }}
|
||||
{{range .Comments}}//{{.}}
|
||||
{{end -}}
|
||||
{{.MethodName}}(ctx context.Context, {{.Arg.Pair}}) (int64, error)
|
||||
{{- end}}
|
||||
{{- if and (eq .Cmd ":execresult") ($dbtxParam) }}
|
||||
{{range .Comments}}//{{.}}
|
||||
{{end -}}
|
||||
{{.MethodName}}(ctx context.Context, db DBTX, {{.Arg.Pair}}) (pgconn.CommandTag, error)
|
||||
{{- else if eq .Cmd ":execresult" }}
|
||||
{{range .Comments}}//{{.}}
|
||||
{{end -}}
|
||||
{{.MethodName}}(ctx context.Context, {{.Arg.Pair}}) (pgconn.CommandTag, error)
|
||||
{{- end}}
|
||||
{{- if and (eq .Cmd ":copyfrom") ($dbtxParam) }}
|
||||
{{range .Comments}}//{{.}}
|
||||
{{end -}}
|
||||
{{.MethodName}}(ctx context.Context, db DBTX, {{.Arg.SlicePair}}) (int64, error)
|
||||
{{- else if eq .Cmd ":copyfrom" }}
|
||||
{{range .Comments}}//{{.}}
|
||||
{{end -}}
|
||||
{{.MethodName}}(ctx context.Context, {{.Arg.SlicePair}}) (int64, error)
|
||||
{{- end}}
|
||||
{{- if and (or (eq .Cmd ":batchexec") (eq .Cmd ":batchmany") (eq .Cmd ":batchone")) ($dbtxParam) }}
|
||||
{{range .Comments}}//{{.}}
|
||||
{{end -}}
|
||||
{{.MethodName}}(ctx context.Context, db DBTX, {{.Arg.SlicePair}}) *{{.MethodName}}BatchResults
|
||||
{{- else if or (eq .Cmd ":batchexec") (eq .Cmd ":batchmany") (eq .Cmd ":batchone") }}
|
||||
{{range .Comments}}//{{.}}
|
||||
{{end -}}
|
||||
{{.MethodName}}(ctx context.Context, {{.Arg.SlicePair}}) *{{.MethodName}}BatchResults
|
||||
{{- end}}
|
||||
|
||||
{{- end}}
|
||||
}
|
||||
|
||||
var _ Querier = (*Queries)(nil)
|
||||
{{end}}
|
||||
142
internal/codegen/golang/templates/pgx/queryCode.tmpl
Normal file
142
internal/codegen/golang/templates/pgx/queryCode.tmpl
Normal file
@@ -0,0 +1,142 @@
|
||||
{{define "queryCodePgx"}}
|
||||
{{range .GoQueries}}
|
||||
{{if $.OutputQuery .SourceName}}
|
||||
{{if and (ne .Cmd ":copyfrom") (ne (hasPrefix .Cmd ":batch") true)}}
|
||||
const {{.ConstantName}} = {{$.Q}}-- name: {{.MethodName}} {{.Cmd}}
|
||||
{{escape .SQL}}
|
||||
{{$.Q}}
|
||||
{{end}}
|
||||
|
||||
{{if ne (hasPrefix .Cmd ":batch") true}}
|
||||
{{if .Arg.EmitStruct}}
|
||||
type {{.Arg.Type}} struct { {{- range .Arg.Struct.Fields}}
|
||||
{{.Name}} {{.Type}} {{if .Tag}}{{$.Q}}{{.Tag}}{{$.Q}}{{end}}
|
||||
{{- end}}
|
||||
}
|
||||
{{end}}
|
||||
|
||||
{{if .Ret.EmitStruct}}
|
||||
type {{.Ret.Type}} struct { {{- range .Ret.Struct.Fields}}
|
||||
{{.Name}} {{.Type}} {{if .Tag}}{{$.Q}}{{.Tag}}{{$.Q}}{{end}}
|
||||
{{- end}}
|
||||
}
|
||||
{{end}}
|
||||
{{end}}
|
||||
|
||||
{{if eq .Cmd ":one"}}
|
||||
{{range .Comments}}//{{.}}
|
||||
{{end -}}
|
||||
{{- if $.EmitMethodsWithDBArgument -}}
|
||||
func (q *Queries) {{.MethodName}}(ctx context.Context, db DBTX, {{.Arg.Pair}}) ({{.Ret.DefineType}}, error) {
|
||||
row := db.QueryRow(ctx, {{.ConstantName}}, {{.Arg.Params}})
|
||||
{{- else -}}
|
||||
func (q *Queries) {{.MethodName}}(ctx context.Context, {{.Arg.Pair}}) ({{.Ret.DefineType}}, error) {
|
||||
row := q.db.QueryRow(ctx, {{.ConstantName}}, {{.Arg.Params}})
|
||||
{{- end}}
|
||||
{{- if or (ne .Arg.Pair .Ret.Pair) (ne .Arg.DefineType .Ret.DefineType) }}
|
||||
var {{.Ret.Name}} {{.Ret.Type}}
|
||||
{{- end}}
|
||||
err := row.Scan({{.Ret.Scan}})
|
||||
{{- if $.WrapErrors}}
|
||||
if err != nil {
|
||||
err = fmt.Errorf("query {{.MethodName}}: %w", err)
|
||||
}
|
||||
{{- end}}
|
||||
return {{.Ret.ReturnName}}, err
|
||||
}
|
||||
{{end}}
|
||||
|
||||
{{if eq .Cmd ":many"}}
|
||||
{{range .Comments}}//{{.}}
|
||||
{{end -}}
|
||||
{{- if $.EmitMethodsWithDBArgument -}}
|
||||
func (q *Queries) {{.MethodName}}(ctx context.Context, db DBTX, {{.Arg.Pair}}) ([]{{.Ret.DefineType}}, error) {
|
||||
rows, err := db.Query(ctx, {{.ConstantName}}, {{.Arg.Params}})
|
||||
{{- else -}}
|
||||
func (q *Queries) {{.MethodName}}(ctx context.Context, {{.Arg.Pair}}) ([]{{.Ret.DefineType}}, error) {
|
||||
rows, err := q.db.Query(ctx, {{.ConstantName}}, {{.Arg.Params}})
|
||||
{{- end}}
|
||||
if err != nil {
|
||||
return nil, {{if $.WrapErrors}}fmt.Errorf("query {{.MethodName}}: %w", err){{else}}err{{end}}
|
||||
}
|
||||
defer rows.Close()
|
||||
{{- if $.EmitEmptySlices}}
|
||||
items := []{{.Ret.DefineType}}{}
|
||||
{{else}}
|
||||
var items []{{.Ret.DefineType}}
|
||||
{{end -}}
|
||||
for rows.Next() {
|
||||
var {{.Ret.Name}} {{.Ret.Type}}
|
||||
if err := rows.Scan({{.Ret.Scan}}); err != nil {
|
||||
return nil, {{if $.WrapErrors}}fmt.Errorf("query {{.MethodName}}: %w", err){{else}}err{{end}}
|
||||
}
|
||||
items = append(items, {{.Ret.ReturnName}})
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, {{if $.WrapErrors}}fmt.Errorf("query {{.MethodName}}: %w", err){{else}}err{{end}}
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
{{end}}
|
||||
|
||||
{{if eq .Cmd ":exec"}}
|
||||
{{range .Comments}}//{{.}}
|
||||
{{end -}}
|
||||
{{- if $.EmitMethodsWithDBArgument -}}
|
||||
func (q *Queries) {{.MethodName}}(ctx context.Context, db DBTX, {{.Arg.Pair}}) error {
|
||||
_, err := db.Exec(ctx, {{.ConstantName}}, {{.Arg.Params}})
|
||||
{{- else -}}
|
||||
func (q *Queries) {{.MethodName}}(ctx context.Context, {{.Arg.Pair}}) error {
|
||||
_, err := q.db.Exec(ctx, {{.ConstantName}}, {{.Arg.Params}})
|
||||
{{- end}}
|
||||
{{- if $.WrapErrors }}
|
||||
if err != nil {
|
||||
return fmt.Errorf("query {{.MethodName}}: %w", err)
|
||||
}
|
||||
return nil
|
||||
{{- else }}
|
||||
return err
|
||||
{{- end }}
|
||||
}
|
||||
{{end}}
|
||||
|
||||
{{if eq .Cmd ":execrows"}}
|
||||
{{range .Comments}}//{{.}}
|
||||
{{end -}}
|
||||
{{if $.EmitMethodsWithDBArgument -}}
|
||||
func (q *Queries) {{.MethodName}}(ctx context.Context, db DBTX, {{.Arg.Pair}}) (int64, error) {
|
||||
result, err := db.Exec(ctx, {{.ConstantName}}, {{.Arg.Params}})
|
||||
{{- else -}}
|
||||
func (q *Queries) {{.MethodName}}(ctx context.Context, {{.Arg.Pair}}) (int64, error) {
|
||||
result, err := q.db.Exec(ctx, {{.ConstantName}}, {{.Arg.Params}})
|
||||
{{- end}}
|
||||
if err != nil {
|
||||
return 0, {{if $.WrapErrors}}fmt.Errorf("query {{.MethodName}}: %w", err){{else}}err{{end}}
|
||||
}
|
||||
return result.RowsAffected(), nil
|
||||
}
|
||||
{{end}}
|
||||
|
||||
{{if eq .Cmd ":execresult"}}
|
||||
{{range .Comments}}//{{.}}
|
||||
{{end -}}
|
||||
{{- if $.EmitMethodsWithDBArgument -}}
|
||||
func (q *Queries) {{.MethodName}}(ctx context.Context, db DBTX, {{.Arg.Pair}}) (pgconn.CommandTag, error) {
|
||||
{{queryRetval .}} db.Exec(ctx, {{.ConstantName}}, {{.Arg.Params}})
|
||||
{{- else -}}
|
||||
func (q *Queries) {{.MethodName}}(ctx context.Context, {{.Arg.Pair}}) (pgconn.CommandTag, error) {
|
||||
{{queryRetval .}} q.db.Exec(ctx, {{.ConstantName}}, {{.Arg.Params}})
|
||||
{{- end}}
|
||||
{{- if $.WrapErrors}}
|
||||
if err != nil {
|
||||
err = fmt.Errorf("query {{.MethodName}}: %w", err)
|
||||
}
|
||||
return result, err
|
||||
{{- end}}
|
||||
}
|
||||
{{end}}
|
||||
|
||||
|
||||
{{end}}
|
||||
{{end}}
|
||||
{{end}}
|
||||
105
internal/codegen/golang/templates/stdlib/dbCode.tmpl
Normal file
105
internal/codegen/golang/templates/stdlib/dbCode.tmpl
Normal file
@@ -0,0 +1,105 @@
|
||||
{{define "dbCodeTemplateStd"}}
|
||||
type DBTX interface {
|
||||
ExecContext(context.Context, string, ...any) (sql.Result, error)
|
||||
PrepareContext(context.Context, string) (*sql.Stmt, error)
|
||||
QueryContext(context.Context, string, ...any) (*sql.Rows, error)
|
||||
QueryRowContext(context.Context, string, ...any) *sql.Row
|
||||
}
|
||||
|
||||
{{ if .EmitMethodsWithDBArgument}}
|
||||
func New() *Queries {
|
||||
return &Queries{}
|
||||
{{- else -}}
|
||||
func New(db DBTX) *Queries {
|
||||
return &Queries{db: db}
|
||||
{{- end}}
|
||||
}
|
||||
|
||||
{{if .EmitPreparedQueries}}
|
||||
func Prepare(ctx context.Context, db DBTX) (*Queries, error) {
|
||||
q := Queries{db: db}
|
||||
var err error
|
||||
{{- if eq (len .GoQueries) 0 }}
|
||||
_ = err
|
||||
{{- end }}
|
||||
{{- range .GoQueries }}
|
||||
if q.{{.FieldName}}, err = db.PrepareContext(ctx, {{.ConstantName}}); err != nil {
|
||||
return nil, fmt.Errorf("error preparing query {{.MethodName}}: %w", err)
|
||||
}
|
||||
{{- end}}
|
||||
return &q, nil
|
||||
}
|
||||
|
||||
func (q *Queries) Close() error {
|
||||
var err error
|
||||
{{- range .GoQueries }}
|
||||
if q.{{.FieldName}} != nil {
|
||||
if cerr := q.{{.FieldName}}.Close(); cerr != nil {
|
||||
err = fmt.Errorf("error closing {{.FieldName}}: %w", cerr)
|
||||
}
|
||||
}
|
||||
{{- end}}
|
||||
return err
|
||||
}
|
||||
|
||||
func (q *Queries) exec(ctx context.Context, stmt *sql.Stmt, query string, args ...any) (sql.Result, error) {
|
||||
switch {
|
||||
case stmt != nil && q.tx != nil:
|
||||
return q.tx.StmtContext(ctx, stmt).ExecContext(ctx, args...)
|
||||
case stmt != nil:
|
||||
return stmt.ExecContext(ctx, args...)
|
||||
default:
|
||||
return q.db.ExecContext(ctx, query, args...)
|
||||
}
|
||||
}
|
||||
|
||||
func (q *Queries) query(ctx context.Context, stmt *sql.Stmt, query string, args ...any) (*sql.Rows, error) {
|
||||
switch {
|
||||
case stmt != nil && q.tx != nil:
|
||||
return q.tx.StmtContext(ctx, stmt).QueryContext(ctx, args...)
|
||||
case stmt != nil:
|
||||
return stmt.QueryContext(ctx, args...)
|
||||
default:
|
||||
return q.db.QueryContext(ctx, query, args...)
|
||||
}
|
||||
}
|
||||
|
||||
func (q *Queries) queryRow(ctx context.Context, stmt *sql.Stmt, query string, args ...any) (*sql.Row) {
|
||||
switch {
|
||||
case stmt != nil && q.tx != nil:
|
||||
return q.tx.StmtContext(ctx, stmt).QueryRowContext(ctx, args...)
|
||||
case stmt != nil:
|
||||
return stmt.QueryRowContext(ctx, args...)
|
||||
default:
|
||||
return q.db.QueryRowContext(ctx, query, args...)
|
||||
}
|
||||
}
|
||||
{{end}}
|
||||
|
||||
type Queries struct {
|
||||
{{- if not .EmitMethodsWithDBArgument}}
|
||||
db DBTX
|
||||
{{- end}}
|
||||
|
||||
{{- if .EmitPreparedQueries}}
|
||||
tx *sql.Tx
|
||||
{{- range .GoQueries}}
|
||||
{{.FieldName}} *sql.Stmt
|
||||
{{- end}}
|
||||
{{- end}}
|
||||
}
|
||||
|
||||
{{if not .EmitMethodsWithDBArgument}}
|
||||
func (q *Queries) WithTx(tx *sql.Tx) *Queries {
|
||||
return &Queries{
|
||||
db: tx,
|
||||
{{- if .EmitPreparedQueries}}
|
||||
tx: tx,
|
||||
{{- range .GoQueries}}
|
||||
{{.FieldName}}: q.{{.FieldName}},
|
||||
{{- end}}
|
||||
{{- end}}
|
||||
}
|
||||
}
|
||||
{{end}}
|
||||
{{end}}
|
||||
63
internal/codegen/golang/templates/stdlib/interfaceCode.tmpl
Normal file
63
internal/codegen/golang/templates/stdlib/interfaceCode.tmpl
Normal file
@@ -0,0 +1,63 @@
|
||||
{{define "interfaceCodeStd"}}
|
||||
type Querier interface {
|
||||
{{- $dbtxParam := .EmitMethodsWithDBArgument -}}
|
||||
{{- range .GoQueries}}
|
||||
{{- if and (eq .Cmd ":one") ($dbtxParam) }}
|
||||
{{range .Comments}}//{{.}}
|
||||
{{end -}}
|
||||
{{.MethodName}}(ctx context.Context, db DBTX, {{.Arg.Pair}}) ({{.Ret.DefineType}}, error)
|
||||
{{- else if eq .Cmd ":one"}}
|
||||
{{range .Comments}}//{{.}}
|
||||
{{end -}}
|
||||
{{.MethodName}}(ctx context.Context, {{.Arg.Pair}}) ({{.Ret.DefineType}}, error)
|
||||
{{- end}}
|
||||
{{- if and (eq .Cmd ":many") ($dbtxParam) }}
|
||||
{{range .Comments}}//{{.}}
|
||||
{{end -}}
|
||||
{{.MethodName}}(ctx context.Context, db DBTX, {{.Arg.Pair}}) ([]{{.Ret.DefineType}}, error)
|
||||
{{- else if eq .Cmd ":many"}}
|
||||
{{range .Comments}}//{{.}}
|
||||
{{end -}}
|
||||
{{.MethodName}}(ctx context.Context, {{.Arg.Pair}}) ([]{{.Ret.DefineType}}, error)
|
||||
{{- end}}
|
||||
{{- if and (eq .Cmd ":exec") ($dbtxParam) }}
|
||||
{{range .Comments}}//{{.}}
|
||||
{{end -}}
|
||||
{{.MethodName}}(ctx context.Context, db DBTX, {{.Arg.Pair}}) error
|
||||
{{- else if eq .Cmd ":exec"}}
|
||||
{{range .Comments}}//{{.}}
|
||||
{{end -}}
|
||||
{{.MethodName}}(ctx context.Context, {{.Arg.Pair}}) error
|
||||
{{- end}}
|
||||
{{- if and (eq .Cmd ":execrows") ($dbtxParam) }}
|
||||
{{range .Comments}}//{{.}}
|
||||
{{end -}}
|
||||
{{.MethodName}}(ctx context.Context, db DBTX, {{.Arg.Pair}}) (int64, error)
|
||||
{{- else if eq .Cmd ":execrows"}}
|
||||
{{range .Comments}}//{{.}}
|
||||
{{end -}}
|
||||
{{.MethodName}}(ctx context.Context, {{.Arg.Pair}}) (int64, error)
|
||||
{{- end}}
|
||||
{{- if and (eq .Cmd ":execlastid") ($dbtxParam) }}
|
||||
{{range .Comments}}//{{.}}
|
||||
{{end -}}
|
||||
{{.MethodName}}(ctx context.Context, db DBTX, {{.Arg.Pair}}) (int64, error)
|
||||
{{- else if eq .Cmd ":execlastid"}}
|
||||
{{range .Comments}}//{{.}}
|
||||
{{end -}}
|
||||
{{.MethodName}}(ctx context.Context, {{.Arg.Pair}}) (int64, error)
|
||||
{{- end}}
|
||||
{{- if and (eq .Cmd ":execresult") ($dbtxParam) }}
|
||||
{{range .Comments}}//{{.}}
|
||||
{{end -}}
|
||||
{{.MethodName}}(ctx context.Context, db DBTX, {{.Arg.Pair}}) (sql.Result, error)
|
||||
{{- else if eq .Cmd ":execresult"}}
|
||||
{{range .Comments}}//{{.}}
|
||||
{{end -}}
|
||||
{{.MethodName}}(ctx context.Context, {{.Arg.Pair}}) (sql.Result, error)
|
||||
{{- end}}
|
||||
{{- end}}
|
||||
}
|
||||
|
||||
var _ Querier = (*Queries)(nil)
|
||||
{{end}}
|
||||
171
internal/codegen/golang/templates/stdlib/queryCode.tmpl
Normal file
171
internal/codegen/golang/templates/stdlib/queryCode.tmpl
Normal file
@@ -0,0 +1,171 @@
|
||||
{{define "queryCodeStd"}}
|
||||
{{range .GoQueries}}
|
||||
{{if $.OutputQuery .SourceName}}
|
||||
const {{.ConstantName}} = {{$.Q}}-- name: {{.MethodName}} {{.Cmd}}
|
||||
{{escape .SQL}}
|
||||
{{$.Q}}
|
||||
|
||||
{{if .Arg.EmitStruct}}
|
||||
type {{.Arg.Type}} struct { {{- range .Arg.UniqueFields}}
|
||||
{{.Name}} {{.Type}} {{if .Tag}}{{$.Q}}{{.Tag}}{{$.Q}}{{end}}
|
||||
{{- end}}
|
||||
}
|
||||
{{end}}
|
||||
|
||||
{{if .Ret.EmitStruct}}
|
||||
type {{.Ret.Type}} struct { {{- range .Ret.Struct.Fields}}
|
||||
{{.Name}} {{.Type}} {{if .Tag}}{{$.Q}}{{.Tag}}{{$.Q}}{{end}}
|
||||
{{- end}}
|
||||
}
|
||||
{{end}}
|
||||
|
||||
{{if eq .Cmd ":one"}}
|
||||
{{range .Comments}}//{{.}}
|
||||
{{end -}}
|
||||
func (q *Queries) {{.MethodName}}(ctx context.Context, {{ dbarg }} {{.Arg.Pair}}) ({{.Ret.DefineType}}, error) {
|
||||
{{- template "queryCodeStdExec" . }}
|
||||
{{- if or (ne .Arg.Pair .Ret.Pair) (ne .Arg.DefineType .Ret.DefineType) }}
|
||||
var {{.Ret.Name}} {{.Ret.Type}}
|
||||
{{- end}}
|
||||
err := row.Scan({{.Ret.Scan}})
|
||||
{{- if $.WrapErrors}}
|
||||
if err != nil {
|
||||
err = fmt.Errorf("query {{.MethodName}}: %w", err)
|
||||
}
|
||||
{{- end}}
|
||||
return {{.Ret.ReturnName}}, err
|
||||
}
|
||||
{{end}}
|
||||
|
||||
{{if eq .Cmd ":many"}}
|
||||
{{range .Comments}}//{{.}}
|
||||
{{end -}}
|
||||
func (q *Queries) {{.MethodName}}(ctx context.Context, {{ dbarg }} {{.Arg.Pair}}) ([]{{.Ret.DefineType}}, error) {
|
||||
{{- template "queryCodeStdExec" . }}
|
||||
if err != nil {
|
||||
return nil, {{if $.WrapErrors}}fmt.Errorf("query {{.MethodName}}: %w", err){{else}}err{{end}}
|
||||
}
|
||||
defer rows.Close()
|
||||
{{- if $.EmitEmptySlices}}
|
||||
items := []{{.Ret.DefineType}}{}
|
||||
{{else}}
|
||||
var items []{{.Ret.DefineType}}
|
||||
{{end -}}
|
||||
for rows.Next() {
|
||||
var {{.Ret.Name}} {{.Ret.Type}}
|
||||
if err := rows.Scan({{.Ret.Scan}}); err != nil {
|
||||
return nil, {{if $.WrapErrors}}fmt.Errorf("query {{.MethodName}}: %w", err){{else}}err{{end}}
|
||||
}
|
||||
items = append(items, {{.Ret.ReturnName}})
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, {{if $.WrapErrors}}fmt.Errorf("query {{.MethodName}}: %w", err){{else}}err{{end}}
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, {{if $.WrapErrors}}fmt.Errorf("query {{.MethodName}}: %w", err){{else}}err{{end}}
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
{{end}}
|
||||
|
||||
{{if eq .Cmd ":exec"}}
|
||||
{{range .Comments}}//{{.}}
|
||||
{{end -}}
|
||||
func (q *Queries) {{.MethodName}}(ctx context.Context, {{ dbarg }} {{.Arg.Pair}}) error {
|
||||
{{- template "queryCodeStdExec" . }}
|
||||
{{- if $.WrapErrors}}
|
||||
if err != nil {
|
||||
err = fmt.Errorf("query {{.MethodName}}: %w", err)
|
||||
}
|
||||
{{- end}}
|
||||
return err
|
||||
}
|
||||
{{end}}
|
||||
|
||||
{{if eq .Cmd ":execrows"}}
|
||||
{{range .Comments}}//{{.}}
|
||||
{{end -}}
|
||||
func (q *Queries) {{.MethodName}}(ctx context.Context, {{ dbarg }} {{.Arg.Pair}}) (int64, error) {
|
||||
{{- template "queryCodeStdExec" . }}
|
||||
if err != nil {
|
||||
return 0, {{if $.WrapErrors}}fmt.Errorf("query {{.MethodName}}: %w", err){{else}}err{{end}}
|
||||
}
|
||||
return result.RowsAffected()
|
||||
}
|
||||
{{end}}
|
||||
|
||||
{{if eq .Cmd ":execlastid"}}
|
||||
{{range .Comments}}//{{.}}
|
||||
{{end -}}
|
||||
func (q *Queries) {{.MethodName}}(ctx context.Context, {{ dbarg }} {{.Arg.Pair}}) (int64, error) {
|
||||
{{- template "queryCodeStdExec" . }}
|
||||
if err != nil {
|
||||
return 0, {{if $.WrapErrors}}fmt.Errorf("query {{.MethodName}}: %w", err){{else}}err{{end}}
|
||||
}
|
||||
return result.LastInsertId()
|
||||
}
|
||||
{{end}}
|
||||
|
||||
{{if eq .Cmd ":execresult"}}
|
||||
{{range .Comments}}//{{.}}
|
||||
{{end -}}
|
||||
func (q *Queries) {{.MethodName}}(ctx context.Context, {{ dbarg }} {{.Arg.Pair}}) (sql.Result, error) {
|
||||
{{- template "queryCodeStdExec" . }}
|
||||
{{- if $.WrapErrors}}
|
||||
if err != nil {
|
||||
err = fmt.Errorf("query {{.MethodName}}: %w", err)
|
||||
}
|
||||
return result, err
|
||||
{{- end}}
|
||||
}
|
||||
{{end}}
|
||||
|
||||
{{end}}
|
||||
{{end}}
|
||||
{{end}}
|
||||
|
||||
{{define "queryCodeStdExec"}}
|
||||
{{- if .Arg.HasSqlcSlices }}
|
||||
query := {{.ConstantName}}
|
||||
var queryParams []any
|
||||
{{- if .Arg.Struct }}
|
||||
{{- $arg := .Arg }}
|
||||
{{- range .Arg.Struct.Fields }}
|
||||
{{- if .HasSqlcSlice }}
|
||||
if len({{$arg.VariableForField .}}) > 0 {
|
||||
for _, v := range {{$arg.VariableForField .}} {
|
||||
queryParams = append(queryParams, v)
|
||||
}
|
||||
query = strings.Replace(query, "/*SLICE:{{.Column.Name}}*/?", strings.Repeat(",?", len({{$arg.VariableForField .}}))[1:], 1)
|
||||
} else {
|
||||
query = strings.Replace(query, "/*SLICE:{{.Column.Name}}*/?", "NULL", 1)
|
||||
}
|
||||
{{- else }}
|
||||
queryParams = append(queryParams, {{$arg.VariableForField .}})
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- else }}
|
||||
{{- /* Single argument parameter to this goroutine (they are not packed
|
||||
in a struct), because .Arg.HasSqlcSlices further up above was true,
|
||||
this section is 100% a slice (impossible to get here otherwise).
|
||||
*/}}
|
||||
if len({{.Arg.Name}}) > 0 {
|
||||
for _, v := range {{.Arg.Name}} {
|
||||
queryParams = append(queryParams, v)
|
||||
}
|
||||
query = strings.Replace(query, "/*SLICE:{{.Arg.Column.Name}}*/?", strings.Repeat(",?", len({{.Arg.Name}}))[1:], 1)
|
||||
} else {
|
||||
query = strings.Replace(query, "/*SLICE:{{.Arg.Column.Name}}*/?", "NULL", 1)
|
||||
}
|
||||
{{- end }}
|
||||
{{- if emitPreparedQueries }}
|
||||
{{ queryRetval . }} {{ queryMethod . }}(ctx, nil, query, queryParams...)
|
||||
{{- else}}
|
||||
{{ queryRetval . }} {{ queryMethod . }}(ctx, query, queryParams...)
|
||||
{{- end -}}
|
||||
{{- else if emitPreparedQueries }}
|
||||
{{- queryRetval . }} {{ queryMethod . }}(ctx, q.{{.FieldName}}, {{.ConstantName}}, {{.Arg.Params}})
|
||||
{{- else}}
|
||||
{{- queryRetval . }} {{ queryMethod . }}(ctx, {{.ConstantName}}, {{.Arg.Params}})
|
||||
{{- end -}}
|
||||
{{end}}
|
||||
254
internal/codegen/golang/templates/template.tmpl
Normal file
254
internal/codegen/golang/templates/template.tmpl
Normal file
@@ -0,0 +1,254 @@
|
||||
{{define "dbFile"}}
|
||||
{{if .BuildTags}}
|
||||
//go:build {{.BuildTags}}
|
||||
|
||||
{{end}}// Code generated by sqlc. DO NOT EDIT.
|
||||
{{if not .OmitSqlcVersion}}// versions:
|
||||
// sqlc {{.SqlcVersion}}
|
||||
{{end}}
|
||||
|
||||
package {{.Package}}
|
||||
|
||||
{{ if hasImports .SourceName }}
|
||||
import (
|
||||
{{range imports .SourceName}}
|
||||
{{range .}}{{.}}
|
||||
{{end}}
|
||||
{{end}}
|
||||
)
|
||||
{{end}}
|
||||
|
||||
{{template "dbCode" . }}
|
||||
{{end}}
|
||||
|
||||
{{define "dbCode"}}
|
||||
|
||||
{{if .SQLDriver.IsPGX }}
|
||||
{{- template "dbCodeTemplatePgx" .}}
|
||||
{{else}}
|
||||
{{- template "dbCodeTemplateStd" .}}
|
||||
{{end}}
|
||||
|
||||
{{end}}
|
||||
|
||||
{{define "interfaceFile"}}
|
||||
{{if .BuildTags}}
|
||||
//go:build {{.BuildTags}}
|
||||
|
||||
{{end}}// Code generated by sqlc. DO NOT EDIT.
|
||||
{{if not .OmitSqlcVersion}}// versions:
|
||||
// sqlc {{.SqlcVersion}}
|
||||
{{end}}
|
||||
|
||||
package {{.Package}}
|
||||
|
||||
{{ if hasImports .SourceName }}
|
||||
import (
|
||||
{{range imports .SourceName}}
|
||||
{{range .}}{{.}}
|
||||
{{end}}
|
||||
{{end}}
|
||||
)
|
||||
{{end}}
|
||||
|
||||
{{template "interfaceCode" . }}
|
||||
{{end}}
|
||||
|
||||
{{define "interfaceCode"}}
|
||||
{{if .SQLDriver.IsPGX }}
|
||||
{{- template "interfaceCodePgx" .}}
|
||||
{{else}}
|
||||
{{- template "interfaceCodeStd" .}}
|
||||
{{end}}
|
||||
{{end}}
|
||||
|
||||
{{define "modelsFile"}}
|
||||
{{if .BuildTags}}
|
||||
//go:build {{.BuildTags}}
|
||||
|
||||
{{end}}// Code generated by sqlc. DO NOT EDIT.
|
||||
{{if not .OmitSqlcVersion}}// versions:
|
||||
// sqlc {{.SqlcVersion}}
|
||||
{{end}}
|
||||
|
||||
package {{.ModelsPackage}}
|
||||
|
||||
{{ if hasImports .SourceName }}
|
||||
import (
|
||||
{{range imports .SourceName}}
|
||||
{{range .}}{{.}}
|
||||
{{end}}
|
||||
{{end}}
|
||||
)
|
||||
{{end}}
|
||||
|
||||
{{template "modelsCode" . }}
|
||||
{{end}}
|
||||
|
||||
{{define "modelsCode"}}
|
||||
{{range .Enums}}
|
||||
{{if .Comment}}{{comment .Comment}}{{end}}
|
||||
type {{.Name}} string
|
||||
|
||||
const (
|
||||
{{- range .Constants}}
|
||||
{{.Name}} {{.Type}} = "{{.Value}}"
|
||||
{{- end}}
|
||||
)
|
||||
|
||||
func (e *{{.Name}}) Scan(src any) error {
|
||||
switch s := src.(type) {
|
||||
case []byte:
|
||||
*e = {{.Name}}(s)
|
||||
case string:
|
||||
*e = {{.Name}}(s)
|
||||
default:
|
||||
return fmt.Errorf("unsupported scan type for {{.Name}}: %T", src)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type Null{{.Name}} struct {
|
||||
{{.Name}} {{.Name}} {{if .NameTag}}{{$.Q}}{{.NameTag}}{{$.Q}}{{end}}
|
||||
Valid bool {{if .ValidTag}}{{$.Q}}{{.ValidTag}}{{$.Q}}{{end}} // Valid is true if {{.Name}} is not NULL
|
||||
}
|
||||
|
||||
// Scan implements the Scanner interface.
|
||||
func (ns *Null{{.Name}}) Scan(value any) error {
|
||||
if value == nil {
|
||||
ns.{{.Name}}, ns.Valid = "", false
|
||||
return nil
|
||||
}
|
||||
ns.Valid = true
|
||||
return ns.{{.Name}}.Scan(value)
|
||||
}
|
||||
|
||||
// Value implements the driver Valuer interface.
|
||||
func (ns Null{{.Name}}) Value() (driver.Value, error) {
|
||||
if !ns.Valid {
|
||||
return nil, nil
|
||||
}
|
||||
return string(ns.{{.Name}}), nil
|
||||
}
|
||||
|
||||
|
||||
{{ if $.EmitEnumValidMethod }}
|
||||
func (e {{.Name}}) Valid() bool {
|
||||
switch e {
|
||||
case {{ range $idx, $name := .Constants }}{{ if ne $idx 0 }},{{ "\n" }}{{ end }}{{ .Name }}{{ end }}:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
{{ end }}
|
||||
|
||||
{{ if $.EmitAllEnumValues }}
|
||||
func All{{ .Name }}Values() []{{ .Name }} {
|
||||
return []{{ .Name }}{ {{ range .Constants}}{{ "\n" }}{{ .Name }},{{ end }}
|
||||
}
|
||||
}
|
||||
{{ end }}
|
||||
{{end}}
|
||||
|
||||
{{range .Structs}}
|
||||
{{if .Comment}}{{comment .Comment}}{{end}}
|
||||
type {{.Name}} struct { {{- range .Fields}}
|
||||
{{- if .Comment}}
|
||||
{{comment .Comment}}{{else}}
|
||||
{{- end}}
|
||||
{{.Name}} {{.Type}} {{if .Tag}}{{$.Q}}{{.Tag}}{{$.Q}}{{end}}
|
||||
{{- end}}
|
||||
}
|
||||
{{end}}
|
||||
{{end}}
|
||||
|
||||
{{define "queryFile"}}
|
||||
{{if .BuildTags}}
|
||||
//go:build {{.BuildTags}}
|
||||
|
||||
{{end}}// Code generated by sqlc. DO NOT EDIT.
|
||||
{{if not .OmitSqlcVersion}}// versions:
|
||||
// sqlc {{.SqlcVersion}}
|
||||
{{end}}// source: {{.SourceName}}
|
||||
|
||||
package {{.Package}}
|
||||
|
||||
{{ if hasImports .SourceName }}
|
||||
import (
|
||||
{{range imports .SourceName}}
|
||||
{{range .}}{{.}}
|
||||
{{end}}
|
||||
{{end}}
|
||||
)
|
||||
{{end}}
|
||||
|
||||
{{template "queryCode" . }}
|
||||
{{end}}
|
||||
|
||||
{{define "queryCode"}}
|
||||
{{if .SQLDriver.IsPGX }}
|
||||
{{- template "queryCodePgx" .}}
|
||||
{{else}}
|
||||
{{- template "queryCodeStd" .}}
|
||||
{{end}}
|
||||
{{end}}
|
||||
|
||||
{{define "copyfromFile"}}
|
||||
{{if .BuildTags}}
|
||||
//go:build {{.BuildTags}}
|
||||
|
||||
{{end}}// Code generated by sqlc. DO NOT EDIT.
|
||||
{{if not .OmitSqlcVersion}}// versions:
|
||||
// sqlc {{.SqlcVersion}}
|
||||
{{end}}// source: {{.SourceName}}
|
||||
|
||||
package {{.Package}}
|
||||
|
||||
{{ if hasImports .SourceName }}
|
||||
import (
|
||||
{{range imports .SourceName}}
|
||||
{{range .}}{{.}}
|
||||
{{end}}
|
||||
{{end}}
|
||||
)
|
||||
{{end}}
|
||||
|
||||
{{template "copyfromCode" . }}
|
||||
{{end}}
|
||||
|
||||
{{define "copyfromCode"}}
|
||||
{{if .SQLDriver.IsPGX }}
|
||||
{{- template "copyfromCodePgx" .}}
|
||||
{{else if .SQLDriver.IsGoSQLDriverMySQL }}
|
||||
{{- template "copyfromCodeGoSqlDriver" .}}
|
||||
{{end}}
|
||||
{{end}}
|
||||
|
||||
{{define "batchFile"}}
|
||||
{{if .BuildTags}}
|
||||
//go:build {{.BuildTags}}
|
||||
|
||||
{{end}}// Code generated by sqlc. DO NOT EDIT.
|
||||
{{if not .OmitSqlcVersion}}// versions:
|
||||
// sqlc {{.SqlcVersion}}
|
||||
{{end}}// source: {{.SourceName}}
|
||||
|
||||
package {{.Package}}
|
||||
|
||||
{{ if hasImports .SourceName }}
|
||||
import (
|
||||
{{range imports .SourceName}}
|
||||
{{range .}}{{.}}
|
||||
{{end}}
|
||||
{{end}}
|
||||
)
|
||||
{{end}}
|
||||
|
||||
{{template "batchCode" . }}
|
||||
{{end}}
|
||||
|
||||
{{define "batchCode"}}
|
||||
{{if .SQLDriver.IsPGX }}
|
||||
{{- template "batchCodePgx" .}}
|
||||
{{end}}
|
||||
{{end}}
|
||||
68
internal/codegen/json/gen.go
Normal file
68
internal/codegen/json/gen.go
Normal file
@@ -0,0 +1,68 @@
|
||||
package json
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
ejson "encoding/json"
|
||||
"fmt"
|
||||
|
||||
"google.golang.org/protobuf/encoding/protojson"
|
||||
|
||||
"github.com/sqlc-dev/sqlc/internal/plugin"
|
||||
)
|
||||
|
||||
func parseOptions(req *plugin.GenerateRequest) (*opts, error) {
|
||||
if len(req.PluginOptions) == 0 {
|
||||
return new(opts), nil
|
||||
}
|
||||
|
||||
var options *opts
|
||||
dec := ejson.NewDecoder(bytes.NewReader(req.PluginOptions))
|
||||
dec.DisallowUnknownFields()
|
||||
if err := dec.Decode(&options); err != nil {
|
||||
return options, fmt.Errorf("unmarshalling options: %s", err)
|
||||
}
|
||||
return options, nil
|
||||
}
|
||||
|
||||
func Generate(ctx context.Context, req *plugin.GenerateRequest) (*plugin.GenerateResponse, error) {
|
||||
options, err := parseOptions(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
indent := " "
|
||||
if options.Indent != "" {
|
||||
indent = options.Indent
|
||||
}
|
||||
|
||||
filename := "codegen_request.json"
|
||||
if options.Filename != "" {
|
||||
filename = options.Filename
|
||||
}
|
||||
|
||||
// The output of protojson has randomized whitespace
|
||||
// https://github.com/golang/protobuf/issues/1082
|
||||
m := &protojson.MarshalOptions{
|
||||
EmitUnpopulated: true,
|
||||
Indent: "",
|
||||
UseProtoNames: true,
|
||||
}
|
||||
data, err := m.Marshal(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var rm ejson.RawMessage = data
|
||||
blob, err := ejson.MarshalIndent(rm, "", indent)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &plugin.GenerateResponse{
|
||||
Files: []*plugin.File{
|
||||
{
|
||||
Name: filename,
|
||||
Contents: append(blob, '\n'),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
7
internal/codegen/json/opts.go
Normal file
7
internal/codegen/json/opts.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package json
|
||||
|
||||
type opts struct {
|
||||
Out string `json:"out"`
|
||||
Indent string `json:"indent,omitempty"`
|
||||
Filename string `json:"filename,omitempty"`
|
||||
}
|
||||
33
internal/codegen/sdk/sdk.go
Normal file
33
internal/codegen/sdk/sdk.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package sdk
|
||||
|
||||
import (
|
||||
"github.com/sqlc-dev/sqlc/internal/pattern"
|
||||
"github.com/sqlc-dev/sqlc/internal/plugin"
|
||||
)
|
||||
|
||||
func DataType(n *plugin.Identifier) string {
|
||||
if n.Schema != "" {
|
||||
return n.Schema + "." + n.Name
|
||||
} else {
|
||||
return n.Name
|
||||
}
|
||||
}
|
||||
|
||||
func MatchString(pat, target string) bool {
|
||||
matcher, err := pattern.MatchCompile(pat)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return matcher.MatchString(target)
|
||||
}
|
||||
|
||||
func SameTableName(tableID, f *plugin.Identifier, defaultSchema string) bool {
|
||||
if tableID == nil {
|
||||
return false
|
||||
}
|
||||
schema := tableID.Schema
|
||||
if tableID.Schema == "" {
|
||||
schema = defaultSchema
|
||||
}
|
||||
return tableID.Catalog == f.Catalog && schema == f.Schema && tableID.Name == f.Name
|
||||
}
|
||||
42
internal/codegen/sdk/utils.go
Normal file
42
internal/codegen/sdk/utils.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package sdk
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
func LowerTitle(s string) string {
|
||||
if s == "" {
|
||||
return s
|
||||
}
|
||||
|
||||
a := []rune(s)
|
||||
a[0] = unicode.ToLower(a[0])
|
||||
return string(a)
|
||||
}
|
||||
|
||||
func Title(s string) string {
|
||||
return strings.Title(s)
|
||||
}
|
||||
|
||||
// Go string literals cannot contain backtick. If a string contains
|
||||
// a backtick, replace it the following way:
|
||||
//
|
||||
// input:
|
||||
//
|
||||
// SELECT `group` FROM foo
|
||||
//
|
||||
// output:
|
||||
//
|
||||
// SELECT ` + "`" + `group` + "`" + ` FROM foo
|
||||
//
|
||||
// # The escaped string must be rendered inside an existing string literal
|
||||
//
|
||||
// A string cannot be escaped twice
|
||||
func EscapeBacktick(s string) string {
|
||||
return strings.Replace(s, "`", "`+\"`\"+`", -1)
|
||||
}
|
||||
|
||||
func DoubleSlashComment(s string) string {
|
||||
return "// " + strings.ReplaceAll(s, "\n", "\n// ")
|
||||
}
|
||||
28
internal/codegen/sdk/utils_test.go
Normal file
28
internal/codegen/sdk/utils_test.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package sdk
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestLowerTitle(t *testing.T) {
|
||||
|
||||
// empty
|
||||
if LowerTitle("") != "" {
|
||||
t.Fatal("expected empty title to remain empty")
|
||||
}
|
||||
|
||||
// all lowercase
|
||||
if LowerTitle("lowercase") != "lowercase" {
|
||||
t.Fatal("expected no changes when input is all lowercase")
|
||||
}
|
||||
|
||||
// all uppercase
|
||||
if LowerTitle("UPPERCASE") != "uPPERCASE" {
|
||||
t.Fatal("expected first rune to be lower when input is all uppercase")
|
||||
}
|
||||
|
||||
// Title Case
|
||||
if LowerTitle("Title Case") != "title Case" {
|
||||
t.Fatal("expected first rune to be lower when input is Title Case")
|
||||
}
|
||||
}
|
||||
214
internal/compiler/analyze.go
Normal file
214
internal/compiler/analyze.go
Normal file
@@ -0,0 +1,214 @@
|
||||
package compiler
|
||||
|
||||
import (
|
||||
"sort"
|
||||
|
||||
analyzer "github.com/sqlc-dev/sqlc/internal/analysis"
|
||||
"github.com/sqlc-dev/sqlc/internal/config"
|
||||
"github.com/sqlc-dev/sqlc/internal/source"
|
||||
"github.com/sqlc-dev/sqlc/internal/sql/ast"
|
||||
"github.com/sqlc-dev/sqlc/internal/sql/named"
|
||||
"github.com/sqlc-dev/sqlc/internal/sql/preprocess"
|
||||
"github.com/sqlc-dev/sqlc/internal/sql/validate"
|
||||
)
|
||||
|
||||
type analysis struct {
|
||||
Table *ast.TableName
|
||||
Columns []*Column
|
||||
Parameters []Parameter
|
||||
Named *named.ParamSet
|
||||
Query string
|
||||
}
|
||||
|
||||
func convertTableName(id *analyzer.Identifier) *ast.TableName {
|
||||
if id == nil {
|
||||
return nil
|
||||
}
|
||||
return &ast.TableName{
|
||||
Catalog: id.Catalog,
|
||||
Schema: id.Schema,
|
||||
Name: id.Name,
|
||||
}
|
||||
}
|
||||
|
||||
func convertTypeName(id *analyzer.Identifier) *ast.TypeName {
|
||||
if id == nil {
|
||||
return nil
|
||||
}
|
||||
return &ast.TypeName{
|
||||
Catalog: id.Catalog,
|
||||
Schema: id.Schema,
|
||||
Name: id.Name,
|
||||
}
|
||||
}
|
||||
|
||||
func convertColumn(c *analyzer.Column) *Column {
|
||||
length := int(c.Length)
|
||||
return &Column{
|
||||
Name: c.Name,
|
||||
OriginalName: c.OriginalName,
|
||||
DataType: c.DataType,
|
||||
NotNull: c.NotNull,
|
||||
Unsigned: c.Unsigned,
|
||||
IsArray: c.IsArray,
|
||||
ArrayDims: int(c.ArrayDims),
|
||||
Comment: c.Comment,
|
||||
Length: &length,
|
||||
IsNamedParam: c.IsNamedParam,
|
||||
IsFuncCall: c.IsFuncCall,
|
||||
Scope: c.Scope,
|
||||
Table: convertTableName(c.Table),
|
||||
TableAlias: c.TableAlias,
|
||||
Type: convertTypeName(c.Type),
|
||||
EmbedTable: convertTableName(c.EmbedTable),
|
||||
IsSqlcSlice: c.IsSqlcSlice,
|
||||
}
|
||||
}
|
||||
|
||||
func combineAnalysis(prev *analysis, a *analyzer.Analysis) *analysis {
|
||||
var cols []*Column
|
||||
for _, c := range a.Columns {
|
||||
cols = append(cols, convertColumn(c))
|
||||
}
|
||||
var params []Parameter
|
||||
for _, p := range a.Params {
|
||||
params = append(params, Parameter{
|
||||
Number: int(p.Number),
|
||||
Column: convertColumn(p.Column),
|
||||
})
|
||||
}
|
||||
if len(prev.Columns) == len(cols) {
|
||||
for i := range prev.Columns {
|
||||
// Only override column types if the analyzer provides a specific type
|
||||
// (not "any"), since the catalog-based inference may have better info
|
||||
if cols[i].DataType != "any" {
|
||||
prev.Columns[i].DataType = cols[i].DataType
|
||||
prev.Columns[i].IsArray = cols[i].IsArray
|
||||
prev.Columns[i].ArrayDims = cols[i].ArrayDims
|
||||
}
|
||||
}
|
||||
} else {
|
||||
embedding := false
|
||||
for i := range prev.Columns {
|
||||
if prev.Columns[i].EmbedTable != nil {
|
||||
embedding = true
|
||||
}
|
||||
}
|
||||
if !embedding {
|
||||
prev.Columns = cols
|
||||
}
|
||||
}
|
||||
if len(prev.Parameters) == len(params) {
|
||||
for i := range prev.Parameters {
|
||||
// Only override parameter types if the analyzer provides a specific type
|
||||
// (not "any"), since the catalog-based inference may have better info
|
||||
if params[i].Column.DataType != "any" {
|
||||
prev.Parameters[i].Column.DataType = params[i].Column.DataType
|
||||
prev.Parameters[i].Column.IsArray = params[i].Column.IsArray
|
||||
prev.Parameters[i].Column.ArrayDims = params[i].Column.ArrayDims
|
||||
}
|
||||
}
|
||||
} else {
|
||||
prev.Parameters = params
|
||||
}
|
||||
return prev
|
||||
}
|
||||
|
||||
func (c *Compiler) analyzeQuery(raw *ast.RawStmt, query string, pre *preprocess.Statement) (*analysis, error) {
|
||||
return c._analyzeQuery(raw, query, pre, true)
|
||||
}
|
||||
|
||||
func (c *Compiler) inferQuery(raw *ast.RawStmt, query string, pre *preprocess.Statement) (*analysis, error) {
|
||||
return c._analyzeQuery(raw, query, pre, false)
|
||||
}
|
||||
|
||||
func (c *Compiler) _analyzeQuery(raw *ast.RawStmt, query string, pre *preprocess.Statement, failfast bool) (*analysis, error) {
|
||||
errors := make([]error, 0)
|
||||
check := func(err error) error {
|
||||
if failfast {
|
||||
return err
|
||||
}
|
||||
if err != nil {
|
||||
errors = append(errors, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
dollar := pre.Dollar
|
||||
if err := check(pre.ParamErr); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
namedParams := pre.Params
|
||||
|
||||
var table *ast.TableName
|
||||
switch n := raw.Stmt.(type) {
|
||||
case *ast.InsertStmt:
|
||||
var err error
|
||||
table, err = ParseTableName(n.Relation)
|
||||
if err := check(err); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := check(validate.InsertStmt(c.catalog, table, n)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if err := check(validate.FuncCall(c.catalog, c.combo, raw)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := check(validate.Slice(raw, pre.Slices)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rvs := rangeVars(raw.Stmt)
|
||||
refs, errs := findParameters(raw.Stmt)
|
||||
if len(errs) > 0 {
|
||||
if failfast {
|
||||
return nil, errs[0]
|
||||
}
|
||||
errors = append(errors, errs...)
|
||||
}
|
||||
refs = uniqueParamRefs(refs, dollar)
|
||||
if c.conf.Engine == config.EngineMySQL || !dollar {
|
||||
sort.Slice(refs, func(i, j int) bool { return refs[i].ref.Location < refs[j].ref.Location })
|
||||
} else {
|
||||
sort.Slice(refs, func(i, j int) bool { return refs[i].ref.Number < refs[j].ref.Number })
|
||||
}
|
||||
embeds := pre.Embeds
|
||||
qc, err := c.buildQueryCatalog(c.catalog, raw.Stmt, embeds)
|
||||
if err := check(err); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
params, err := c.resolveCatalogRefs(qc, rvs, refs, namedParams, embeds)
|
||||
if err := check(err); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cols, err := c.outputColumns(qc, raw.Stmt)
|
||||
if err := check(err); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
edits, err := c.expand(qc, raw)
|
||||
if check(err); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
expanded, err := source.Mutate(query, edits)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var rerr error
|
||||
if len(errors) > 0 {
|
||||
rerr = errors[0]
|
||||
}
|
||||
|
||||
return &analysis{
|
||||
Table: table,
|
||||
Columns: cols,
|
||||
Parameters: params,
|
||||
Query: expanded,
|
||||
Named: namedParams,
|
||||
}, rerr
|
||||
}
|
||||
62
internal/compiler/catalog_core.go
Normal file
62
internal/compiler/catalog_core.go
Normal file
@@ -0,0 +1,62 @@
|
||||
package compiler
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/sqlc-dev/sqlc/internal/core"
|
||||
"github.com/sqlc-dev/sqlc/internal/sql/ast"
|
||||
"github.com/sqlc-dev/sqlc/internal/sql/catalog"
|
||||
)
|
||||
|
||||
// coreResultCatalog dumps the core catalog into the legacy catalog shape a
|
||||
// Result carries, so codegen sees the same table models either way a query
|
||||
// set was analyzed. Only relations make the trip: codegen reads tables and
|
||||
// their columns to build models, and none of the types, functions or
|
||||
// operators the core catalog also holds.
|
||||
func coreResultCatalog(c *core.Catalog) (*catalog.Catalog, error) {
|
||||
cat := catalog.New("public")
|
||||
namespaces, err := c.Namespaces()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, ns := range namespaces {
|
||||
schema := &catalog.Schema{Name: ns.Name}
|
||||
tables, err := c.TablesInNamespace(ns.OID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, table := range tables {
|
||||
cols, err := c.ClassCodegenColumns(table.OID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
t := &catalog.Table{Rel: &ast.TableName{Schema: ns.Name, Name: table.Name}}
|
||||
for _, col := range cols {
|
||||
// Codegen reads a data type and an array flag, and renders
|
||||
// a "[]" per dimension, so an array of arrays of integers
|
||||
// is the type integer with two dimensions.
|
||||
expr, err := c.TypeExprOf(col.TypeOID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
inner := expr.Innermost()
|
||||
column := &catalog.Column{
|
||||
Name: col.Name,
|
||||
Type: ast.TypeName{Name: strings.TrimSuffix(inner.Name, " unsigned")},
|
||||
IsNotNull: col.NotNull,
|
||||
IsArray: expr.IsArray(),
|
||||
ArrayDims: expr.ArrayDims(),
|
||||
IsUnsigned: strings.HasSuffix(inner.Name, " unsigned"),
|
||||
}
|
||||
if len(inner.Args) > 0 && inner.Args[0].Int != nil {
|
||||
l := int(*inner.Args[0].Int)
|
||||
column.Length = &l
|
||||
}
|
||||
t.Columns = append(t.Columns, column)
|
||||
}
|
||||
schema.Tables = append(schema.Tables, t)
|
||||
}
|
||||
cat.Schemas = append(cat.Schemas, schema)
|
||||
}
|
||||
return cat, nil
|
||||
}
|
||||
137
internal/compiler/compat.go
Normal file
137
internal/compiler/compat.go
Normal file
@@ -0,0 +1,137 @@
|
||||
package compiler
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/sqlc-dev/sqlc/internal/sql/ast"
|
||||
"github.com/sqlc-dev/sqlc/internal/sql/astutils"
|
||||
)
|
||||
|
||||
// This is mainly copy-pasted from internal/postgresql/parse.go
|
||||
func stringSlice(list *ast.List) []string {
|
||||
items := []string{}
|
||||
for _, item := range list.Items {
|
||||
if n, ok := item.(*ast.String); ok {
|
||||
items = append(items, n.Str)
|
||||
}
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
type Relation struct {
|
||||
Catalog string
|
||||
Schema string
|
||||
Name string
|
||||
}
|
||||
|
||||
func parseRelation(node ast.Node) (*Relation, error) {
|
||||
switch n := node.(type) {
|
||||
case *ast.Boolean:
|
||||
if n == nil {
|
||||
return nil, fmt.Errorf("unexpected nil in %T node", n)
|
||||
}
|
||||
return &Relation{Name: "bool"}, nil
|
||||
|
||||
case *ast.List:
|
||||
if n == nil {
|
||||
return nil, fmt.Errorf("unexpected nil in %T node", n)
|
||||
}
|
||||
parts := stringSlice(n)
|
||||
switch len(parts) {
|
||||
case 1:
|
||||
return &Relation{
|
||||
Name: parts[0],
|
||||
}, nil
|
||||
case 2:
|
||||
return &Relation{
|
||||
Schema: parts[0],
|
||||
Name: parts[1],
|
||||
}, nil
|
||||
case 3:
|
||||
return &Relation{
|
||||
Catalog: parts[0],
|
||||
Schema: parts[1],
|
||||
Name: parts[2],
|
||||
}, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid name: %s", astutils.Join(n, "."))
|
||||
}
|
||||
|
||||
case *ast.RangeVar:
|
||||
if n == nil {
|
||||
return nil, fmt.Errorf("unexpected nil in %T node", n)
|
||||
}
|
||||
name := Relation{}
|
||||
if n.Catalogname != nil {
|
||||
name.Catalog = *n.Catalogname
|
||||
}
|
||||
if n.Schemaname != nil {
|
||||
name.Schema = *n.Schemaname
|
||||
}
|
||||
if n.Relname != nil {
|
||||
name.Name = *n.Relname
|
||||
}
|
||||
return &name, nil
|
||||
|
||||
case *ast.TypeName:
|
||||
if n == nil {
|
||||
return nil, fmt.Errorf("unexpected nil in %T node", n)
|
||||
}
|
||||
if n.Names != nil {
|
||||
return parseRelation(n.Names)
|
||||
} else {
|
||||
return &Relation{Name: n.Name}, nil
|
||||
}
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("unexpected node type: %T", node)
|
||||
}
|
||||
}
|
||||
|
||||
func ParseTableName(node ast.Node) (*ast.TableName, error) {
|
||||
rel, err := parseRelation(node)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parse table name: %w", err)
|
||||
}
|
||||
return &ast.TableName{
|
||||
Catalog: rel.Catalog,
|
||||
Schema: rel.Schema,
|
||||
Name: rel.Name,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func ParseTypeName(node ast.Node) (*ast.TypeName, error) {
|
||||
rel, err := parseRelation(node)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parse type name: %w", err)
|
||||
}
|
||||
return &ast.TypeName{
|
||||
Catalog: rel.Catalog,
|
||||
Schema: rel.Schema,
|
||||
Name: rel.Name,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func ParseRelationString(name string) (*Relation, error) {
|
||||
parts := strings.Split(name, ".")
|
||||
switch len(parts) {
|
||||
case 1:
|
||||
return &Relation{
|
||||
Name: parts[0],
|
||||
}, nil
|
||||
case 2:
|
||||
return &Relation{
|
||||
Schema: parts[0],
|
||||
Name: parts[1],
|
||||
}, nil
|
||||
case 3:
|
||||
return &Relation{
|
||||
Catalog: parts[0],
|
||||
Schema: parts[1],
|
||||
Name: parts[2],
|
||||
}, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid name: %s", name)
|
||||
}
|
||||
}
|
||||
300
internal/compiler/compile.go
Normal file
300
internal/compiler/compile.go
Normal file
@@ -0,0 +1,300 @@
|
||||
package compiler
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/sync/errgroup"
|
||||
|
||||
"github.com/sqlc-dev/sqlc/internal/core"
|
||||
coreschema "github.com/sqlc-dev/sqlc/internal/core/schema"
|
||||
"github.com/sqlc-dev/sqlc/internal/migrations"
|
||||
"github.com/sqlc-dev/sqlc/internal/multierr"
|
||||
"github.com/sqlc-dev/sqlc/internal/opts"
|
||||
"github.com/sqlc-dev/sqlc/internal/rpc"
|
||||
"github.com/sqlc-dev/sqlc/internal/source"
|
||||
"github.com/sqlc-dev/sqlc/internal/sql/ast"
|
||||
"github.com/sqlc-dev/sqlc/internal/sql/preprocess"
|
||||
"github.com/sqlc-dev/sqlc/internal/sql/sqlerr"
|
||||
"github.com/sqlc-dev/sqlc/internal/sql/sqlpath"
|
||||
)
|
||||
|
||||
// TODO: Rename this interface Engine
|
||||
type Parser interface {
|
||||
Parse(io.Reader) ([]ast.Statement, error)
|
||||
CommentSyntax() source.CommentSyntax
|
||||
IsReservedKeyword(string) bool
|
||||
}
|
||||
|
||||
// schemaFile is a schema file's contents, after the rewrites that are applied
|
||||
// before any of it is parsed.
|
||||
type schemaFile struct {
|
||||
name string
|
||||
contents string
|
||||
}
|
||||
|
||||
func (c *Compiler) parseCatalog(schemas []string) error {
|
||||
paths, err := sqlpath.Glob(schemas)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
merr := multierr.New()
|
||||
files := make([]schemaFile, 0, len(paths))
|
||||
for _, path := range paths {
|
||||
blob, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
merr.Add(path, "", 0, err)
|
||||
continue
|
||||
}
|
||||
contents := migrations.RemoveRollbackStatements(string(blob))
|
||||
contents = migrations.RemovePsqlMetaCommands(contents)
|
||||
files = append(files, schemaFile{name: path, contents: contents})
|
||||
c.schema = append(c.schema, contents)
|
||||
}
|
||||
|
||||
if c.coreAnalysis {
|
||||
// A schema file that could not be read is not part of the schema the
|
||||
// action was keyed on, so there is no catalog to restore and nothing
|
||||
// further to report.
|
||||
if len(merr.Errs()) > 0 {
|
||||
return merr
|
||||
}
|
||||
if err := c.parseCatalogCore(files, merr); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
c.parseCatalogLegacy(files, merr)
|
||||
}
|
||||
|
||||
if len(merr.Errs()) > 0 {
|
||||
return merr
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Compiler) parseCatalogLegacy(files []schemaFile, merr *multierr.Error) {
|
||||
for _, file := range files {
|
||||
stmts, err := c.parser.Parse(strings.NewReader(file.contents))
|
||||
if err != nil {
|
||||
// A schema file and a query file are often the same file, so a
|
||||
// query's sqlc syntax can fail here. Look for an explanation
|
||||
// before reporting the syntax error it caused.
|
||||
if reported := addSyntaxErrors(merr, file.name, file.contents, preprocess.File(c.conf.Engine, file.contents)); !reported {
|
||||
merr.Add(file.name, file.contents, 0, err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
for i := range stmts {
|
||||
if err := c.catalog.Update(stmts[i], c); err != nil {
|
||||
merr.Add(file.name, file.contents, stmts[i].Pos(), err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// parseCatalogCore builds the catalog the analysis core works against: the
|
||||
// dialect's seed with the schema's DDL applied. The two are one cached action,
|
||||
// so a run that has built this catalog before restores it and never parses the
|
||||
// schema at all.
|
||||
func (c *Compiler) parseCatalogCore(files []schemaFile, merr *multierr.Error) error {
|
||||
contents := make([]string, 0, len(files))
|
||||
for _, file := range files {
|
||||
contents = append(contents, file.contents)
|
||||
}
|
||||
|
||||
cat, err := core.NewCached(string(c.conf.Engine), contents, func(cat *core.Catalog) error {
|
||||
for _, file := range files {
|
||||
stmts, err := c.parser.Parse(strings.NewReader(file.contents))
|
||||
if err != nil {
|
||||
// A schema file and a query file are often the same file, so a
|
||||
// query's sqlc syntax can fail here. Look for an explanation
|
||||
// before reporting the syntax error it caused.
|
||||
if reported := addSyntaxErrors(merr, file.name, file.contents, preprocess.File(c.conf.Engine, file.contents)); !reported {
|
||||
merr.Add(file.name, file.contents, 0, err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
for i := range stmts {
|
||||
if err := coreschema.Apply(cat, stmts[i].Raw); err != nil {
|
||||
merr.Add(file.name, file.contents, stmts[i].Pos(), err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
// A catalog the whole schema did not make it into is not the catalog
|
||||
// this action describes, so it is not one to keep.
|
||||
if len(merr.Errs()) > 0 {
|
||||
return merr
|
||||
}
|
||||
return nil
|
||||
}, c.coreDialect)
|
||||
|
||||
if cat == nil {
|
||||
return fmt.Errorf("%s: init catalog: %w", c.conf.Engine, err)
|
||||
}
|
||||
// Whatever apply reported is already in merr, which the caller returns.
|
||||
c.coreCatalog = cat
|
||||
|
||||
// Codegen consumes the catalog through the Result, in the legacy shape.
|
||||
legacy, err := coreResultCatalog(cat)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s: dump catalog: %w", c.conf.Engine, err)
|
||||
}
|
||||
c.catalog = legacy
|
||||
return nil
|
||||
}
|
||||
|
||||
// statement is a parsed query awaiting analysis, with what error reporting
|
||||
// needs to place it back in the file it came from.
|
||||
type statement struct {
|
||||
filename string
|
||||
src string
|
||||
pp *preprocess.Result
|
||||
raw *ast.RawStmt
|
||||
}
|
||||
|
||||
func (c *Compiler) parseQueries(o opts.Parser) (*Result, error) {
|
||||
merr := multierr.New()
|
||||
files, err := sqlpath.Glob(c.conf.Queries)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var stmts []statement
|
||||
for _, filename := range files {
|
||||
blob, err := os.ReadFile(filename)
|
||||
if err != nil {
|
||||
merr.Add(filename, "", 0, err)
|
||||
continue
|
||||
}
|
||||
src := string(blob)
|
||||
|
||||
// sqlc syntax is not SQL. Rewrite it to native placeholders before the
|
||||
// engine parser ever sees the query, so parsers only handle SQL.
|
||||
pp := preprocess.File(c.conf.Engine, src)
|
||||
|
||||
parsed, err := c.parser.Parse(strings.NewReader(pp.Text))
|
||||
if err != nil {
|
||||
if reported := addSyntaxErrors(merr, filename, src, pp); !reported {
|
||||
merr.Add(filename, src, 0, err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
for _, stmt := range parsed {
|
||||
stmts = append(stmts, statement{filename: filename, src: src, pp: pp, raw: stmt.Raw})
|
||||
}
|
||||
}
|
||||
|
||||
queries, errs := c.analyzeStatements(stmts, o)
|
||||
|
||||
var q []*Query
|
||||
set := map[string]struct{}{}
|
||||
for i, stmt := range stmts {
|
||||
if err := errs[i]; err != nil {
|
||||
var e *sqlerr.Error
|
||||
loc := stmt.raw.Pos()
|
||||
if errors.As(err, &e) && e.Location != 0 {
|
||||
loc = e.Location
|
||||
}
|
||||
// Locations are reported against the rewritten query; map them
|
||||
// back so errors point at what the user wrote.
|
||||
loc = stmt.pp.Origin(loc)
|
||||
if e != nil && e.Location != 0 {
|
||||
e.Location = loc
|
||||
}
|
||||
merr.Add(stmt.filename, stmt.src, loc, err)
|
||||
// If this rpc unauthenticated error bubbles up, then all future parsing/analysis will fail
|
||||
if errors.Is(err, rpc.ErrUnauthenticated) {
|
||||
return nil, merr
|
||||
}
|
||||
continue
|
||||
}
|
||||
query := queries[i]
|
||||
if query == nil {
|
||||
continue
|
||||
}
|
||||
query.Metadata.Filename = filepath.Base(stmt.filename)
|
||||
queryName := query.Metadata.Name
|
||||
if queryName != "" {
|
||||
if _, exists := set[queryName]; exists {
|
||||
merr.Add(stmt.filename, stmt.src, stmt.pp.Origin(stmt.raw.Pos()), fmt.Errorf("duplicate query name: %s", queryName))
|
||||
continue
|
||||
}
|
||||
set[queryName] = struct{}{}
|
||||
}
|
||||
q = append(q, query)
|
||||
}
|
||||
|
||||
if len(merr.Errs()) > 0 {
|
||||
return nil, merr
|
||||
}
|
||||
if len(q) == 0 {
|
||||
return nil, fmt.Errorf("no queries contained in paths %s", strings.Join(c.conf.Queries, ","))
|
||||
}
|
||||
|
||||
return &Result{
|
||||
Catalog: c.catalog,
|
||||
Queries: q,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// analyzeStatements analyzes each statement, returning the results in the
|
||||
// order the statements were given so that queries and errors come out in
|
||||
// source order however they were produced.
|
||||
//
|
||||
// The analysis core reads the catalog and nothing else, so its statements are
|
||||
// analyzed concurrently. Every other path holds state that a second goroutine
|
||||
// would race — a database connection, the legacy catalog — and stays serial.
|
||||
func (c *Compiler) analyzeStatements(stmts []statement, o opts.Parser) ([]*Query, []error) {
|
||||
queries := make([]*Query, len(stmts))
|
||||
errs := make([]error, len(stmts))
|
||||
|
||||
analyze := func(i int) {
|
||||
queries[i], errs[i] = c.parseQuery(stmts[i].raw, stmts[i].pp, o)
|
||||
}
|
||||
|
||||
workers := runtime.GOMAXPROCS(0)
|
||||
if !c.coreAnalysis || workers < 2 || len(stmts) < 2 {
|
||||
for i := range stmts {
|
||||
analyze(i)
|
||||
}
|
||||
return queries, errs
|
||||
}
|
||||
|
||||
var g errgroup.Group
|
||||
g.SetLimit(workers)
|
||||
for i := range stmts {
|
||||
g.Go(func() error {
|
||||
analyze(i)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
g.Wait()
|
||||
return queries, errs
|
||||
}
|
||||
|
||||
func addSyntaxErrors(merr *multierr.Error, filename, src string, pp *preprocess.Result) bool {
|
||||
var found bool
|
||||
for _, stmt := range pp.Statements() {
|
||||
if stmt.Err == nil {
|
||||
continue
|
||||
}
|
||||
found = true
|
||||
loc := pp.Origin(stmt.Start)
|
||||
var e *sqlerr.Error
|
||||
if errors.As(stmt.Err, &e) && e.Location != 0 {
|
||||
loc = pp.Origin(e.Location)
|
||||
e.Location = loc
|
||||
}
|
||||
merr.Add(filename, src, loc, stmt.Err)
|
||||
}
|
||||
return found
|
||||
}
|
||||
199
internal/compiler/engine.go
Normal file
199
internal/compiler/engine.go
Normal file
@@ -0,0 +1,199 @@
|
||||
package compiler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/sqlc-dev/sqlc/internal/analyzer"
|
||||
"github.com/sqlc-dev/sqlc/internal/config"
|
||||
"github.com/sqlc-dev/sqlc/internal/core"
|
||||
"github.com/sqlc-dev/sqlc/internal/dbmanager"
|
||||
"github.com/sqlc-dev/sqlc/internal/engine/clickhouse"
|
||||
"github.com/sqlc-dev/sqlc/internal/engine/dolphin"
|
||||
"github.com/sqlc-dev/sqlc/internal/engine/duckdb"
|
||||
"github.com/sqlc-dev/sqlc/internal/engine/googlesql"
|
||||
"github.com/sqlc-dev/sqlc/internal/engine/mssql"
|
||||
"github.com/sqlc-dev/sqlc/internal/engine/postgresql"
|
||||
pganalyze "github.com/sqlc-dev/sqlc/internal/engine/postgresql/analyzer"
|
||||
"github.com/sqlc-dev/sqlc/internal/engine/sqlite"
|
||||
sqliteanalyze "github.com/sqlc-dev/sqlc/internal/engine/sqlite/analyzer"
|
||||
"github.com/sqlc-dev/sqlc/internal/opts"
|
||||
"github.com/sqlc-dev/sqlc/internal/sql/catalog"
|
||||
)
|
||||
|
||||
type Compiler struct {
|
||||
conf config.SQL
|
||||
combo config.CombinedSettings
|
||||
catalog *catalog.Catalog
|
||||
parser Parser
|
||||
result *Result
|
||||
analyzer analyzer.Analyzer
|
||||
client dbmanager.Client
|
||||
selector selector
|
||||
|
||||
coreCatalog *core.Catalog
|
||||
|
||||
// coreAnalysis routes every engine through the core catalog and analyzer,
|
||||
// and coreDialect seeds the catalog it does so with. The catalog itself is
|
||||
// not built until the schema is loaded, because the two are cached
|
||||
// together.
|
||||
coreAnalysis bool
|
||||
coreDialect core.Option
|
||||
|
||||
// newParser builds a parser for the configured engine, and is set for
|
||||
// every engine either path supports. The core path analyzes statements
|
||||
// concurrently, and a parser holds enough state that two goroutines cannot
|
||||
// share one, so a goroutine that has to parse something of its own — the
|
||||
// query text it just rewrote — builds its own rather than taking c.parser.
|
||||
newParser func() Parser
|
||||
|
||||
schema []string
|
||||
}
|
||||
|
||||
// Option configures a Compiler.
|
||||
type Option func(*Compiler)
|
||||
|
||||
// WithCoreAnalysis analyzes queries with the core catalog and analyzer rather
|
||||
// than each engine's own analysis path. ClickHouse and GoogleSQL always do;
|
||||
// this opts the remaining engines in.
|
||||
func WithCoreAnalysis() Option {
|
||||
return func(c *Compiler) { c.coreAnalysis = true }
|
||||
}
|
||||
|
||||
func NewCompiler(conf config.SQL, combo config.CombinedSettings, parserOpts opts.Parser, options ...Option) (*Compiler, error) {
|
||||
c := &Compiler{conf: conf, combo: combo}
|
||||
for _, o := range options {
|
||||
o(c)
|
||||
}
|
||||
|
||||
// ClickHouse, GoogleSQL, SQL Server and DuckDB have no legacy analysis
|
||||
// path to fall back to.
|
||||
switch conf.Engine {
|
||||
case config.EngineClickHouse, config.EngineGoogleSQL, config.EngineMSSQL, config.EngineDuckDB:
|
||||
c.coreAnalysis = true
|
||||
}
|
||||
if c.coreAnalysis {
|
||||
if err := c.initCore(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
if conf.Database != nil && conf.Database.Managed {
|
||||
client := dbmanager.NewClient(combo.Global.Servers)
|
||||
c.client = client
|
||||
}
|
||||
|
||||
switch conf.Engine {
|
||||
case config.EngineSQLite:
|
||||
c.newParser = func() Parser { return sqlite.NewParser() }
|
||||
c.catalog = sqlite.NewCatalog()
|
||||
c.selector = newSQLiteSelector()
|
||||
|
||||
if conf.Database != nil {
|
||||
if conf.Analyzer.Database == nil || *conf.Analyzer.Database {
|
||||
c.analyzer = analyzer.Cached(
|
||||
sqliteanalyze.New(*conf.Database),
|
||||
combo.Global,
|
||||
*conf.Database,
|
||||
)
|
||||
}
|
||||
}
|
||||
case config.EngineMySQL:
|
||||
c.newParser = func() Parser { return dolphin.NewParser() }
|
||||
c.catalog = dolphin.NewCatalog()
|
||||
c.selector = newDefaultSelector()
|
||||
case config.EnginePostgreSQL:
|
||||
c.newParser = func() Parser { return postgresql.NewParser() }
|
||||
c.catalog = postgresql.NewCatalog()
|
||||
c.selector = newDefaultSelector()
|
||||
|
||||
if conf.Database != nil {
|
||||
if conf.Analyzer.Database == nil || *conf.Analyzer.Database {
|
||||
c.analyzer = analyzer.Cached(
|
||||
pganalyze.New(c.client, *conf.Database),
|
||||
combo.Global,
|
||||
*conf.Database,
|
||||
)
|
||||
}
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown engine: %s", conf.Engine)
|
||||
}
|
||||
c.parser = c.newParser()
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// initCore wires up the engine's parser and a core catalog seeded with its
|
||||
// dialect. No legacy catalog and no analyzer connection are involved.
|
||||
func (c *Compiler) initCore() error {
|
||||
var dialect core.Option
|
||||
switch c.conf.Engine {
|
||||
case config.EngineSQLite:
|
||||
c.newParser = func() Parser { return sqlite.NewParser() }
|
||||
c.selector = newSQLiteSelector()
|
||||
dialect = sqlite.Dialect()
|
||||
case config.EngineMySQL:
|
||||
c.newParser = func() Parser { return dolphin.NewParser() }
|
||||
c.selector = newDefaultSelector()
|
||||
dialect = dolphin.Dialect()
|
||||
case config.EnginePostgreSQL:
|
||||
c.newParser = func() Parser { return postgresql.NewParser() }
|
||||
c.selector = newDefaultSelector()
|
||||
dialect = postgresql.Dialect()
|
||||
case config.EngineClickHouse:
|
||||
c.newParser = func() Parser { return clickhouse.NewParser() }
|
||||
c.selector = newDefaultSelector()
|
||||
dialect = clickhouse.Dialect()
|
||||
case config.EngineGoogleSQL:
|
||||
c.newParser = func() Parser { return googlesql.NewParser() }
|
||||
c.selector = newDefaultSelector()
|
||||
dialect = googlesql.Dialect()
|
||||
case config.EngineMSSQL:
|
||||
c.newParser = func() Parser { return mssql.NewParser() }
|
||||
c.selector = newDefaultSelector()
|
||||
dialect = mssql.Dialect()
|
||||
case config.EngineDuckDB:
|
||||
c.newParser = func() Parser { return duckdb.NewParser() }
|
||||
c.selector = newDefaultSelector()
|
||||
dialect = duckdb.Dialect()
|
||||
default:
|
||||
return fmt.Errorf("unknown engine: %s", c.conf.Engine)
|
||||
}
|
||||
c.parser = c.newParser()
|
||||
c.coreDialect = dialect
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Compiler) Catalog() *catalog.Catalog {
|
||||
return c.catalog
|
||||
}
|
||||
|
||||
func (c *Compiler) ParseCatalog(schema []string) error {
|
||||
return c.parseCatalog(schema)
|
||||
}
|
||||
|
||||
func (c *Compiler) ParseQueries(queries []string, o opts.Parser) error {
|
||||
r, err := c.parseQueries(o)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.result = r
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Compiler) Result() *Result {
|
||||
return c.result
|
||||
}
|
||||
|
||||
func (c *Compiler) Close(ctx context.Context) {
|
||||
if c.analyzer != nil {
|
||||
c.analyzer.Close(ctx)
|
||||
}
|
||||
if c.client != nil {
|
||||
c.client.Close(ctx)
|
||||
}
|
||||
if c.coreCatalog != nil {
|
||||
c.coreCatalog.Close()
|
||||
}
|
||||
}
|
||||
211
internal/compiler/expand.go
Normal file
211
internal/compiler/expand.go
Normal file
@@ -0,0 +1,211 @@
|
||||
package compiler
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/sqlc-dev/sqlc/internal/config"
|
||||
"github.com/sqlc-dev/sqlc/internal/source"
|
||||
"github.com/sqlc-dev/sqlc/internal/sql/ast"
|
||||
"github.com/sqlc-dev/sqlc/internal/sql/astutils"
|
||||
)
|
||||
|
||||
func (c *Compiler) expand(qc *QueryCatalog, raw *ast.RawStmt) ([]source.Edit, error) {
|
||||
// Return early if there are no A_Star nodes to expand
|
||||
stars := astutils.Search(raw, func(node ast.Node) bool {
|
||||
_, ok := node.(*ast.A_Star)
|
||||
return ok
|
||||
})
|
||||
if len(stars.Items) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
list := astutils.Search(raw, func(node ast.Node) bool {
|
||||
switch node.(type) {
|
||||
case *ast.DeleteStmt:
|
||||
case *ast.InsertStmt:
|
||||
case *ast.SelectStmt:
|
||||
case *ast.UpdateStmt:
|
||||
default:
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
if len(list.Items) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
var edits []source.Edit
|
||||
for _, item := range list.Items {
|
||||
edit, err := c.expandStmt(qc, raw, item)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
edits = append(edits, edit...)
|
||||
}
|
||||
return edits, nil
|
||||
}
|
||||
|
||||
var validPostgresIdent = regexp.MustCompile(`^[a-z_][a-z0-9_$]*$`)
|
||||
|
||||
func (c *Compiler) quoteIdent(ident string) string {
|
||||
if c.parser.IsReservedKeyword(ident) {
|
||||
return c.quote(ident)
|
||||
}
|
||||
// SQL identifiers and key words must begin with a letter (a-z, but also
|
||||
// letters with diacritical marks and non-Latin letters) or an underscore
|
||||
// (_). Subsequent characters in an identifier or key word can be letters,
|
||||
// underscores, digits (0-9), or dollar signs ($).
|
||||
//
|
||||
// https://www.postgresql.org/docs/current/sql-syntax-lexical.html#SQL-SYNTAX-IDENTIFIERS
|
||||
if c.conf.Engine == config.EnginePostgreSQL {
|
||||
// camelCase means the column is also camelCase
|
||||
if strings.ToLower(ident) != ident {
|
||||
return c.quote(ident)
|
||||
}
|
||||
if !validPostgresIdent.MatchString(strings.ToLower(ident)) {
|
||||
return c.quote(ident)
|
||||
}
|
||||
}
|
||||
return ident
|
||||
}
|
||||
|
||||
func (c *Compiler) quote(x string) string {
|
||||
switch c.conf.Engine {
|
||||
case config.EngineMySQL:
|
||||
return "`" + x + "`"
|
||||
default:
|
||||
return "\"" + x + "\""
|
||||
}
|
||||
}
|
||||
|
||||
// starOldFunc measures how much of the query text a star reference occupies,
|
||||
// so an edit replaces the reference and nothing else. Each part is measured
|
||||
// both bare and quoted: an embed was rewritten to "table.*" in the query text,
|
||||
// preserving the way the user quoted the table, so it is measured the same way
|
||||
// as a star reference the user wrote.
|
||||
func (c *Compiler) starOldFunc(parts []string) func(string) int {
|
||||
old := make([]string, 0, len(parts))
|
||||
for _, p := range parts {
|
||||
if p == "*" {
|
||||
old = append(old, p)
|
||||
} else {
|
||||
old = append(old, c.quoteIdent(p))
|
||||
}
|
||||
}
|
||||
return func(s string) int {
|
||||
length := 0
|
||||
for i, o := range old {
|
||||
if hasSeparator := i > 0; hasSeparator {
|
||||
length++
|
||||
}
|
||||
if strings.HasPrefix(s[length:], o) {
|
||||
length += len(o)
|
||||
} else if quoted := c.quote(o); strings.HasPrefix(s[length:], quoted) {
|
||||
length += len(quoted)
|
||||
} else {
|
||||
length += len(o)
|
||||
}
|
||||
}
|
||||
return length
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Compiler) expandStmt(qc *QueryCatalog, raw *ast.RawStmt, node ast.Node) ([]source.Edit, error) {
|
||||
tables, err := c.sourceTables(qc, node)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Virtual tables for the OLD and NEW aliases available in a RETURNING
|
||||
// clause (PostgreSQL 18)
|
||||
rtables, err := c.returningTables(qc, node)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var targets *ast.List
|
||||
switch n := node.(type) {
|
||||
case *ast.DeleteStmt:
|
||||
targets = n.ReturningList
|
||||
case *ast.InsertStmt:
|
||||
targets = n.ReturningList
|
||||
case *ast.SelectStmt:
|
||||
targets = n.TargetList
|
||||
case *ast.UpdateStmt:
|
||||
targets = n.ReturningList
|
||||
default:
|
||||
return nil, fmt.Errorf("outputColumns: unsupported node type: %T", n)
|
||||
}
|
||||
|
||||
var edits []source.Edit
|
||||
for _, target := range targets.Items {
|
||||
res, ok := target.(*ast.ResTarget)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
ref, ok := res.Val.(*ast.ColumnRef)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if !hasStarRef(ref) {
|
||||
continue
|
||||
}
|
||||
var parts, cols []string
|
||||
for _, f := range ref.Fields.Items {
|
||||
switch field := f.(type) {
|
||||
case *ast.String:
|
||||
parts = append(parts, field.Str)
|
||||
case *ast.A_Star:
|
||||
parts = append(parts, "*")
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown field in ColumnRef: %T", f)
|
||||
}
|
||||
}
|
||||
scope := astutils.Join(ref.Fields, ".")
|
||||
counts := map[string]int{}
|
||||
if scope == "" {
|
||||
for _, t := range tables {
|
||||
for _, c := range t.Columns {
|
||||
counts[c.Name] += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
starTables := tables
|
||||
if vt := returningTableForScope(tables, rtables, scope); vt != nil {
|
||||
starTables = []*Table{vt}
|
||||
}
|
||||
for _, t := range starTables {
|
||||
if scope != "" && scope != t.Rel.Name {
|
||||
continue
|
||||
}
|
||||
tableName := c.quoteIdent(t.Rel.Name)
|
||||
scopeName := c.quoteIdent(scope)
|
||||
for _, column := range t.Columns {
|
||||
cname := column.Name
|
||||
if res.Name != nil {
|
||||
cname = *res.Name
|
||||
}
|
||||
cname = c.quoteIdent(cname)
|
||||
if scope != "" {
|
||||
cname = scopeName + "." + cname
|
||||
}
|
||||
if counts[cname] > 1 {
|
||||
cname = tableName + "." + cname
|
||||
}
|
||||
|
||||
// This is important for SQLite in particular which needs to
|
||||
// wrap jsonb column values with `json(colname)` so they're in a
|
||||
// publicly usable format (i.e. not jsonb).
|
||||
cname = c.selector.ColumnExpr(cname, column)
|
||||
cols = append(cols, cname)
|
||||
}
|
||||
}
|
||||
edits = append(edits, source.Edit{
|
||||
Location: res.Location - raw.StmtLocation,
|
||||
OldFunc: c.starOldFunc(parts),
|
||||
New: strings.Join(cols, ", "),
|
||||
})
|
||||
}
|
||||
|
||||
return edits, nil
|
||||
}
|
||||
76
internal/compiler/expand_core.go
Normal file
76
internal/compiler/expand_core.go
Normal file
@@ -0,0 +1,76 @@
|
||||
package compiler
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/sqlc-dev/sqlc/internal/core"
|
||||
"github.com/sqlc-dev/sqlc/internal/source"
|
||||
"github.com/sqlc-dev/sqlc/internal/sql/ast"
|
||||
)
|
||||
|
||||
// expandCore rewrites the stars in a query's text with the columns the core
|
||||
// analyzer resolved them to. The analyzer has already walked the statement and
|
||||
// its subqueries, so there is nothing left to look up here: what remains is
|
||||
// deciding how each name is written, which is the engine's business and not
|
||||
// the core's.
|
||||
func (c *Compiler) expandCore(raw *ast.RawStmt, stars []core.StarExpansion) []source.Edit {
|
||||
if len(stars) == 0 {
|
||||
return nil
|
||||
}
|
||||
edits := make([]source.Edit, 0, len(stars))
|
||||
seen := make(map[int]bool, len(stars))
|
||||
for _, star := range stars {
|
||||
// The analyzer types an expression again when a later clause refers to
|
||||
// the output name it was given, so "SELECT (SELECT * FROM t) AS x ...
|
||||
// GROUP BY x" reports the star in the subquery once per pass. The
|
||||
// passes agree on what the star covers, and editing the same reference
|
||||
// twice would overlap, so only the first is kept.
|
||||
if seen[star.Location] {
|
||||
continue
|
||||
}
|
||||
seen[star.Location] = true
|
||||
|
||||
// Everything before the star qualifies it: "foo.*" is scoped to foo,
|
||||
// while a bare "*" covers every relation in the FROM clause.
|
||||
scope := strings.Join(star.Fields[:len(star.Fields)-1], ".")
|
||||
|
||||
// An unqualified star that covers more than one relation may name the
|
||||
// same column twice, so those are written with their relation.
|
||||
counts := map[string]int{}
|
||||
if scope == "" {
|
||||
for _, col := range star.Columns {
|
||||
counts[col.Name]++
|
||||
}
|
||||
}
|
||||
|
||||
cols := make([]string, 0, len(star.Columns))
|
||||
for _, col := range star.Columns {
|
||||
cname := col.Name
|
||||
if star.Alias != "" {
|
||||
cname = star.Alias
|
||||
}
|
||||
cname = c.quoteIdent(cname)
|
||||
if scope != "" {
|
||||
cname = c.quoteIdent(scope) + "." + cname
|
||||
}
|
||||
if counts[cname] > 1 {
|
||||
cname = c.quoteIdent(col.Relation) + "." + cname
|
||||
}
|
||||
|
||||
// This is important for SQLite in particular which needs to wrap
|
||||
// jsonb column values with `json(colname)` so they're in a publicly
|
||||
// usable format (i.e. not jsonb).
|
||||
cols = append(cols, c.selector.ColumnExpr(cname, &Column{
|
||||
Name: col.Name,
|
||||
DataType: col.DataType,
|
||||
}))
|
||||
}
|
||||
|
||||
edits = append(edits, source.Edit{
|
||||
Location: star.Location - raw.StmtLocation,
|
||||
OldFunc: c.starOldFunc(star.Fields),
|
||||
New: strings.Join(cols, ", "),
|
||||
})
|
||||
}
|
||||
return edits
|
||||
}
|
||||
214
internal/compiler/find_params.go
Normal file
214
internal/compiler/find_params.go
Normal file
@@ -0,0 +1,214 @@
|
||||
package compiler
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/sqlc-dev/sqlc/internal/sql/ast"
|
||||
"github.com/sqlc-dev/sqlc/internal/sql/astutils"
|
||||
)
|
||||
|
||||
func findParameters(root ast.Node) ([]paramRef, []error) {
|
||||
refs := make([]paramRef, 0)
|
||||
errors := make([]error, 0)
|
||||
v := paramSearch{seen: make(map[int]struct{}), refs: &refs, errs: &errors}
|
||||
astutils.Walk(v, root)
|
||||
if len(*v.errs) > 0 {
|
||||
return refs, *v.errs
|
||||
} else {
|
||||
return refs, nil
|
||||
}
|
||||
}
|
||||
|
||||
type paramRef struct {
|
||||
parent ast.Node
|
||||
rv *ast.RangeVar
|
||||
ref *ast.ParamRef
|
||||
name string // Named parameter support
|
||||
}
|
||||
|
||||
type paramSearch struct {
|
||||
parent ast.Node
|
||||
rangeVar *ast.RangeVar
|
||||
refs *[]paramRef
|
||||
seen map[int]struct{}
|
||||
errs *[]error
|
||||
|
||||
// XXX: Gross state hack for limit
|
||||
limitCount ast.Node
|
||||
limitOffset ast.Node
|
||||
}
|
||||
|
||||
type limitCount struct {
|
||||
}
|
||||
|
||||
func (l *limitCount) Pos() int {
|
||||
return 0
|
||||
}
|
||||
|
||||
type limitOffset struct {
|
||||
}
|
||||
|
||||
func (l *limitOffset) Pos() int {
|
||||
return 0
|
||||
}
|
||||
|
||||
func (p paramSearch) Visit(node ast.Node) astutils.Visitor {
|
||||
switch n := node.(type) {
|
||||
|
||||
case *ast.A_Expr:
|
||||
p.parent = node
|
||||
|
||||
case *ast.BetweenExpr:
|
||||
p.parent = node
|
||||
|
||||
case *ast.CallStmt:
|
||||
p.parent = n.FuncCall
|
||||
|
||||
case *ast.DeleteStmt:
|
||||
if n.LimitCount != nil {
|
||||
p.limitCount = n.LimitCount
|
||||
}
|
||||
|
||||
case *ast.FuncCall:
|
||||
p.parent = node
|
||||
|
||||
case *ast.InsertStmt:
|
||||
if s, ok := n.SelectStmt.(*ast.SelectStmt); ok {
|
||||
for i, item := range s.TargetList.Items {
|
||||
target, ok := item.(*ast.ResTarget)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
ref, ok := target.Val.(*ast.ParamRef)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if len(n.Cols.Items) <= i {
|
||||
*p.errs = append(*p.errs, fmt.Errorf("INSERT has more expressions than target columns"))
|
||||
return p
|
||||
}
|
||||
*p.refs = append(*p.refs, paramRef{parent: n.Cols.Items[i], ref: ref, rv: n.Relation})
|
||||
p.seen[ref.Location] = struct{}{}
|
||||
}
|
||||
for _, item := range s.ValuesLists.Items {
|
||||
vl, ok := item.(*ast.List)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
for i, v := range vl.Items {
|
||||
ref, ok := v.(*ast.ParamRef)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if len(n.Cols.Items) <= i {
|
||||
*p.errs = append(*p.errs, fmt.Errorf("INSERT has more expressions than target columns"))
|
||||
return p
|
||||
}
|
||||
*p.refs = append(*p.refs, paramRef{parent: n.Cols.Items[i], ref: ref, rv: n.Relation})
|
||||
p.seen[ref.Location] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
case *ast.UpdateStmt:
|
||||
for _, item := range n.TargetList.Items {
|
||||
target, ok := item.(*ast.ResTarget)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
ref, ok := target.Val.(*ast.ParamRef)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
for _, relation := range n.Relations.Items {
|
||||
rv, ok := relation.(*ast.RangeVar)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
*p.refs = append(*p.refs, paramRef{parent: target, ref: ref, rv: rv})
|
||||
}
|
||||
p.seen[ref.Location] = struct{}{}
|
||||
}
|
||||
if n.LimitCount != nil {
|
||||
p.limitCount = n.LimitCount
|
||||
}
|
||||
|
||||
case *ast.RangeVar:
|
||||
p.rangeVar = n
|
||||
|
||||
case *ast.ResTarget:
|
||||
p.parent = node
|
||||
|
||||
case *ast.SelectStmt:
|
||||
if n.LimitCount != nil {
|
||||
p.limitCount = n.LimitCount
|
||||
}
|
||||
if n.LimitOffset != nil {
|
||||
p.limitOffset = n.LimitOffset
|
||||
}
|
||||
|
||||
case *ast.TypeCast:
|
||||
p.parent = node
|
||||
|
||||
case *ast.ParamRef:
|
||||
parent := p.parent
|
||||
|
||||
if count, ok := p.limitCount.(*ast.ParamRef); ok {
|
||||
if n.Number == count.Number {
|
||||
parent = &limitCount{}
|
||||
}
|
||||
}
|
||||
|
||||
if offset, ok := p.limitOffset.(*ast.ParamRef); ok {
|
||||
if n.Number == offset.Number {
|
||||
parent = &limitOffset{}
|
||||
}
|
||||
}
|
||||
if _, found := p.seen[n.Location]; found {
|
||||
break
|
||||
}
|
||||
|
||||
// Special, terrible case for *ast.MultiAssignRef
|
||||
set := true
|
||||
if res, ok := parent.(*ast.ResTarget); ok {
|
||||
if multi, ok := res.Val.(*ast.MultiAssignRef); ok {
|
||||
set = false
|
||||
if row, ok := multi.Source.(*ast.RowExpr); ok {
|
||||
for i, arg := range row.Args.Items {
|
||||
if ref, ok := arg.(*ast.ParamRef); ok {
|
||||
if multi.Colno == i+1 && ref.Number == n.Number {
|
||||
set = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if set {
|
||||
*p.refs = append(*p.refs, paramRef{parent: parent, ref: n, rv: p.rangeVar})
|
||||
p.seen[n.Location] = struct{}{}
|
||||
}
|
||||
return nil
|
||||
|
||||
case *ast.In:
|
||||
if n.Sel == nil {
|
||||
p.parent = node
|
||||
} else {
|
||||
if sel, ok := n.Sel.(*ast.SelectStmt); ok && sel.FromClause != nil && len(sel.FromClause.Items) > 0 {
|
||||
from := sel.FromClause
|
||||
if schema, ok := from.Items[0].(*ast.RangeVar); ok && schema != nil {
|
||||
p.rangeVar = &ast.RangeVar{
|
||||
Catalogname: schema.Catalogname,
|
||||
Schemaname: schema.Schemaname,
|
||||
Relname: schema.Relname,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if _, ok := n.Expr.(*ast.ParamRef); ok {
|
||||
p.Visit(n.Expr)
|
||||
}
|
||||
}
|
||||
return p
|
||||
}
|
||||
812
internal/compiler/output_columns.go
Normal file
812
internal/compiler/output_columns.go
Normal file
@@ -0,0 +1,812 @@
|
||||
package compiler
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/sqlc-dev/sqlc/internal/config"
|
||||
"github.com/sqlc-dev/sqlc/internal/sql/ast"
|
||||
"github.com/sqlc-dev/sqlc/internal/sql/astutils"
|
||||
"github.com/sqlc-dev/sqlc/internal/sql/catalog"
|
||||
"github.com/sqlc-dev/sqlc/internal/sql/lang"
|
||||
"github.com/sqlc-dev/sqlc/internal/sql/sqlerr"
|
||||
)
|
||||
|
||||
// OutputColumns determines which columns a statement will output
|
||||
func (c *Compiler) OutputColumns(stmt ast.Node) ([]*catalog.Column, error) {
|
||||
qc, err := c.buildQueryCatalog(c.catalog, stmt, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cols, err := c.outputColumns(qc, stmt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
catCols := make([]*catalog.Column, 0, len(cols))
|
||||
for _, col := range cols {
|
||||
catCols = append(catCols, &catalog.Column{
|
||||
Name: col.Name,
|
||||
Type: ast.TypeName{Name: col.DataType},
|
||||
IsNotNull: col.NotNull,
|
||||
IsUnsigned: col.Unsigned,
|
||||
IsArray: col.IsArray,
|
||||
ArrayDims: col.ArrayDims,
|
||||
Comment: col.Comment,
|
||||
Length: col.Length,
|
||||
})
|
||||
}
|
||||
return catCols, nil
|
||||
}
|
||||
|
||||
func hasStarRef(cf *ast.ColumnRef) bool {
|
||||
for _, item := range cf.Fields.Items {
|
||||
if _, ok := item.(*ast.A_Star); ok {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Compute the output columns for a statement.
|
||||
//
|
||||
// Return an error if column references are ambiguous
|
||||
// Return an error if column references don't exist
|
||||
func (c *Compiler) outputColumns(qc *QueryCatalog, node ast.Node) ([]*Column, error) {
|
||||
tables, err := c.sourceTables(qc, node)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Virtual tables for the OLD and NEW aliases available in a RETURNING
|
||||
// clause (PostgreSQL 18)
|
||||
rtables, err := c.returningTables(qc, node)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
targets := &ast.List{}
|
||||
switch n := node.(type) {
|
||||
case *ast.DeleteStmt:
|
||||
targets = n.ReturningList
|
||||
case *ast.InsertStmt:
|
||||
targets = n.ReturningList
|
||||
case *ast.SelectStmt:
|
||||
targets = n.TargetList
|
||||
isUnion := len(targets.Items) == 0 && n.Larg != nil
|
||||
|
||||
if n.GroupClause != nil {
|
||||
for _, item := range n.GroupClause.Items {
|
||||
if err := c.findColumnForNode(item, tables, targets); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
validateOrderBy := true
|
||||
if c.conf.StrictOrderBy != nil {
|
||||
validateOrderBy = *c.conf.StrictOrderBy
|
||||
}
|
||||
if !isUnion && validateOrderBy {
|
||||
if n.SortClause != nil {
|
||||
for _, item := range n.SortClause.Items {
|
||||
sb, ok := item.(*ast.SortBy)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if err := c.findColumnForNode(sb.Node, tables, targets); err != nil {
|
||||
return nil, fmt.Errorf("%v: if you want to skip this validation, set 'strict_order_by' to false", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
if n.WindowClause != nil {
|
||||
for _, item := range n.WindowClause.Items {
|
||||
sb, ok := item.(*ast.List)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
for _, single := range sb.Items {
|
||||
caseExpr, ok := single.(*ast.CaseExpr)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if err := c.findColumnForNode(caseExpr.Xpr, tables, targets); err != nil {
|
||||
return nil, fmt.Errorf("%v: if you want to skip this validation, set 'strict_order_by' to false", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// For UNION queries, targets is empty and we need to look for the
|
||||
// columns in Largs.
|
||||
if isUnion {
|
||||
return c.outputColumns(qc, n.Larg)
|
||||
}
|
||||
case *ast.UpdateStmt:
|
||||
targets = n.ReturningList
|
||||
}
|
||||
|
||||
var cols []*Column
|
||||
|
||||
for _, target := range targets.Items {
|
||||
res, ok := target.(*ast.ResTarget)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
switch n := res.Val.(type) {
|
||||
|
||||
case *ast.A_Const:
|
||||
name := ""
|
||||
if res.Name != nil {
|
||||
name = *res.Name
|
||||
}
|
||||
switch n.Val.(type) {
|
||||
case *ast.String:
|
||||
cols = append(cols, &Column{Name: name, DataType: "text", NotNull: true})
|
||||
case *ast.Integer:
|
||||
cols = append(cols, &Column{Name: name, DataType: "int", NotNull: true})
|
||||
case *ast.Float:
|
||||
cols = append(cols, &Column{Name: name, DataType: "float", NotNull: true})
|
||||
case *ast.Boolean:
|
||||
cols = append(cols, &Column{Name: name, DataType: "bool", NotNull: true})
|
||||
default:
|
||||
cols = append(cols, &Column{Name: name, DataType: "any", NotNull: false})
|
||||
}
|
||||
|
||||
case *ast.A_Expr:
|
||||
name := ""
|
||||
if res.Name != nil {
|
||||
name = *res.Name
|
||||
}
|
||||
switch op := astutils.Join(n.Name, ""); {
|
||||
case lang.IsComparisonOperator(op):
|
||||
// TODO: Generate a name for these operations
|
||||
cols = append(cols, &Column{Name: name, DataType: "bool", NotNull: true})
|
||||
case lang.IsMathematicalOperator(op):
|
||||
cols = append(cols, &Column{Name: name, DataType: "int", NotNull: true})
|
||||
default:
|
||||
cols = append(cols, &Column{Name: name, DataType: "any", NotNull: false})
|
||||
}
|
||||
|
||||
case *ast.BoolExpr:
|
||||
name := ""
|
||||
if res.Name != nil {
|
||||
name = *res.Name
|
||||
}
|
||||
notNull := false
|
||||
if len(n.Args.Items) == 1 {
|
||||
switch n.Boolop {
|
||||
case ast.BoolExprTypeIsNull, ast.BoolExprTypeIsNotNull:
|
||||
notNull = true
|
||||
case ast.BoolExprTypeNot:
|
||||
sublink, ok := n.Args.Items[0].(*ast.SubLink)
|
||||
if ok && sublink.SubLinkType == ast.EXISTS_SUBLINK {
|
||||
notNull = true
|
||||
if name == "" {
|
||||
name = "not_exists"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
cols = append(cols, &Column{Name: name, DataType: "bool", NotNull: notNull})
|
||||
|
||||
case *ast.CaseExpr:
|
||||
name := ""
|
||||
if res.Name != nil {
|
||||
name = *res.Name
|
||||
}
|
||||
// TODO: The TypeCase and A_Const code has been copied from below. Instead, we
|
||||
// need a recurse function to get the type of a node.
|
||||
if tc, ok := n.Defresult.(*ast.TypeCast); ok {
|
||||
if tc.TypeName == nil {
|
||||
return nil, errors.New("no type name type cast")
|
||||
}
|
||||
name := ""
|
||||
if ref, ok := tc.Arg.(*ast.ColumnRef); ok {
|
||||
name = astutils.Join(ref.Fields, "_")
|
||||
}
|
||||
if res.Name != nil {
|
||||
name = *res.Name
|
||||
}
|
||||
// TODO Validate column names
|
||||
col := toColumn(tc.TypeName)
|
||||
col.Name = name
|
||||
cols = append(cols, col)
|
||||
} else if aconst, ok := n.Defresult.(*ast.A_Const); ok {
|
||||
switch aconst.Val.(type) {
|
||||
case *ast.String:
|
||||
cols = append(cols, &Column{Name: name, DataType: "text", NotNull: true})
|
||||
case *ast.Integer:
|
||||
cols = append(cols, &Column{Name: name, DataType: "int", NotNull: true})
|
||||
case *ast.Float:
|
||||
cols = append(cols, &Column{Name: name, DataType: "float", NotNull: true})
|
||||
case *ast.Boolean:
|
||||
cols = append(cols, &Column{Name: name, DataType: "bool", NotNull: true})
|
||||
default:
|
||||
cols = append(cols, &Column{Name: name, DataType: "any", NotNull: false})
|
||||
}
|
||||
} else {
|
||||
cols = append(cols, &Column{Name: name, DataType: "any", NotNull: false})
|
||||
}
|
||||
|
||||
case *ast.CoalesceExpr:
|
||||
name := "coalesce"
|
||||
if res.Name != nil {
|
||||
name = *res.Name
|
||||
}
|
||||
var firstColumn *Column
|
||||
var shouldNotBeNull bool
|
||||
for _, arg := range n.Args.Items {
|
||||
if _, ok := arg.(*ast.A_Const); ok {
|
||||
shouldNotBeNull = true
|
||||
continue
|
||||
}
|
||||
if ref, ok := arg.(*ast.ColumnRef); ok {
|
||||
columns, err := outputColumnRefs(res, tablesForRef(ref, tables, rtables), ref)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, c := range columns {
|
||||
if firstColumn == nil {
|
||||
firstColumn = c
|
||||
}
|
||||
shouldNotBeNull = shouldNotBeNull || c.NotNull
|
||||
}
|
||||
}
|
||||
}
|
||||
if firstColumn != nil {
|
||||
firstColumn.NotNull = shouldNotBeNull
|
||||
firstColumn.skipTableRequiredCheck = true
|
||||
cols = append(cols, firstColumn)
|
||||
} else {
|
||||
cols = append(cols, &Column{Name: name, DataType: "any", NotNull: false})
|
||||
}
|
||||
|
||||
case *ast.ColumnRef:
|
||||
if hasStarRef(n) {
|
||||
|
||||
// add a column with a reference to an embedded table
|
||||
if embed, ok := qc.embeds.Find(n.Location, res.Location); ok {
|
||||
cols = append(cols, &Column{
|
||||
Name: embed.Table.Name,
|
||||
EmbedTable: embed.Table,
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
// TODO: This code is copied in func expand()
|
||||
scope := astutils.Join(n.Fields, ".")
|
||||
starTables := tables
|
||||
if vt := returningTableForScope(tables, rtables, scope); vt != nil {
|
||||
starTables = []*Table{vt}
|
||||
}
|
||||
for _, t := range starTables {
|
||||
if scope != "" && scope != t.Rel.Name {
|
||||
continue
|
||||
}
|
||||
for _, c := range t.Columns {
|
||||
cname := c.Name
|
||||
if res.Name != nil {
|
||||
cname = *res.Name
|
||||
}
|
||||
cols = append(cols, &Column{
|
||||
Name: cname,
|
||||
OriginalName: c.Name,
|
||||
Type: c.Type,
|
||||
Scope: scope,
|
||||
Table: c.Table,
|
||||
TableAlias: t.Rel.Name,
|
||||
DataType: c.DataType,
|
||||
NotNull: c.NotNull,
|
||||
Unsigned: c.Unsigned,
|
||||
IsArray: c.IsArray,
|
||||
ArrayDims: c.ArrayDims,
|
||||
Length: c.Length,
|
||||
})
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
columns, err := outputColumnRefs(res, tablesForRef(n, tables, rtables), n)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cols = append(cols, columns...)
|
||||
|
||||
case *ast.FuncCall:
|
||||
rel := n.Func
|
||||
name := rel.Name
|
||||
if res.Name != nil {
|
||||
name = *res.Name
|
||||
}
|
||||
fun, err := qc.catalog.ResolveFuncCall(n)
|
||||
if err == nil {
|
||||
cols = append(cols, &Column{
|
||||
Name: name,
|
||||
DataType: dataType(fun.ReturnType),
|
||||
NotNull: !fun.ReturnTypeNullable,
|
||||
IsFuncCall: true,
|
||||
})
|
||||
} else {
|
||||
cols = append(cols, &Column{
|
||||
Name: name,
|
||||
DataType: "any",
|
||||
IsFuncCall: true,
|
||||
})
|
||||
}
|
||||
|
||||
case *ast.SubLink:
|
||||
name := "exists"
|
||||
if res.Name != nil {
|
||||
name = *res.Name
|
||||
}
|
||||
switch n.SubLinkType {
|
||||
case ast.EXISTS_SUBLINK:
|
||||
cols = append(cols, &Column{Name: name, DataType: "bool", NotNull: true})
|
||||
case ast.EXPR_SUBLINK:
|
||||
subcols, err := c.outputColumns(qc, n.Subselect)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
first := subcols[0]
|
||||
if res.Name != nil {
|
||||
first.Name = *res.Name
|
||||
}
|
||||
cols = append(cols, first)
|
||||
default:
|
||||
cols = append(cols, &Column{Name: name, DataType: "any", NotNull: false})
|
||||
}
|
||||
|
||||
case *ast.TypeCast:
|
||||
if n.TypeName == nil {
|
||||
return nil, errors.New("no type name type cast")
|
||||
}
|
||||
name := ""
|
||||
if ref, ok := n.Arg.(*ast.ColumnRef); ok {
|
||||
name = astutils.Join(ref.Fields, "_")
|
||||
}
|
||||
if res.Name != nil {
|
||||
name = *res.Name
|
||||
}
|
||||
// TODO Validate column names
|
||||
col := toColumn(n.TypeName)
|
||||
col.Name = name
|
||||
// TODO Add correct, real type inference
|
||||
if constant, ok := n.Arg.(*ast.A_Const); ok {
|
||||
if _, ok := constant.Val.(*ast.Null); ok {
|
||||
col.NotNull = false
|
||||
}
|
||||
}
|
||||
cols = append(cols, col)
|
||||
|
||||
case *ast.SelectStmt:
|
||||
subcols, err := c.outputColumns(qc, n)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
first := subcols[0]
|
||||
if res.Name != nil {
|
||||
first.Name = *res.Name
|
||||
}
|
||||
cols = append(cols, first)
|
||||
|
||||
default:
|
||||
name := ""
|
||||
if res.Name != nil {
|
||||
name = *res.Name
|
||||
}
|
||||
cols = append(cols, &Column{Name: name, DataType: "any", NotNull: false})
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
if n, ok := node.(*ast.SelectStmt); ok {
|
||||
for _, col := range cols {
|
||||
if !col.NotNull || col.Table == nil || col.skipTableRequiredCheck {
|
||||
continue
|
||||
}
|
||||
for _, f := range n.FromClause.Items {
|
||||
res := isTableRequired(f, col, tableRequired)
|
||||
if res != tableNotFound {
|
||||
col.NotNull = res == tableRequired
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return cols, nil
|
||||
}
|
||||
|
||||
const (
|
||||
tableNotFound = iota
|
||||
tableRequired
|
||||
tableOptional
|
||||
)
|
||||
|
||||
func isTableRequired(n ast.Node, col *Column, prior int) int {
|
||||
switch n := n.(type) {
|
||||
case *ast.RangeVar:
|
||||
tableMatch := *n.Relname == col.Table.Name
|
||||
aliasMatch := true
|
||||
if n.Alias != nil && col.TableAlias != "" {
|
||||
aliasMatch = *n.Alias.Aliasname == col.TableAlias
|
||||
}
|
||||
if aliasMatch && tableMatch {
|
||||
return prior
|
||||
}
|
||||
case *ast.JoinExpr:
|
||||
helper := func(l, r int) int {
|
||||
if res := isTableRequired(n.Larg, col, l); res != tableNotFound {
|
||||
return res
|
||||
}
|
||||
if res := isTableRequired(n.Rarg, col, r); res != tableNotFound {
|
||||
return res
|
||||
}
|
||||
return tableNotFound
|
||||
}
|
||||
switch n.Jointype {
|
||||
case ast.JoinTypeLeft:
|
||||
return helper(tableRequired, tableOptional)
|
||||
case ast.JoinTypeRight:
|
||||
return helper(tableOptional, tableRequired)
|
||||
case ast.JoinTypeFull:
|
||||
return helper(tableOptional, tableOptional)
|
||||
case ast.JoinTypeInner, ast.JoinTypeCross, ast.JoinTypeComma:
|
||||
return helper(tableRequired, tableRequired)
|
||||
}
|
||||
case *ast.List:
|
||||
for _, item := range n.Items {
|
||||
if res := isTableRequired(item, col, prior); res != tableNotFound {
|
||||
return res
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return tableNotFound
|
||||
}
|
||||
|
||||
type tableVisitor struct {
|
||||
list ast.List
|
||||
}
|
||||
|
||||
func (r *tableVisitor) Visit(n ast.Node) astutils.Visitor {
|
||||
switch n.(type) {
|
||||
case *ast.RangeVar, *ast.RangeFunction:
|
||||
r.list.Items = append(r.list.Items, n)
|
||||
return r
|
||||
case *ast.RangeSubselect:
|
||||
r.list.Items = append(r.list.Items, n)
|
||||
return nil
|
||||
default:
|
||||
return r
|
||||
}
|
||||
}
|
||||
|
||||
// Compute the output columns for a statement.
|
||||
//
|
||||
// Return an error if column references are ambiguous
|
||||
// Return an error if column references don't exist
|
||||
// Return an error if a table is referenced twice
|
||||
// Return an error if an unknown column is referenced
|
||||
func (c *Compiler) sourceTables(qc *QueryCatalog, node ast.Node) ([]*Table, error) {
|
||||
list := &ast.List{}
|
||||
switch n := node.(type) {
|
||||
case *ast.DeleteStmt:
|
||||
if n.Relations != nil {
|
||||
list = n.Relations
|
||||
} else if n.FromClause != nil {
|
||||
// Multi-table DELETE: walk FromClause to find tables
|
||||
var tv tableVisitor
|
||||
astutils.Walk(&tv, n.FromClause)
|
||||
list = &tv.list
|
||||
}
|
||||
case *ast.InsertStmt:
|
||||
list = &ast.List{
|
||||
Items: []ast.Node{n.Relation},
|
||||
}
|
||||
case *ast.SelectStmt:
|
||||
var tv tableVisitor
|
||||
astutils.Walk(&tv, n.FromClause)
|
||||
list = &tv.list
|
||||
case *ast.TruncateStmt:
|
||||
list = astutils.Search(n.Relations, func(node ast.Node) bool {
|
||||
_, ok := node.(*ast.RangeVar)
|
||||
return ok
|
||||
})
|
||||
case *ast.RefreshMatViewStmt:
|
||||
list = astutils.Search(n.Relation, func(node ast.Node) bool {
|
||||
_, ok := node.(*ast.RangeVar)
|
||||
return ok
|
||||
})
|
||||
case *ast.UpdateStmt:
|
||||
var tv tableVisitor
|
||||
astutils.Walk(&tv, n.FromClause)
|
||||
astutils.Walk(&tv, n.Relations)
|
||||
list = &tv.list
|
||||
}
|
||||
|
||||
var tables []*Table
|
||||
for _, item := range list.Items {
|
||||
switch n := item.(type) {
|
||||
|
||||
case *ast.RangeFunction:
|
||||
var funcCall *ast.FuncCall
|
||||
switch f := n.Functions.Items[0].(type) {
|
||||
case *ast.List:
|
||||
switch fi := f.Items[0].(type) {
|
||||
case *ast.FuncCall:
|
||||
funcCall = fi
|
||||
case *ast.SQLValueFunction:
|
||||
continue // TODO handle this correctly
|
||||
default:
|
||||
continue
|
||||
}
|
||||
case *ast.FuncCall:
|
||||
funcCall = f
|
||||
default:
|
||||
return nil, fmt.Errorf("sourceTables: unsupported function call type %T", n.Functions.Items[0])
|
||||
}
|
||||
|
||||
// If the function or table can't be found, don't error out. There
|
||||
// are many queries that depend on functions unknown to sqlc.
|
||||
fn, err := qc.GetFunc(funcCall.Func)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
var table *Table
|
||||
if fn.ReturnType != nil {
|
||||
table, err = qc.GetTable(&ast.TableName{
|
||||
Catalog: fn.ReturnType.Catalog,
|
||||
Schema: fn.ReturnType.Schema,
|
||||
Name: fn.ReturnType.Name,
|
||||
})
|
||||
}
|
||||
if table == nil || err != nil {
|
||||
if n.Alias != nil && len(n.Alias.Colnames.Items) > 0 {
|
||||
table = &Table{}
|
||||
for _, colName := range n.Alias.Colnames.Items {
|
||||
table.Columns = append(table.Columns, &Column{
|
||||
Name: colName.(*ast.String).Str,
|
||||
DataType: "any",
|
||||
})
|
||||
}
|
||||
} else {
|
||||
colName := fn.Rel.Name
|
||||
if n.Alias != nil {
|
||||
colName = *n.Alias.Aliasname
|
||||
}
|
||||
table = &Table{
|
||||
Rel: &ast.TableName{
|
||||
Catalog: fn.Rel.Catalog,
|
||||
Schema: fn.Rel.Schema,
|
||||
Name: fn.Rel.Name,
|
||||
},
|
||||
}
|
||||
if len(fn.Outs) > 0 {
|
||||
for _, arg := range fn.Outs {
|
||||
table.Columns = append(table.Columns, &Column{
|
||||
Name: arg.Name,
|
||||
DataType: arg.Type.Name,
|
||||
})
|
||||
}
|
||||
}
|
||||
if fn.ReturnType != nil {
|
||||
table.Columns = []*Column{
|
||||
{
|
||||
Name: colName,
|
||||
DataType: fn.ReturnType.Name,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if n.Alias != nil {
|
||||
table.Rel = &ast.TableName{
|
||||
Name: *n.Alias.Aliasname,
|
||||
}
|
||||
}
|
||||
tables = append(tables, table)
|
||||
|
||||
case *ast.RangeSubselect:
|
||||
cols, err := c.outputColumns(qc, n.Subquery)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var tableName string
|
||||
if n.Alias != nil {
|
||||
tableName = *n.Alias.Aliasname
|
||||
}
|
||||
|
||||
tables = append(tables, &Table{
|
||||
Rel: &ast.TableName{
|
||||
Name: tableName,
|
||||
},
|
||||
Columns: cols,
|
||||
})
|
||||
|
||||
case *ast.RangeVar:
|
||||
fqn, err := ParseTableName(n)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if qc == nil {
|
||||
return nil, fmt.Errorf("query catalog is empty")
|
||||
}
|
||||
table, cerr := qc.GetTable(fqn)
|
||||
if cerr != nil {
|
||||
// TODO: Update error location
|
||||
// cerr.Location = n.Location
|
||||
// return nil, *cerr
|
||||
return nil, cerr
|
||||
}
|
||||
if n.Alias != nil {
|
||||
table.Rel = &ast.TableName{
|
||||
Catalog: table.Rel.Catalog,
|
||||
Schema: table.Rel.Schema,
|
||||
Name: *n.Alias.Aliasname,
|
||||
}
|
||||
}
|
||||
tables = append(tables, table)
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("sourceTable: unsupported list item type: %T", n)
|
||||
}
|
||||
}
|
||||
return tables, nil
|
||||
}
|
||||
|
||||
func outputColumnRefs(res *ast.ResTarget, tables []*Table, node *ast.ColumnRef) ([]*Column, error) {
|
||||
parts := stringSlice(node.Fields)
|
||||
var schema, name, alias string
|
||||
switch {
|
||||
case len(parts) == 1:
|
||||
name = parts[0]
|
||||
case len(parts) == 2:
|
||||
alias = parts[0]
|
||||
name = parts[1]
|
||||
case len(parts) == 3:
|
||||
schema = parts[0]
|
||||
alias = parts[1]
|
||||
name = parts[2]
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown number of fields: %d", len(parts))
|
||||
}
|
||||
var cols []*Column
|
||||
var found int
|
||||
for _, t := range tables {
|
||||
if schema != "" && t.Rel.Schema != schema {
|
||||
continue
|
||||
}
|
||||
if alias != "" && t.Rel.Name != alias {
|
||||
continue
|
||||
}
|
||||
for _, c := range t.Columns {
|
||||
|
||||
if c.Name == name {
|
||||
found += 1
|
||||
cname := c.Name
|
||||
if res.Name != nil {
|
||||
cname = *res.Name
|
||||
}
|
||||
cols = append(cols, &Column{
|
||||
Name: cname,
|
||||
Type: c.Type,
|
||||
Table: c.Table,
|
||||
TableAlias: alias,
|
||||
DataType: c.DataType,
|
||||
NotNull: c.NotNull,
|
||||
Unsigned: c.Unsigned,
|
||||
IsArray: c.IsArray,
|
||||
ArrayDims: c.ArrayDims,
|
||||
Length: c.Length,
|
||||
EmbedTable: c.EmbedTable,
|
||||
OriginalName: c.Name,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
if found == 0 {
|
||||
return nil, &sqlerr.Error{
|
||||
Code: "42703",
|
||||
Message: fmt.Sprintf("column %q does not exist", name),
|
||||
Location: res.Location,
|
||||
}
|
||||
}
|
||||
if found > 1 {
|
||||
return nil, &sqlerr.Error{
|
||||
Code: "42703",
|
||||
Message: fmt.Sprintf("column reference %q is ambiguous", name),
|
||||
Location: res.Location,
|
||||
}
|
||||
}
|
||||
return cols, nil
|
||||
}
|
||||
|
||||
func (c *Compiler) findColumnForNode(item ast.Node, tables []*Table, targetList *ast.List) error {
|
||||
ref, ok := item.(*ast.ColumnRef)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
if c.isImplicitColumnRef(ref) {
|
||||
return nil
|
||||
}
|
||||
return findColumnForRef(ref, tables, targetList)
|
||||
}
|
||||
|
||||
// isImplicitColumnRef reports whether ref names a column that every table in
|
||||
// the dialect has without declaring it, and which therefore never appears in
|
||||
// a catalog built from the schema. SQLite gives each ordinary table a rowid,
|
||||
// reachable under three spellings.
|
||||
func (c *Compiler) isImplicitColumnRef(ref *ast.ColumnRef) bool {
|
||||
if c.conf.Engine != config.EngineSQLite {
|
||||
return false
|
||||
}
|
||||
parts := stringSlice(ref.Fields)
|
||||
if len(parts) == 0 {
|
||||
return false
|
||||
}
|
||||
switch parts[len(parts)-1] {
|
||||
case "rowid", "_rowid_", "oid":
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func findColumnForRef(ref *ast.ColumnRef, tables []*Table, targetList *ast.List) error {
|
||||
parts := stringSlice(ref.Fields)
|
||||
var alias, name string
|
||||
if len(parts) == 1 {
|
||||
name = parts[0]
|
||||
} else if len(parts) == 2 {
|
||||
alias = parts[0]
|
||||
name = parts[1]
|
||||
}
|
||||
|
||||
var found int
|
||||
for _, t := range tables {
|
||||
if alias != "" && t.Rel.Name != alias {
|
||||
continue
|
||||
}
|
||||
|
||||
// Find matching column
|
||||
for _, c := range t.Columns {
|
||||
if c.Name == name {
|
||||
found++
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Find matching alias if necessary
|
||||
if found == 0 {
|
||||
for _, c := range targetList.Items {
|
||||
resTarget, ok := c.(*ast.ResTarget)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if resTarget.Name != nil && *resTarget.Name == name {
|
||||
found++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if found == 0 {
|
||||
return &sqlerr.Error{
|
||||
Code: "42703",
|
||||
Message: fmt.Sprintf("column reference %q not found", name),
|
||||
Location: ref.Location,
|
||||
}
|
||||
}
|
||||
if found > 1 {
|
||||
return &sqlerr.Error{
|
||||
Code: "42703",
|
||||
Message: fmt.Sprintf("column reference %q is ambiguous", name),
|
||||
Location: ref.Location,
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
201
internal/compiler/parse.go
Normal file
201
internal/compiler/parse.go
Normal file
@@ -0,0 +1,201 @@
|
||||
package compiler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/sqlc-dev/sqlc/internal/debug"
|
||||
"github.com/sqlc-dev/sqlc/internal/metadata"
|
||||
"github.com/sqlc-dev/sqlc/internal/opts"
|
||||
"github.com/sqlc-dev/sqlc/internal/source"
|
||||
"github.com/sqlc-dev/sqlc/internal/sql/ast"
|
||||
"github.com/sqlc-dev/sqlc/internal/sql/astutils"
|
||||
"github.com/sqlc-dev/sqlc/internal/sql/preprocess"
|
||||
"github.com/sqlc-dev/sqlc/internal/sql/validate"
|
||||
"github.com/sqlc-dev/sqlc/internal/sqlcdebug"
|
||||
)
|
||||
|
||||
var debugDumpAST = sqlcdebug.New("dumpast")
|
||||
|
||||
func (c *Compiler) parseQuery(stmt ast.Node, pp *preprocess.Result, o opts.Parser) (*Query, error) {
|
||||
raw, ok := stmt.(*ast.RawStmt)
|
||||
if !ok {
|
||||
return nil, errors.New("node is not a statement")
|
||||
}
|
||||
|
||||
// The preprocessor already replaced every sqlc.* construct with native SQL
|
||||
// and recorded what it replaced.
|
||||
pre := pp.Statement(raw.StmtLocation)
|
||||
if pre.Err != nil {
|
||||
return nil, pre.Err
|
||||
}
|
||||
|
||||
// Engines number bind parameters in whatever order they convert the AST.
|
||||
// Restore the numbering the preprocessor assigned in source order.
|
||||
renumberParams(raw, pre.Numbers)
|
||||
|
||||
if c.coreCatalog != nil {
|
||||
return c.parseQueryCore(raw, pp.Text, pre)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
if debugDumpAST.Value() == "1" {
|
||||
debug.Dump(stmt)
|
||||
}
|
||||
|
||||
rawSQL, err := source.Pluck(pp.Text, raw.StmtLocation, raw.StmtLen)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if rawSQL == "" {
|
||||
return nil, errors.New("missing semicolon at end of file")
|
||||
}
|
||||
|
||||
name, cmd, err := metadata.ParseQueryNameAndType(rawSQL, metadata.CommentSyntax(c.parser.CommentSyntax()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if name == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if err := validate.Cmd(raw.Stmt, name, cmd); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
md := metadata.Metadata{
|
||||
Name: name,
|
||||
Cmd: cmd,
|
||||
}
|
||||
|
||||
// TODO eventually can use this for name and type/cmd parsing too
|
||||
cleanedComments, err := source.CleanedComments(rawSQL, c.parser.CommentSyntax())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
md.Params, md.Flags, md.RuleSkiplist, err = metadata.ParseCommentFlags(cleanedComments)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var anlys *analysis
|
||||
if c.analyzer != nil {
|
||||
inference, _ := c.inferQuery(raw, rawSQL, pre)
|
||||
if inference == nil {
|
||||
inference = &analysis{}
|
||||
}
|
||||
if inference.Query == "" {
|
||||
inference.Query = rawSQL
|
||||
}
|
||||
|
||||
result, err := c.analyzer.Analyze(ctx, raw, inference.Query, c.schema, inference.Named)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// If the query uses star expansion, verify that it was edited. If not,
|
||||
// return an error.
|
||||
stars := astutils.Search(raw, func(node ast.Node) bool {
|
||||
_, ok := node.(*ast.A_Star)
|
||||
return ok
|
||||
})
|
||||
hasStars := len(stars.Items) > 0
|
||||
unchanged := inference.Query == rawSQL
|
||||
if unchanged && hasStars {
|
||||
return nil, fmt.Errorf("star expansion failed for query")
|
||||
}
|
||||
|
||||
// FOOTGUN: combineAnalysis mutates inference
|
||||
anlys = combineAnalysis(inference, result)
|
||||
} else {
|
||||
anlys, err = c.analyzeQuery(raw, rawSQL, pre)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
expanded := anlys.Query
|
||||
|
||||
// If the query string was edited, make sure the syntax is valid
|
||||
if expanded != rawSQL {
|
||||
if _, err := c.parser.Parse(strings.NewReader(expanded)); err != nil {
|
||||
return nil, fmt.Errorf("edited query syntax is invalid: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
trimmed, comments, err := source.StripComments(expanded)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
md.Comments = comments
|
||||
|
||||
return &Query{
|
||||
RawStmt: raw,
|
||||
Metadata: md,
|
||||
Params: anlys.Parameters,
|
||||
Columns: anlys.Columns,
|
||||
SQL: trimmed,
|
||||
InsertIntoTable: anlys.Table,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// renumberParams applies the parameter numbers the preprocessor assigned,
|
||||
// matching each node by the location of its placeholder in the query text.
|
||||
func renumberParams(raw *ast.RawStmt, numbers map[int]int) {
|
||||
if len(numbers) == 0 {
|
||||
return
|
||||
}
|
||||
astutils.Walk(astutils.VisitorFunc(func(node ast.Node) {
|
||||
ref, ok := node.(*ast.ParamRef)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if n, ok := numbers[ref.Location]; ok {
|
||||
ref.Number = n
|
||||
}
|
||||
}), raw)
|
||||
}
|
||||
|
||||
func rangeVars(root ast.Node) []*ast.RangeVar {
|
||||
var vars []*ast.RangeVar
|
||||
find := astutils.VisitorFunc(func(node ast.Node) {
|
||||
switch n := node.(type) {
|
||||
case *ast.RangeVar:
|
||||
vars = append(vars, n)
|
||||
}
|
||||
})
|
||||
astutils.Walk(find, root)
|
||||
return vars
|
||||
}
|
||||
|
||||
func uniqueParamRefs(in []paramRef, dollar bool) []paramRef {
|
||||
m := make(map[int]bool, len(in))
|
||||
o := make([]paramRef, 0, len(in))
|
||||
for _, v := range in {
|
||||
if !m[v.ref.Number] {
|
||||
m[v.ref.Number] = true
|
||||
if v.ref.Number != 0 {
|
||||
o = append(o, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
if !dollar {
|
||||
start := 1
|
||||
for _, v := range in {
|
||||
if v.ref.Number == 0 {
|
||||
for m[start] {
|
||||
start++
|
||||
}
|
||||
v.ref.Number = start
|
||||
o = append(o, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
return o
|
||||
}
|
||||
165
internal/compiler/parse_core.go
Normal file
165
internal/compiler/parse_core.go
Normal file
@@ -0,0 +1,165 @@
|
||||
package compiler
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/sqlc-dev/sqlc/internal/core"
|
||||
coreanalyzer "github.com/sqlc-dev/sqlc/internal/core/analyzer"
|
||||
"github.com/sqlc-dev/sqlc/internal/metadata"
|
||||
"github.com/sqlc-dev/sqlc/internal/source"
|
||||
"github.com/sqlc-dev/sqlc/internal/sql/ast"
|
||||
"github.com/sqlc-dev/sqlc/internal/sql/named"
|
||||
"github.com/sqlc-dev/sqlc/internal/sql/preprocess"
|
||||
"github.com/sqlc-dev/sqlc/internal/sql/validate"
|
||||
)
|
||||
|
||||
func (c *Compiler) parseQueryCore(raw *ast.RawStmt, src string, pre *preprocess.Statement) (*Query, error) {
|
||||
rawSQL, err := source.Pluck(src, raw.StmtLocation, raw.StmtLen)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if strings.TrimSpace(rawSQL) == "" {
|
||||
return nil, errors.New("missing semicolon at end of file")
|
||||
}
|
||||
|
||||
name, cmd, err := metadata.ParseQueryNameAndType(rawSQL, metadata.CommentSyntax(c.parser.CommentSyntax()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if name == "" {
|
||||
return nil, nil
|
||||
}
|
||||
if err := validate.Cmd(raw.Stmt, name, cmd); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
md := metadata.Metadata{Name: name, Cmd: cmd}
|
||||
cleanedComments, err := source.CleanedComments(rawSQL, c.parser.CommentSyntax())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
md.Params, md.Flags, md.RuleSkiplist, err = metadata.ParseCommentFlags(cleanedComments)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if pre.ParamErr != nil {
|
||||
return nil, pre.ParamErr
|
||||
}
|
||||
namedParams := pre.Params
|
||||
expanded := rawSQL
|
||||
|
||||
var cols []*Column
|
||||
var params []Parameter
|
||||
switch raw.Stmt.(type) {
|
||||
case *ast.SelectStmt, *ast.InsertStmt, *ast.UpdateStmt, *ast.DeleteStmt:
|
||||
res, err := coreanalyzer.Prepare(c.coreCatalog, raw)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, col := range res.Columns {
|
||||
cols = append(cols, coreColumn(col))
|
||||
}
|
||||
for _, p := range res.Parameters {
|
||||
params = append(params, Parameter{Number: p.Number, Column: coreParamColumn(p, namedParams)})
|
||||
}
|
||||
expanded, err = source.Mutate(rawSQL, c.expandCore(raw, res.Stars))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// If the query string was edited, make sure the syntax is valid
|
||||
if expanded != rawSQL {
|
||||
if _, err := c.newParser().Parse(strings.NewReader(expanded)); err != nil {
|
||||
return nil, fmt.Errorf("edited query syntax is invalid: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
trimmed, comments, err := source.StripComments(expanded)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
md.Comments = comments
|
||||
|
||||
var insertTable *ast.TableName
|
||||
if ins, ok := raw.Stmt.(*ast.InsertStmt); ok {
|
||||
insertTable, _ = ParseTableName(ins.Relation)
|
||||
}
|
||||
|
||||
return &Query{
|
||||
RawStmt: raw,
|
||||
Metadata: md,
|
||||
Params: params,
|
||||
Columns: cols,
|
||||
SQL: trimmed,
|
||||
InsertIntoTable: insertTable,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func coreColumn(c core.Column) *Column {
|
||||
col := &Column{
|
||||
Name: c.Name,
|
||||
DataType: c.DataType,
|
||||
NotNull: c.NotNull,
|
||||
IsArray: c.IsArray,
|
||||
TypeExpr: c.Type,
|
||||
}
|
||||
describeType(col, c.Type)
|
||||
if c.Source != nil && c.Source.Table != "" {
|
||||
col.Table = &ast.TableName{Schema: c.Source.Schema, Name: c.Source.Table}
|
||||
col.TableAlias = c.Source.TableAlias
|
||||
col.OriginalName = c.Source.Column
|
||||
}
|
||||
return col
|
||||
}
|
||||
|
||||
// describeType fills in what codegen reads about a type from its
|
||||
// expression: one array dimension per nesting, the length that is the
|
||||
// innermost type's first integer argument (which is how a MySQL tinyint(1)
|
||||
// is told from a tinyint), and whether the innermost type is unsigned.
|
||||
func describeType(col *Column, t *core.TypeExpr) {
|
||||
if t == nil {
|
||||
if col.IsArray {
|
||||
col.ArrayDims = 1
|
||||
}
|
||||
return
|
||||
}
|
||||
col.ArrayDims = t.ArrayDims()
|
||||
inner := t.Innermost()
|
||||
if len(inner.Args) > 0 && inner.Args[0].Int != nil {
|
||||
l := int(*inner.Args[0].Int)
|
||||
col.Length = &l
|
||||
}
|
||||
col.Unsigned = strings.HasSuffix(inner.Name, " unsigned")
|
||||
}
|
||||
|
||||
func coreParamColumn(p core.Parameter, params *named.ParamSet) *Column {
|
||||
col := &Column{
|
||||
Name: p.Name,
|
||||
DataType: p.DataType,
|
||||
NotNull: p.NotNull,
|
||||
IsArray: p.IsArray,
|
||||
TypeExpr: p.Type,
|
||||
}
|
||||
describeType(col, p.Type)
|
||||
if p.Source != nil && p.Source.Table != "" {
|
||||
col.Table = &ast.TableName{Schema: p.Source.Schema, Name: p.Source.Table}
|
||||
col.OriginalName = p.Source.Column
|
||||
}
|
||||
if col.Name == "" && p.Source != nil {
|
||||
col.Name = p.Source.Column
|
||||
}
|
||||
// Merge in what the user asked for: sqlc.narg() makes the parameter
|
||||
// nullable and sqlc.slice() marks it as a slice, whichever way the
|
||||
// analyzer typed it.
|
||||
if param, isNamed := params.FetchMerge(p.Number, named.NewInferredParam(col.Name, p.NotNull)); isNamed {
|
||||
col.Name = param.Name()
|
||||
col.NotNull = param.NotNull()
|
||||
col.IsSqlcSlice = param.IsSqlcSlice()
|
||||
col.IsNamedParam = true
|
||||
}
|
||||
return col
|
||||
}
|
||||
67
internal/compiler/query.go
Normal file
67
internal/compiler/query.go
Normal file
@@ -0,0 +1,67 @@
|
||||
package compiler
|
||||
|
||||
import (
|
||||
"github.com/sqlc-dev/sqlc/internal/core"
|
||||
"github.com/sqlc-dev/sqlc/internal/metadata"
|
||||
"github.com/sqlc-dev/sqlc/internal/sql/ast"
|
||||
"github.com/sqlc-dev/sqlc/internal/sql/catalog"
|
||||
)
|
||||
|
||||
type Function struct {
|
||||
Rel *ast.FuncName
|
||||
ReturnType *ast.TypeName
|
||||
Outs []*catalog.Argument
|
||||
}
|
||||
|
||||
type Table struct {
|
||||
Rel *ast.TableName
|
||||
Columns []*Column
|
||||
}
|
||||
|
||||
type Column struct {
|
||||
Name string
|
||||
OriginalName string
|
||||
DataType string
|
||||
NotNull bool
|
||||
Unsigned bool
|
||||
IsArray bool
|
||||
ArrayDims int
|
||||
Comment string
|
||||
Length *int
|
||||
IsNamedParam bool
|
||||
IsFuncCall bool
|
||||
|
||||
// XXX: Figure out what PostgreSQL calls `foo.id`
|
||||
Scope string
|
||||
Table *ast.TableName
|
||||
TableAlias string
|
||||
Type *ast.TypeName
|
||||
EmbedTable *ast.TableName
|
||||
|
||||
// TypeExpr is the type as the analysis core wrote it, with the
|
||||
// arguments and nesting DataType and IsArray flatten away. It is unset
|
||||
// on the legacy path.
|
||||
TypeExpr *core.TypeExpr
|
||||
|
||||
IsSqlcSlice bool // is this sqlc.slice()
|
||||
|
||||
skipTableRequiredCheck bool
|
||||
}
|
||||
|
||||
type Query struct {
|
||||
SQL string
|
||||
Metadata metadata.Metadata
|
||||
Columns []*Column
|
||||
Params []Parameter
|
||||
|
||||
// Needed for CopyFrom
|
||||
InsertIntoTable *ast.TableName
|
||||
|
||||
// Needed for vet
|
||||
RawStmt *ast.RawStmt
|
||||
}
|
||||
|
||||
type Parameter struct {
|
||||
Number int
|
||||
Column *Column
|
||||
}
|
||||
109
internal/compiler/query_catalog.go
Normal file
109
internal/compiler/query_catalog.go
Normal file
@@ -0,0 +1,109 @@
|
||||
package compiler
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/sqlc-dev/sqlc/internal/sql/ast"
|
||||
"github.com/sqlc-dev/sqlc/internal/sql/catalog"
|
||||
"github.com/sqlc-dev/sqlc/internal/sql/preprocess"
|
||||
)
|
||||
|
||||
type QueryCatalog struct {
|
||||
catalog *catalog.Catalog
|
||||
ctes map[string]*Table
|
||||
embeds preprocess.EmbedSet
|
||||
}
|
||||
|
||||
func (comp *Compiler) buildQueryCatalog(c *catalog.Catalog, node ast.Node, embeds preprocess.EmbedSet) (*QueryCatalog, error) {
|
||||
var with *ast.WithClause
|
||||
switch n := node.(type) {
|
||||
case *ast.DeleteStmt:
|
||||
with = n.WithClause
|
||||
case *ast.InsertStmt:
|
||||
with = n.WithClause
|
||||
case *ast.UpdateStmt:
|
||||
with = n.WithClause
|
||||
case *ast.SelectStmt:
|
||||
with = n.WithClause
|
||||
default:
|
||||
with = nil
|
||||
}
|
||||
qc := &QueryCatalog{catalog: c, ctes: map[string]*Table{}, embeds: embeds}
|
||||
if with != nil {
|
||||
for _, item := range with.Ctes.Items {
|
||||
if cte, ok := item.(*ast.CommonTableExpr); ok {
|
||||
cols, err := comp.outputColumns(qc, cte.Ctequery)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var names []string
|
||||
if cte.Aliascolnames != nil {
|
||||
for _, item := range cte.Aliascolnames.Items {
|
||||
if val, ok := item.(*ast.String); ok {
|
||||
names = append(names, val.Str)
|
||||
} else {
|
||||
names = append(names, "")
|
||||
}
|
||||
}
|
||||
}
|
||||
rel := &ast.TableName{Name: *cte.Ctename}
|
||||
for i := range cols {
|
||||
cols[i].Table = rel
|
||||
if len(names) > i {
|
||||
cols[i].Name = names[i]
|
||||
}
|
||||
}
|
||||
qc.ctes[*cte.Ctename] = &Table{
|
||||
Rel: rel,
|
||||
Columns: cols,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return qc, nil
|
||||
}
|
||||
|
||||
func ConvertColumn(rel *ast.TableName, c *catalog.Column) *Column {
|
||||
return &Column{
|
||||
Table: rel,
|
||||
Name: c.Name,
|
||||
DataType: dataType(&c.Type),
|
||||
NotNull: c.IsNotNull,
|
||||
Unsigned: c.IsUnsigned,
|
||||
IsArray: c.IsArray,
|
||||
ArrayDims: c.ArrayDims,
|
||||
Type: &c.Type,
|
||||
Length: c.Length,
|
||||
}
|
||||
}
|
||||
|
||||
func (qc QueryCatalog) GetTable(rel *ast.TableName) (*Table, error) {
|
||||
cte, exists := qc.ctes[rel.Name]
|
||||
if exists {
|
||||
return &Table{Rel: rel, Columns: cte.Columns}, nil
|
||||
}
|
||||
src, err := qc.catalog.GetTable(rel)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var cols []*Column
|
||||
for _, c := range src.Columns {
|
||||
cols = append(cols, ConvertColumn(rel, c))
|
||||
}
|
||||
return &Table{Rel: rel, Columns: cols}, nil
|
||||
}
|
||||
|
||||
func (qc QueryCatalog) GetFunc(rel *ast.FuncName) (*Function, error) {
|
||||
funcs, err := qc.catalog.ListFuncsByName(rel)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(funcs) == 0 {
|
||||
return nil, fmt.Errorf("function not found: %s", rel.Name)
|
||||
}
|
||||
return &Function{
|
||||
Rel: rel,
|
||||
Outs: funcs[0].OutArgs(),
|
||||
ReturnType: funcs[0].ReturnType,
|
||||
}, nil
|
||||
}
|
||||
638
internal/compiler/resolve.go
Normal file
638
internal/compiler/resolve.go
Normal file
@@ -0,0 +1,638 @@
|
||||
package compiler
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"strconv"
|
||||
|
||||
"github.com/sqlc-dev/sqlc/internal/sql/ast"
|
||||
"github.com/sqlc-dev/sqlc/internal/sql/astutils"
|
||||
"github.com/sqlc-dev/sqlc/internal/sql/catalog"
|
||||
"github.com/sqlc-dev/sqlc/internal/sql/named"
|
||||
"github.com/sqlc-dev/sqlc/internal/sql/preprocess"
|
||||
"github.com/sqlc-dev/sqlc/internal/sql/sqlerr"
|
||||
)
|
||||
|
||||
func dataType(n *ast.TypeName) string {
|
||||
if n.Schema != "" {
|
||||
return n.Schema + "." + n.Name
|
||||
} else {
|
||||
return n.Name
|
||||
}
|
||||
}
|
||||
|
||||
func (comp *Compiler) resolveCatalogRefs(qc *QueryCatalog, rvs []*ast.RangeVar, args []paramRef, params *named.ParamSet, embeds preprocess.EmbedSet) ([]Parameter, error) {
|
||||
c := comp.catalog
|
||||
|
||||
aliasMap := map[string]*ast.TableName{}
|
||||
// TODO: Deprecate defaultTable
|
||||
var defaultTable *ast.TableName
|
||||
var tables []*ast.TableName
|
||||
|
||||
typeMap := map[string]map[string]map[string]*catalog.Column{}
|
||||
indexTable := func(table catalog.Table) error {
|
||||
tables = append(tables, table.Rel)
|
||||
if defaultTable == nil {
|
||||
defaultTable = table.Rel
|
||||
}
|
||||
schema := table.Rel.Schema
|
||||
if schema == "" {
|
||||
schema = c.DefaultSchema
|
||||
}
|
||||
if _, exists := typeMap[schema]; !exists {
|
||||
typeMap[schema] = map[string]map[string]*catalog.Column{}
|
||||
}
|
||||
typeMap[schema][table.Rel.Name] = map[string]*catalog.Column{}
|
||||
for _, c := range table.Columns {
|
||||
cc := c
|
||||
typeMap[schema][table.Rel.Name][c.Name] = cc
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, rv := range rvs {
|
||||
if rv.Relname == nil {
|
||||
continue
|
||||
}
|
||||
fqn, err := ParseTableName(rv)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, found := aliasMap[fqn.Name]; found {
|
||||
continue
|
||||
}
|
||||
table, err := c.GetTable(fqn)
|
||||
if err != nil {
|
||||
if qc == nil {
|
||||
continue
|
||||
}
|
||||
// If the table name doesn't exist, first check if it's a CTE
|
||||
if _, qcerr := qc.GetTable(fqn); qcerr != nil {
|
||||
return nil, err
|
||||
}
|
||||
continue
|
||||
}
|
||||
err = indexTable(table)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if rv.Alias != nil {
|
||||
aliasMap[*rv.Alias.Aliasname] = fqn
|
||||
}
|
||||
}
|
||||
|
||||
// resolve a table for an embed
|
||||
for _, embed := range embeds {
|
||||
table, err := c.GetTable(embed.Table)
|
||||
if err == nil {
|
||||
embed.Table = table.Rel
|
||||
continue
|
||||
}
|
||||
|
||||
if alias, ok := aliasMap[embed.Table.Name]; ok {
|
||||
embed.Table = alias
|
||||
continue
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("unable to resolve table with %q: %w", embed.Orig(), err)
|
||||
}
|
||||
|
||||
var a []Parameter
|
||||
|
||||
addUnknownParam := func(ref paramRef) {
|
||||
defaultP := named.NewInferredParam(ref.name, false)
|
||||
p, isNamed := params.FetchMerge(ref.ref.Number, defaultP)
|
||||
a = append(a, Parameter{
|
||||
Number: ref.ref.Number,
|
||||
Column: &Column{
|
||||
Name: p.Name(),
|
||||
DataType: "any",
|
||||
IsNamedParam: isNamed,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
for _, ref := range args {
|
||||
switch n := ref.parent.(type) {
|
||||
|
||||
case *limitOffset:
|
||||
defaultP := named.NewInferredParam("offset", true)
|
||||
p, isNamed := params.FetchMerge(ref.ref.Number, defaultP)
|
||||
a = append(a, Parameter{
|
||||
Number: ref.ref.Number,
|
||||
Column: &Column{
|
||||
Name: p.Name(),
|
||||
DataType: "integer",
|
||||
NotNull: p.NotNull(),
|
||||
IsNamedParam: isNamed,
|
||||
},
|
||||
})
|
||||
|
||||
case *limitCount:
|
||||
defaultP := named.NewInferredParam("limit", true)
|
||||
p, isNamed := params.FetchMerge(ref.ref.Number, defaultP)
|
||||
a = append(a, Parameter{
|
||||
Number: ref.ref.Number,
|
||||
Column: &Column{
|
||||
Name: p.Name(),
|
||||
DataType: "integer",
|
||||
NotNull: p.NotNull(),
|
||||
IsNamedParam: isNamed,
|
||||
},
|
||||
})
|
||||
|
||||
case *ast.A_Expr:
|
||||
// TODO: While this works for a wide range of simple expressions,
|
||||
// more complicated expressions will cause this logic to fail.
|
||||
list := astutils.Search(n.Lexpr, func(node ast.Node) bool {
|
||||
_, ok := node.(*ast.ColumnRef)
|
||||
return ok
|
||||
})
|
||||
if len(list.Items) == 0 {
|
||||
list = astutils.Search(n.Rexpr, func(node ast.Node) bool {
|
||||
_, ok := node.(*ast.ColumnRef)
|
||||
return ok
|
||||
})
|
||||
}
|
||||
|
||||
if len(list.Items) == 0 {
|
||||
// TODO: Move this to database-specific engine package
|
||||
dataType := "any"
|
||||
if astutils.Join(n.Name, ".") == "||" {
|
||||
dataType = "text"
|
||||
}
|
||||
|
||||
defaultP := named.NewParam("")
|
||||
p, isNamed := params.FetchMerge(ref.ref.Number, defaultP)
|
||||
a = append(a, Parameter{
|
||||
Number: ref.ref.Number,
|
||||
Column: &Column{
|
||||
Name: p.Name(),
|
||||
DataType: dataType,
|
||||
IsNamedParam: isNamed,
|
||||
NotNull: p.NotNull(),
|
||||
IsSqlcSlice: p.IsSqlcSlice(),
|
||||
},
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
switch node := list.Items[0].(type) {
|
||||
case *ast.ColumnRef:
|
||||
items := stringSlice(node.Fields)
|
||||
var key, alias string
|
||||
switch len(items) {
|
||||
case 1:
|
||||
key = items[0]
|
||||
case 2:
|
||||
alias = items[0]
|
||||
key = items[1]
|
||||
case 3:
|
||||
// schema := items[0]
|
||||
alias = items[1]
|
||||
key = items[2]
|
||||
default:
|
||||
panic("too many field items: " + strconv.Itoa(len(items)))
|
||||
}
|
||||
|
||||
search := tables
|
||||
if alias != "" {
|
||||
if original, ok := aliasMap[alias]; ok {
|
||||
search = []*ast.TableName{original}
|
||||
} else {
|
||||
var located bool
|
||||
for _, fqn := range tables {
|
||||
if fqn.Name == alias {
|
||||
located = true
|
||||
search = []*ast.TableName{fqn}
|
||||
}
|
||||
}
|
||||
if !located {
|
||||
return nil, &sqlerr.Error{
|
||||
Code: "42703",
|
||||
Message: fmt.Sprintf("table alias %q does not exist", alias),
|
||||
Location: node.Location,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var found int
|
||||
for _, table := range search {
|
||||
schema := table.Schema
|
||||
if schema == "" {
|
||||
schema = c.DefaultSchema
|
||||
}
|
||||
if c, ok := typeMap[schema][table.Name][key]; ok {
|
||||
found += 1
|
||||
if ref.name != "" {
|
||||
key = ref.name
|
||||
}
|
||||
|
||||
defaultP := named.NewInferredParam(key, c.IsNotNull)
|
||||
p, isNamed := params.FetchMerge(ref.ref.Number, defaultP)
|
||||
a = append(a, Parameter{
|
||||
Number: ref.ref.Number,
|
||||
Column: &Column{
|
||||
Name: p.Name(),
|
||||
OriginalName: c.Name,
|
||||
DataType: dataType(&c.Type),
|
||||
NotNull: p.NotNull(),
|
||||
Unsigned: c.IsUnsigned,
|
||||
IsArray: c.IsArray,
|
||||
ArrayDims: c.ArrayDims,
|
||||
Length: c.Length,
|
||||
Table: table,
|
||||
IsNamedParam: isNamed,
|
||||
IsSqlcSlice: p.IsSqlcSlice(),
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if found == 0 {
|
||||
return nil, &sqlerr.Error{
|
||||
Code: "42703",
|
||||
Message: fmt.Sprintf("column %q does not exist", key),
|
||||
Location: node.Location,
|
||||
}
|
||||
}
|
||||
if found > 1 {
|
||||
return nil, &sqlerr.Error{
|
||||
Code: "42703",
|
||||
Message: fmt.Sprintf("column reference %q is ambiguous", key),
|
||||
Location: node.Location,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
case *ast.BetweenExpr:
|
||||
if n == nil || n.Expr == nil || n.Left == nil || n.Right == nil {
|
||||
fmt.Println("ast.BetweenExpr is nil")
|
||||
continue
|
||||
}
|
||||
|
||||
var key string
|
||||
if ref, ok := n.Expr.(*ast.ColumnRef); ok {
|
||||
itemsCount := len(ref.Fields.Items)
|
||||
if str, ok := ref.Fields.Items[itemsCount-1].(*ast.String); ok {
|
||||
key = str.Str
|
||||
}
|
||||
}
|
||||
|
||||
for _, table := range tables {
|
||||
schema := table.Schema
|
||||
if schema == "" {
|
||||
schema = c.DefaultSchema
|
||||
}
|
||||
|
||||
if c, ok := typeMap[schema][table.Name][key]; ok {
|
||||
defaultP := named.NewInferredParam(key, c.IsNotNull)
|
||||
p, isNamed := params.FetchMerge(ref.ref.Number, defaultP)
|
||||
var namePrefix string
|
||||
if !isNamed {
|
||||
if ref.ref == n.Left {
|
||||
namePrefix = "from_"
|
||||
} else if ref.ref == n.Right {
|
||||
namePrefix = "to_"
|
||||
}
|
||||
}
|
||||
|
||||
a = append(a, Parameter{
|
||||
Number: ref.ref.Number,
|
||||
Column: &Column{
|
||||
Name: namePrefix + p.Name(),
|
||||
DataType: dataType(&c.Type),
|
||||
NotNull: p.NotNull(),
|
||||
Unsigned: c.IsUnsigned,
|
||||
IsArray: c.IsArray,
|
||||
ArrayDims: c.ArrayDims,
|
||||
Table: table,
|
||||
IsNamedParam: isNamed,
|
||||
IsSqlcSlice: p.IsSqlcSlice(),
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
case *ast.FuncCall:
|
||||
fun, err := c.ResolveFuncCall(n)
|
||||
if err != nil {
|
||||
// Synthesize a function on the fly to avoid returning with an error
|
||||
// for an unknown Postgres function (e.g. defined in an extension)
|
||||
var args []*catalog.Argument
|
||||
for range n.Args.Items {
|
||||
args = append(args, &catalog.Argument{
|
||||
Type: &ast.TypeName{Name: "any"},
|
||||
})
|
||||
}
|
||||
fun = &catalog.Function{
|
||||
Name: n.Func.Name,
|
||||
Args: args,
|
||||
ReturnType: &ast.TypeName{Name: "any"},
|
||||
}
|
||||
}
|
||||
|
||||
var added bool
|
||||
for i, item := range n.Args.Items {
|
||||
funcName := fun.Name
|
||||
var argName string
|
||||
switch inode := item.(type) {
|
||||
case *ast.ParamRef:
|
||||
if inode.Number != ref.ref.Number {
|
||||
continue
|
||||
}
|
||||
case *ast.TypeCast:
|
||||
pr, ok := inode.Arg.(*ast.ParamRef)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if pr.Number != ref.ref.Number {
|
||||
continue
|
||||
}
|
||||
case *ast.NamedArgExpr:
|
||||
pr, ok := inode.Arg.(*ast.ParamRef)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if pr.Number != ref.ref.Number {
|
||||
continue
|
||||
}
|
||||
if inode.Name != nil {
|
||||
argName = *inode.Name
|
||||
}
|
||||
default:
|
||||
continue
|
||||
}
|
||||
|
||||
if fun.Args == nil {
|
||||
defaultName := funcName
|
||||
if argName != "" {
|
||||
defaultName = argName
|
||||
}
|
||||
|
||||
defaultP := named.NewInferredParam(defaultName, false)
|
||||
p, isNamed := params.FetchMerge(ref.ref.Number, defaultP)
|
||||
added = true
|
||||
a = append(a, Parameter{
|
||||
Number: ref.ref.Number,
|
||||
Column: &Column{
|
||||
Name: p.Name(),
|
||||
DataType: "any",
|
||||
IsNamedParam: isNamed,
|
||||
NotNull: p.NotNull(),
|
||||
IsSqlcSlice: p.IsSqlcSlice(),
|
||||
},
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
var paramName string
|
||||
var paramType *ast.TypeName
|
||||
|
||||
if argName == "" {
|
||||
if i < len(fun.Args) {
|
||||
paramName = fun.Args[i].Name
|
||||
paramType = fun.Args[i].Type
|
||||
}
|
||||
} else {
|
||||
paramName = argName
|
||||
for _, arg := range fun.Args {
|
||||
if arg.Name == argName {
|
||||
paramType = arg.Type
|
||||
}
|
||||
}
|
||||
if paramType == nil {
|
||||
panic(fmt.Sprintf("named argument %s has no type", paramName))
|
||||
}
|
||||
}
|
||||
if paramName == "" {
|
||||
paramName = funcName
|
||||
}
|
||||
if paramType == nil {
|
||||
paramType = &ast.TypeName{Name: ""}
|
||||
}
|
||||
|
||||
defaultP := named.NewInferredParam(paramName, true)
|
||||
p, isNamed := params.FetchMerge(ref.ref.Number, defaultP)
|
||||
added = true
|
||||
a = append(a, Parameter{
|
||||
Number: ref.ref.Number,
|
||||
Column: &Column{
|
||||
Name: p.Name(),
|
||||
DataType: dataType(paramType),
|
||||
NotNull: p.NotNull(),
|
||||
IsNamedParam: isNamed,
|
||||
IsSqlcSlice: p.IsSqlcSlice(),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
if fun.ReturnType == nil {
|
||||
if !added {
|
||||
addUnknownParam(ref)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
table, err := c.GetTable(&ast.TableName{
|
||||
Catalog: fun.ReturnType.Catalog,
|
||||
Schema: fun.ReturnType.Schema,
|
||||
Name: fun.ReturnType.Name,
|
||||
})
|
||||
if err != nil {
|
||||
if !added {
|
||||
addUnknownParam(ref)
|
||||
}
|
||||
continue
|
||||
}
|
||||
err = indexTable(table)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
case *ast.ResTarget:
|
||||
if n.Name == nil {
|
||||
return nil, fmt.Errorf("*ast.ResTarget has nil name")
|
||||
}
|
||||
key := *n.Name
|
||||
|
||||
var schema, rel string
|
||||
// TODO: Deprecate defaultTable
|
||||
if defaultTable != nil {
|
||||
schema = defaultTable.Schema
|
||||
rel = defaultTable.Name
|
||||
}
|
||||
if ref.rv != nil {
|
||||
fqn, err := ParseTableName(ref.rv)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
schema = fqn.Schema
|
||||
rel = fqn.Name
|
||||
}
|
||||
if schema == "" {
|
||||
schema = c.DefaultSchema
|
||||
}
|
||||
|
||||
tableMap, ok := typeMap[schema][rel]
|
||||
if !ok {
|
||||
return nil, sqlerr.RelationNotFound(rel)
|
||||
}
|
||||
|
||||
if c, ok := tableMap[key]; ok {
|
||||
defaultP := named.NewInferredParam(key, c.IsNotNull)
|
||||
p, isNamed := params.FetchMerge(ref.ref.Number, defaultP)
|
||||
a = append(a, Parameter{
|
||||
Number: ref.ref.Number,
|
||||
Column: &Column{
|
||||
Name: p.Name(),
|
||||
OriginalName: c.Name,
|
||||
DataType: dataType(&c.Type),
|
||||
NotNull: p.NotNull(),
|
||||
Unsigned: c.IsUnsigned,
|
||||
IsArray: c.IsArray,
|
||||
ArrayDims: c.ArrayDims,
|
||||
Table: &ast.TableName{Schema: schema, Name: rel},
|
||||
Length: c.Length,
|
||||
IsNamedParam: isNamed,
|
||||
IsSqlcSlice: p.IsSqlcSlice(),
|
||||
},
|
||||
})
|
||||
} else {
|
||||
return nil, &sqlerr.Error{
|
||||
Code: "42703",
|
||||
Message: fmt.Sprintf("column %q does not exist", key),
|
||||
Location: n.Location,
|
||||
}
|
||||
}
|
||||
|
||||
case *ast.TypeCast:
|
||||
if n.TypeName == nil {
|
||||
return nil, fmt.Errorf("*ast.TypeCast has nil type name")
|
||||
}
|
||||
col := toColumn(n.TypeName)
|
||||
defaultP := named.NewInferredParam(col.Name, col.NotNull)
|
||||
p, isNamed := params.FetchMerge(ref.ref.Number, defaultP)
|
||||
|
||||
col.Name = p.Name()
|
||||
col.NotNull = p.NotNull()
|
||||
col.IsNamedParam = isNamed
|
||||
a = append(a, Parameter{
|
||||
Number: ref.ref.Number,
|
||||
Column: col,
|
||||
})
|
||||
|
||||
case *ast.ParamRef:
|
||||
a = append(a, Parameter{Number: ref.ref.Number})
|
||||
|
||||
case *ast.In:
|
||||
if n == nil || n.List == nil {
|
||||
fmt.Println("ast.In is nil")
|
||||
continue
|
||||
}
|
||||
|
||||
number := 0
|
||||
if pr, ok := n.List[0].(*ast.ParamRef); ok {
|
||||
number = pr.Number
|
||||
}
|
||||
|
||||
location := 0
|
||||
var key, alias string
|
||||
var items []string
|
||||
|
||||
if left, ok := n.Expr.(*ast.ColumnRef); ok {
|
||||
location = left.Location
|
||||
items = stringSlice(left.Fields)
|
||||
} else if left, ok := n.Expr.(*ast.ParamRef); ok {
|
||||
if len(n.List) <= 0 {
|
||||
continue
|
||||
}
|
||||
if right, ok := n.List[0].(*ast.ColumnRef); ok {
|
||||
location = left.Location
|
||||
items = stringSlice(right.Fields)
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
|
||||
switch len(items) {
|
||||
case 1:
|
||||
key = items[0]
|
||||
case 2:
|
||||
alias = items[0]
|
||||
key = items[1]
|
||||
default:
|
||||
panic("too many field items: " + strconv.Itoa(len(items)))
|
||||
}
|
||||
|
||||
var found int
|
||||
if n.Sel == nil {
|
||||
search := tables
|
||||
if alias != "" {
|
||||
if original, ok := aliasMap[alias]; ok {
|
||||
search = []*ast.TableName{original}
|
||||
} else {
|
||||
for _, fqn := range tables {
|
||||
if fqn.Name == alias {
|
||||
search = []*ast.TableName{fqn}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, table := range search {
|
||||
schema := table.Schema
|
||||
if schema == "" {
|
||||
schema = c.DefaultSchema
|
||||
}
|
||||
if c, ok := typeMap[schema][table.Name][key]; ok {
|
||||
found += 1
|
||||
if ref.name != "" {
|
||||
key = ref.name
|
||||
}
|
||||
defaultP := named.NewInferredParam(key, c.IsNotNull)
|
||||
p, isNamed := params.FetchMerge(ref.ref.Number, defaultP)
|
||||
a = append(a, Parameter{
|
||||
Number: number,
|
||||
Column: &Column{
|
||||
Name: p.Name(),
|
||||
OriginalName: c.Name,
|
||||
DataType: dataType(&c.Type),
|
||||
NotNull: c.IsNotNull,
|
||||
Unsigned: c.IsUnsigned,
|
||||
IsArray: c.IsArray,
|
||||
ArrayDims: c.ArrayDims,
|
||||
Table: table,
|
||||
IsNamedParam: isNamed,
|
||||
IsSqlcSlice: p.IsSqlcSlice(),
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if found == 0 {
|
||||
return nil, &sqlerr.Error{
|
||||
Code: "42703",
|
||||
Message: fmt.Sprintf("396: column %q does not exist", key),
|
||||
Location: location,
|
||||
}
|
||||
}
|
||||
if found > 1 {
|
||||
return nil, &sqlerr.Error{
|
||||
Code: "42703",
|
||||
Message: fmt.Sprintf("in same name column reference %q is ambiguous", key),
|
||||
Location: location,
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
slog.Debug("unsupported reference type", "type", fmt.Sprintf("%T", n))
|
||||
addUnknownParam(ref)
|
||||
}
|
||||
}
|
||||
return a, nil
|
||||
}
|
||||
10
internal/compiler/result.go
Normal file
10
internal/compiler/result.go
Normal file
@@ -0,0 +1,10 @@
|
||||
package compiler
|
||||
|
||||
import (
|
||||
"github.com/sqlc-dev/sqlc/internal/sql/catalog"
|
||||
)
|
||||
|
||||
type Result struct {
|
||||
Catalog *catalog.Catalog
|
||||
Queries []*Query
|
||||
}
|
||||
138
internal/compiler/returning.go
Normal file
138
internal/compiler/returning.go
Normal file
@@ -0,0 +1,138 @@
|
||||
package compiler
|
||||
|
||||
import (
|
||||
"github.com/sqlc-dev/sqlc/internal/config"
|
||||
"github.com/sqlc-dev/sqlc/internal/sql/ast"
|
||||
)
|
||||
|
||||
// returningTables builds virtual tables for the OLD and NEW aliases that
|
||||
// PostgreSQL 18 makes available in the RETURNING clause of INSERT, UPDATE and
|
||||
// DELETE statements. Each alias exposes the columns of the statement's target
|
||||
// table. For INSERT there is usually no old row and for DELETE there is no
|
||||
// new row, so every column reached through those aliases becomes nullable.
|
||||
func (c *Compiler) returningTables(qc *QueryCatalog, node ast.Node) ([]*Table, error) {
|
||||
if c.conf.Engine != config.EnginePostgreSQL {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var rv *ast.RangeVar
|
||||
var returning *ast.List
|
||||
oldAlias, newAlias := "old", "new"
|
||||
var oldNullable, newNullable bool
|
||||
switch n := node.(type) {
|
||||
case *ast.DeleteStmt:
|
||||
rv = firstRangeVar(n.Relations)
|
||||
returning = n.ReturningList
|
||||
if n.ReturningOldAlias != "" {
|
||||
oldAlias = n.ReturningOldAlias
|
||||
}
|
||||
if n.ReturningNewAlias != "" {
|
||||
newAlias = n.ReturningNewAlias
|
||||
}
|
||||
// A deleted row has no new value
|
||||
newNullable = true
|
||||
case *ast.InsertStmt:
|
||||
rv = n.Relation
|
||||
returning = n.ReturningList
|
||||
if n.ReturningOldAlias != "" {
|
||||
oldAlias = n.ReturningOldAlias
|
||||
}
|
||||
if n.ReturningNewAlias != "" {
|
||||
newAlias = n.ReturningNewAlias
|
||||
}
|
||||
// An inserted row has no old value, except when an ON CONFLICT
|
||||
// clause updates an existing row instead
|
||||
oldNullable = true
|
||||
case *ast.UpdateStmt:
|
||||
rv = firstRangeVar(n.Relations)
|
||||
returning = n.ReturningList
|
||||
if n.ReturningOldAlias != "" {
|
||||
oldAlias = n.ReturningOldAlias
|
||||
}
|
||||
if n.ReturningNewAlias != "" {
|
||||
newAlias = n.ReturningNewAlias
|
||||
}
|
||||
default:
|
||||
return nil, nil
|
||||
}
|
||||
if rv == nil || returning == nil || len(returning.Items) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
fqn, err := ParseTableName(rv)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
build := func(alias string, nullable bool) *Table {
|
||||
table, err := qc.GetTable(fqn)
|
||||
if err != nil {
|
||||
// An unresolvable target table is reported by the regular
|
||||
// source table lookup, so ignore the error here
|
||||
return nil
|
||||
}
|
||||
table.Rel = &ast.TableName{Name: alias}
|
||||
if nullable {
|
||||
for _, col := range table.Columns {
|
||||
col.NotNull = false
|
||||
}
|
||||
}
|
||||
return table
|
||||
}
|
||||
|
||||
var tables []*Table
|
||||
if t := build(oldAlias, oldNullable); t != nil {
|
||||
tables = append(tables, t)
|
||||
}
|
||||
if t := build(newAlias, newNullable); t != nil {
|
||||
tables = append(tables, t)
|
||||
}
|
||||
return tables, nil
|
||||
}
|
||||
|
||||
func firstRangeVar(list *ast.List) *ast.RangeVar {
|
||||
if list == nil {
|
||||
return nil
|
||||
}
|
||||
for _, item := range list.Items {
|
||||
if rv, ok := item.(*ast.RangeVar); ok && rv != nil {
|
||||
return rv
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// returningTableForScope returns the OLD or NEW virtual table named by scope.
|
||||
// A source table with the same name shadows the virtual table, matching
|
||||
// PostgreSQL, where the implicit aliases are only available when no relation
|
||||
// in the query is already known under that name.
|
||||
func returningTableForScope(tables, rtables []*Table, scope string) *Table {
|
||||
if scope == "" {
|
||||
return nil
|
||||
}
|
||||
for _, t := range tables {
|
||||
if t.Rel.Name == scope {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
for _, t := range rtables {
|
||||
if t.Rel.Name == scope {
|
||||
return t
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// tablesForRef resolves a column reference against the source tables,
|
||||
// extended with the OLD or NEW virtual table when the reference is qualified
|
||||
// with one of their names.
|
||||
func tablesForRef(ref *ast.ColumnRef, tables, rtables []*Table) []*Table {
|
||||
parts := stringSlice(ref.Fields)
|
||||
if len(parts) != 2 {
|
||||
return tables
|
||||
}
|
||||
if vt := returningTableForScope(tables, rtables, parts[0]); vt != nil {
|
||||
return append(append([]*Table{}, tables...), vt)
|
||||
}
|
||||
return tables
|
||||
}
|
||||
48
internal/compiler/selector.go
Normal file
48
internal/compiler/selector.go
Normal file
@@ -0,0 +1,48 @@
|
||||
package compiler
|
||||
|
||||
import "strings"
|
||||
|
||||
// selector is an interface used by a compiler for generating expressions for
|
||||
// output columns in a `SELECT ...` or `RETURNING ...` statement.
|
||||
//
|
||||
// This interface is exclusively needed at the moment for SQLite, which must
|
||||
// wrap output `jsonb` columns with a `json(column_name)` invocation so that a
|
||||
// publicly consumable format (i.e. not jsonb) is returned.
|
||||
type selector interface {
|
||||
// ColumnExpr generates output to be used in a `SELECT ...` or `RETURNING
|
||||
// ...` statement based on input column name and metadata.
|
||||
ColumnExpr(name string, column *Column) string
|
||||
}
|
||||
|
||||
// defaultSelector is a selector implementation that does the simpliest possible
|
||||
// pass through when generating column expressions. Its use is suitable for all
|
||||
// database engines not requiring additional customization.
|
||||
type defaultSelector struct{}
|
||||
|
||||
func newDefaultSelector() *defaultSelector {
|
||||
return &defaultSelector{}
|
||||
}
|
||||
|
||||
func (s *defaultSelector) ColumnExpr(name string, column *Column) string {
|
||||
return name
|
||||
}
|
||||
|
||||
type sqliteSelector struct{}
|
||||
|
||||
func newSQLiteSelector() *sqliteSelector {
|
||||
return &sqliteSelector{}
|
||||
}
|
||||
|
||||
func (s *sqliteSelector) ColumnExpr(name string, column *Column) string {
|
||||
// Under SQLite, neither json nor jsonb are real data types, and rather just
|
||||
// of type blob, so database drivers just return whatever raw binary is
|
||||
// stored as values. This is a problem for jsonb, which is considered an
|
||||
// internal format to SQLite and no attempt should be made to parse it
|
||||
// outside of the database itself. For jsonb columns in SQLite, wrap values
|
||||
// in `json(col)` to coerce the internal binary format to JSON parsable by
|
||||
// the user-space application.
|
||||
if strings.EqualFold(column.DataType, "jsonb") {
|
||||
return "json(" + name + ")"
|
||||
}
|
||||
return name
|
||||
}
|
||||
35
internal/compiler/selector_test.go
Normal file
35
internal/compiler/selector_test.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package compiler
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestSelector(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
selectorExpectColumnExpr := func(t *testing.T, selector selector, expected, name string, column *Column) {
|
||||
if actual := selector.ColumnExpr(name, column); expected != actual {
|
||||
t.Errorf("Expected %v, got %v for data type %v", expected, actual, column.DataType)
|
||||
}
|
||||
}
|
||||
|
||||
t.Run("DefaultSelectorColumnExpr", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
selector := newDefaultSelector()
|
||||
|
||||
selectorExpectColumnExpr(t, selector, "my_column", "my_column", &Column{DataType: "integer"})
|
||||
selectorExpectColumnExpr(t, selector, "my_column", "my_column", &Column{DataType: "json"})
|
||||
selectorExpectColumnExpr(t, selector, "my_column", "my_column", &Column{DataType: "jsonb"})
|
||||
selectorExpectColumnExpr(t, selector, "my_column", "my_column", &Column{DataType: "text"})
|
||||
})
|
||||
|
||||
t.Run("SQLiteSelectorColumnExpr", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
selector := newSQLiteSelector()
|
||||
|
||||
selectorExpectColumnExpr(t, selector, "my_column", "my_column", &Column{DataType: "integer"})
|
||||
selectorExpectColumnExpr(t, selector, "my_column", "my_column", &Column{DataType: "json"})
|
||||
selectorExpectColumnExpr(t, selector, "json(my_column)", "my_column", &Column{DataType: "jsonb"})
|
||||
selectorExpectColumnExpr(t, selector, "my_column", "my_column", &Column{DataType: "text"})
|
||||
})
|
||||
}
|
||||
33
internal/compiler/to_column.go
Normal file
33
internal/compiler/to_column.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package compiler
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/sqlc-dev/sqlc/internal/sql/ast"
|
||||
"github.com/sqlc-dev/sqlc/internal/sql/astutils"
|
||||
)
|
||||
|
||||
func arrayDims(n *ast.TypeName) int {
|
||||
if n == nil || n.ArrayBounds == nil {
|
||||
return 0
|
||||
}
|
||||
return len(n.ArrayBounds.Items)
|
||||
}
|
||||
|
||||
func toColumn(n *ast.TypeName) *Column {
|
||||
if n == nil {
|
||||
panic("can't build column for nil type name")
|
||||
}
|
||||
typ, err := ParseTypeName(n)
|
||||
if err != nil {
|
||||
panic("toColumn: " + err.Error())
|
||||
}
|
||||
arrayDims := arrayDims(n)
|
||||
return &Column{
|
||||
Type: typ,
|
||||
DataType: strings.TrimPrefix(astutils.Join(n.Names, "."), "."),
|
||||
NotNull: true, // XXX: How do we know if this should be null?
|
||||
IsArray: arrayDims > 0,
|
||||
ArrayDims: arrayDims,
|
||||
}
|
||||
}
|
||||
239
internal/config/config.go
Normal file
239
internal/config/config.go
Normal file
@@ -0,0 +1,239 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
golang "github.com/sqlc-dev/sqlc/internal/codegen/golang/opts"
|
||||
)
|
||||
|
||||
type versionSetting struct {
|
||||
Number string `json:"version" yaml:"version"`
|
||||
}
|
||||
|
||||
type Engine string
|
||||
|
||||
type Paths []string
|
||||
|
||||
func (p *Paths) UnmarshalJSON(data []byte) error {
|
||||
if string(data[0]) == `[` {
|
||||
var out []string
|
||||
if err := json.Unmarshal(data, &out); err != nil {
|
||||
return nil
|
||||
}
|
||||
*p = Paths(out)
|
||||
return nil
|
||||
}
|
||||
var out string
|
||||
if err := json.Unmarshal(data, &out); err != nil {
|
||||
return nil
|
||||
}
|
||||
*p = Paths([]string{out})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Paths) UnmarshalYAML(unmarshal func(any) error) error {
|
||||
out := []string{}
|
||||
if sliceErr := unmarshal(&out); sliceErr != nil {
|
||||
var ele string
|
||||
if strErr := unmarshal(&ele); strErr != nil {
|
||||
return strErr
|
||||
}
|
||||
out = []string{ele}
|
||||
}
|
||||
|
||||
*p = Paths(out)
|
||||
return nil
|
||||
}
|
||||
|
||||
const (
|
||||
EngineMySQL Engine = "mysql"
|
||||
EnginePostgreSQL Engine = "postgresql"
|
||||
EngineSQLite Engine = "sqlite"
|
||||
EngineClickHouse Engine = "clickhouse"
|
||||
EngineGoogleSQL Engine = "googlesql"
|
||||
EngineMSSQL Engine = "mssql"
|
||||
EngineDuckDB Engine = "duckdb"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Version string `json:"version" yaml:"version"`
|
||||
Cloud Cloud `json:"cloud" yaml:"cloud"`
|
||||
Servers []Server `json:"servers" yaml:"servers"`
|
||||
SQL []SQL `json:"sql" yaml:"sql"`
|
||||
Overrides Overrides `json:"overrides" yaml:"overrides"`
|
||||
Plugins []Plugin `json:"plugins" yaml:"plugins"`
|
||||
Rules []Rule `json:"rules" yaml:"rules"`
|
||||
Options map[string]yaml.Node `json:"options" yaml:"options"`
|
||||
}
|
||||
|
||||
type Server struct {
|
||||
Name string `json:"name,omitempty" yaml:"name"`
|
||||
Engine Engine `json:"engine,omitempty" yaml:"engine"`
|
||||
URI string `json:"uri" yaml:"uri"`
|
||||
}
|
||||
|
||||
type Database struct {
|
||||
URI string `json:"uri" yaml:"uri"`
|
||||
Managed bool `json:"managed" yaml:"managed"`
|
||||
}
|
||||
|
||||
type Cloud struct {
|
||||
Organization string `json:"organization" yaml:"organization"`
|
||||
Project string `json:"project" yaml:"project"`
|
||||
Hostname string `json:"hostname" yaml:"hostname"`
|
||||
AuthToken string `json:"-" yaml:"-"`
|
||||
}
|
||||
|
||||
type Plugin struct {
|
||||
Name string `json:"name" yaml:"name"`
|
||||
Env []string `json:"env" yaml:"env"`
|
||||
Process *struct {
|
||||
Cmd string `json:"cmd" yaml:"cmd"`
|
||||
Format string `json:"format" yaml:"format"`
|
||||
} `json:"process" yaml:"process"`
|
||||
WASM *struct {
|
||||
URL string `json:"url" yaml:"url"`
|
||||
SHA256 string `json:"sha256" yaml:"sha256"`
|
||||
} `json:"wasm" yaml:"wasm"`
|
||||
}
|
||||
|
||||
type Rule struct {
|
||||
Name string `json:"name" yaml:"name"`
|
||||
Rule string `json:"rule" yaml:"rule"`
|
||||
Msg string `json:"message" yaml:"message"`
|
||||
}
|
||||
|
||||
type Overrides struct {
|
||||
Go *golang.GlobalOptions `json:"go,omitempty" yaml:"go"`
|
||||
}
|
||||
|
||||
type SQL struct {
|
||||
Name string `json:"name" yaml:"name"`
|
||||
Engine Engine `json:"engine,omitempty" yaml:"engine"`
|
||||
Schema Paths `json:"schema" yaml:"schema"`
|
||||
Queries Paths `json:"queries" yaml:"queries"`
|
||||
Database *Database `json:"database" yaml:"database"`
|
||||
StrictFunctionChecks bool `json:"strict_function_checks" yaml:"strict_function_checks"`
|
||||
StrictOrderBy *bool `json:"strict_order_by" yaml:"strict_order_by"`
|
||||
Gen SQLGen `json:"gen" yaml:"gen"`
|
||||
Codegen []Codegen `json:"codegen" yaml:"codegen"`
|
||||
Rules []string `json:"rules" yaml:"rules"`
|
||||
Analyzer Analyzer `json:"analyzer" yaml:"analyzer"`
|
||||
}
|
||||
|
||||
type Analyzer struct {
|
||||
Database *bool `json:"database" yaml:"database"`
|
||||
}
|
||||
|
||||
// TODO: Figure out a better name for this
|
||||
type Codegen struct {
|
||||
Out string `json:"out" yaml:"out"`
|
||||
Plugin string `json:"plugin" yaml:"plugin"`
|
||||
Options yaml.Node `json:"options" yaml:"options"`
|
||||
}
|
||||
|
||||
type SQLGen struct {
|
||||
Go *golang.Options `json:"go,omitempty" yaml:"go"`
|
||||
JSON *SQLJSON `json:"json,omitempty" yaml:"json"`
|
||||
}
|
||||
|
||||
type SQLJSON struct {
|
||||
Out string `json:"out" yaml:"out"`
|
||||
Indent string `json:"indent,omitempty" yaml:"indent"`
|
||||
Filename string `json:"filename,omitempty" yaml:"filename"`
|
||||
}
|
||||
|
||||
var ErrMissingEngine = errors.New("unknown engine")
|
||||
var ErrMissingVersion = errors.New("no version number")
|
||||
var ErrNoOutPath = errors.New("no output path")
|
||||
var ErrNoPackagePath = errors.New("missing package path")
|
||||
var ErrNoPackages = errors.New("no packages")
|
||||
var ErrNoQuerierType = errors.New("no querier emit type enabled")
|
||||
var ErrUnknownEngine = errors.New("invalid engine")
|
||||
var ErrUnknownVersion = errors.New("invalid version number")
|
||||
|
||||
var ErrPluginBuiltin = errors.New("a built-in plugin with that name already exists")
|
||||
var ErrPluginNoName = errors.New("missing plugin name")
|
||||
var ErrPluginExists = errors.New("a plugin with that name already exists")
|
||||
var ErrPluginNotFound = errors.New("no plugin found")
|
||||
var ErrPluginNoType = errors.New("plugin: field `process` or `wasm` required")
|
||||
var ErrPluginBothTypes = errors.New("plugin: `process` and `wasm` cannot both be defined")
|
||||
var ErrPluginProcessNoCmd = errors.New("plugin: missing process command")
|
||||
|
||||
var ErrInvalidDatabase = errors.New("database must be managed or have a non-empty URI")
|
||||
var ErrManagedDatabaseNoProject = errors.New(`managed databases require a cloud project
|
||||
|
||||
If you don't have a project, you can create one from the sqlc Cloud
|
||||
dashboard at https://dashboard.sqlc.dev/. If you have a project, ensure
|
||||
you've set its id as the value of the "project" field within the "cloud"
|
||||
section of your sqlc configuration. The id will look similar to
|
||||
"01HA8TWGMYPHK0V2GGMB3R2TP9".`)
|
||||
var ErrManagedDatabaseNoAuthToken = errors.New(`managed databases require an auth token
|
||||
|
||||
If you don't have an auth token, you can create one from the sqlc Cloud
|
||||
dashboard at https://dashboard.sqlc.dev/. If you have an auth token, ensure
|
||||
you've set it as the value of the SQLC_AUTH_TOKEN environment variable.`)
|
||||
|
||||
func ParseConfig(rd io.Reader) (Config, error) {
|
||||
var buf bytes.Buffer
|
||||
var config Config
|
||||
var version versionSetting
|
||||
|
||||
ver := io.TeeReader(rd, &buf)
|
||||
dec := yaml.NewDecoder(ver)
|
||||
if err := dec.Decode(&version); err != nil {
|
||||
return config, err
|
||||
}
|
||||
if version.Number == "" {
|
||||
return config, ErrMissingVersion
|
||||
}
|
||||
var err error
|
||||
switch version.Number {
|
||||
case "1":
|
||||
config, err = v1ParseConfig(&buf)
|
||||
if err != nil {
|
||||
return config, err
|
||||
}
|
||||
case "2":
|
||||
config, err = v2ParseConfig(&buf)
|
||||
if err != nil {
|
||||
return config, err
|
||||
}
|
||||
default:
|
||||
return config, ErrUnknownVersion
|
||||
}
|
||||
err = config.addEnvVars()
|
||||
if err != nil {
|
||||
return config, err
|
||||
}
|
||||
return config, nil
|
||||
}
|
||||
|
||||
type CombinedSettings struct {
|
||||
Global Config
|
||||
Package SQL
|
||||
Go golang.Options
|
||||
JSON SQLJSON
|
||||
|
||||
// TODO: Combine these into a more usable type
|
||||
Codegen Codegen
|
||||
}
|
||||
|
||||
func Combine(conf Config, pkg SQL) CombinedSettings {
|
||||
cs := CombinedSettings{
|
||||
Global: conf,
|
||||
Package: pkg,
|
||||
}
|
||||
if pkg.Gen.Go != nil {
|
||||
cs.Go = *pkg.Gen.Go
|
||||
}
|
||||
if pkg.Gen.JSON != nil {
|
||||
cs.JSON = *pkg.Gen.JSON
|
||||
}
|
||||
return cs
|
||||
}
|
||||
91
internal/config/config_test.go
Normal file
91
internal/config/config_test.go
Normal file
@@ -0,0 +1,91 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
)
|
||||
|
||||
const missingVersion = `{
|
||||
}`
|
||||
|
||||
const missingPackages = `{
|
||||
"version": "1"
|
||||
}`
|
||||
|
||||
const unknownVersion = `{
|
||||
"version": "foo"
|
||||
}`
|
||||
|
||||
const unknownFields = `{
|
||||
"version": "1",
|
||||
"foo": "bar"
|
||||
}`
|
||||
|
||||
func TestBadConfigs(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
err string
|
||||
json string
|
||||
}{
|
||||
{
|
||||
"missing version",
|
||||
"no version number",
|
||||
missingVersion,
|
||||
},
|
||||
{
|
||||
"missing packages",
|
||||
"no packages",
|
||||
missingPackages,
|
||||
},
|
||||
{
|
||||
"unknown version",
|
||||
"invalid version number",
|
||||
unknownVersion,
|
||||
},
|
||||
{
|
||||
"unknown fields",
|
||||
`yaml: unmarshal errors:
|
||||
line 3: field foo not found in type config.V1GenerateSettings`,
|
||||
unknownFields,
|
||||
},
|
||||
} {
|
||||
tt := test
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
_, err := ParseConfig(strings.NewReader(tt.json))
|
||||
if err == nil {
|
||||
t.Fatalf("expected err; got nil")
|
||||
}
|
||||
if diff := cmp.Diff(err.Error(), tt.err); diff != "" {
|
||||
t.Errorf("differed (-want +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const validConfigOne = `{
|
||||
"version": "1"
|
||||
"packages": []
|
||||
}`
|
||||
|
||||
func FuzzConfig(f *testing.F) {
|
||||
f.Add(validConfigOne)
|
||||
f.Fuzz(func(t *testing.T, orig string) {
|
||||
ParseConfig(strings.NewReader(orig))
|
||||
})
|
||||
}
|
||||
|
||||
func TestInvalidConfig(t *testing.T) {
|
||||
err := Validate(&Config{
|
||||
SQL: []SQL{{
|
||||
Database: &Database{
|
||||
URI: "",
|
||||
Managed: false,
|
||||
},
|
||||
}},
|
||||
})
|
||||
if err == nil {
|
||||
t.Errorf("expected err; got nil")
|
||||
}
|
||||
}
|
||||
84
internal/config/convert/convert.go
Normal file
84
internal/config/convert/convert.go
Normal file
@@ -0,0 +1,84 @@
|
||||
package convert
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
func gen(n *yaml.Node) (any, error) {
|
||||
switch n.Kind {
|
||||
|
||||
case yaml.MappingNode:
|
||||
nn := map[string]any{}
|
||||
for i, _ := range n.Content {
|
||||
if i%2 == 0 {
|
||||
k := n.Content[i]
|
||||
v, err := gen(n.Content[i+1])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
nn[k.Value] = v
|
||||
}
|
||||
}
|
||||
return nn, nil
|
||||
|
||||
case yaml.SequenceNode:
|
||||
nn := []any{}
|
||||
for i, _ := range n.Content {
|
||||
v, err := gen(n.Content[i])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
nn = append(nn, v)
|
||||
}
|
||||
return nn, nil
|
||||
|
||||
case yaml.ScalarNode:
|
||||
switch n.Tag {
|
||||
|
||||
case "!!bool":
|
||||
return strconv.ParseBool(n.Value)
|
||||
|
||||
case "!!float":
|
||||
return strconv.ParseFloat(n.Value, 64)
|
||||
|
||||
case "!!int":
|
||||
return strconv.Atoi(n.Value)
|
||||
|
||||
case "!!null":
|
||||
return nil, nil
|
||||
|
||||
case "!!str":
|
||||
return n.Value, nil
|
||||
|
||||
default:
|
||||
return n.Value, nil
|
||||
|
||||
}
|
||||
|
||||
case yaml.AliasNode:
|
||||
return gen(n.Alias)
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown yaml value: %s (%s)", n.Value, n.Tag)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func YAMLtoJSON(n yaml.Node) ([]byte, error) {
|
||||
if n.Kind == 0 {
|
||||
return []byte{}, nil
|
||||
}
|
||||
iface, err := gen(&n)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
blob, err := json.Marshal(iface)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return blob, nil
|
||||
}
|
||||
47
internal/config/convert/convert_test.go
Normal file
47
internal/config/convert/convert_test.go
Normal file
@@ -0,0 +1,47 @@
|
||||
package convert
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
const anchor = `
|
||||
sql:
|
||||
- schema: query.sql
|
||||
queries: query.sql
|
||||
engine: postgresql
|
||||
codegen:
|
||||
- out: gateway/src/gateway/services/organization
|
||||
plugin: py
|
||||
options: &base-options
|
||||
query_parameter_limit: 1
|
||||
package: gateway
|
||||
output_models_file_name: null
|
||||
emit_module: true
|
||||
emit_generators: false
|
||||
emit_async: true
|
||||
|
||||
- schema: query.sql
|
||||
queries: query.sql
|
||||
engine: postgresql
|
||||
codegen:
|
||||
- out: gateway/src/gateway/services/project
|
||||
plugin: py
|
||||
options: *base-options
|
||||
`
|
||||
|
||||
type config struct {
|
||||
SQL yaml.Node `yaml:"sql"`
|
||||
}
|
||||
|
||||
func TestAlias(t *testing.T) {
|
||||
var a config
|
||||
err := yaml.Unmarshal([]byte(anchor), &a)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := gen(&a.SQL); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
17
internal/config/env.go
Normal file
17
internal/config/env.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func (c *Config) addEnvVars() error {
|
||||
authToken := os.Getenv("SQLC_AUTH_TOKEN")
|
||||
if authToken != "" && !strings.HasPrefix(authToken, "sqlc_") {
|
||||
return fmt.Errorf("$SQLC_AUTH_TOKEN doesn't start with \"sqlc_\"")
|
||||
}
|
||||
c.Cloud.AuthToken = authToken
|
||||
|
||||
return nil
|
||||
}
|
||||
196
internal/config/v_one.go
Normal file
196
internal/config/v_one.go
Normal file
@@ -0,0 +1,196 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"path/filepath"
|
||||
|
||||
yaml "gopkg.in/yaml.v3"
|
||||
|
||||
golang "github.com/sqlc-dev/sqlc/internal/codegen/golang/opts"
|
||||
)
|
||||
|
||||
type V1GenerateSettings struct {
|
||||
Version string `json:"version" yaml:"version"`
|
||||
Cloud Cloud `json:"cloud" yaml:"cloud"`
|
||||
Packages []v1PackageSettings `json:"packages" yaml:"packages"`
|
||||
Overrides []golang.Override `json:"overrides,omitempty" yaml:"overrides,omitempty"`
|
||||
Rename map[string]string `json:"rename,omitempty" yaml:"rename,omitempty"`
|
||||
Rules []Rule `json:"rules" yaml:"rules"`
|
||||
}
|
||||
|
||||
type v1PackageSettings struct {
|
||||
Name string `json:"name" yaml:"name"`
|
||||
Engine Engine `json:"engine,omitempty" yaml:"engine"`
|
||||
Database *Database `json:"database,omitempty" yaml:"database"`
|
||||
Analyzer Analyzer `json:"analyzer" yaml:"analyzer"`
|
||||
Path string `json:"path" yaml:"path"`
|
||||
Schema Paths `json:"schema" yaml:"schema"`
|
||||
Queries Paths `json:"queries" yaml:"queries"`
|
||||
EmitInterface bool `json:"emit_interface" yaml:"emit_interface"`
|
||||
EmitJSONTags bool `json:"emit_json_tags" yaml:"emit_json_tags"`
|
||||
JsonTagsIDUppercase bool `json:"json_tags_id_uppercase" yaml:"json_tags_id_uppercase"`
|
||||
EmitDBTags bool `json:"emit_db_tags" yaml:"emit_db_tags"`
|
||||
EmitPreparedQueries bool `json:"emit_prepared_queries" yaml:"emit_prepared_queries"`
|
||||
EmitExactTableNames bool `json:"emit_exact_table_names,omitempty" yaml:"emit_exact_table_names"`
|
||||
EmitEmptySlices bool `json:"emit_empty_slices,omitempty" yaml:"emit_empty_slices"`
|
||||
EmitExportedQueries bool `json:"emit_exported_queries,omitempty" yaml:"emit_exported_queries"`
|
||||
EmitResultStructPointers bool `json:"emit_result_struct_pointers" yaml:"emit_result_struct_pointers"`
|
||||
EmitParamsStructPointers bool `json:"emit_params_struct_pointers" yaml:"emit_params_struct_pointers"`
|
||||
EmitMethodsWithDBArgument bool `json:"emit_methods_with_db_argument" yaml:"emit_methods_with_db_argument"`
|
||||
EmitPointersForNullTypes bool `json:"emit_pointers_for_null_types" yaml:"emit_pointers_for_null_types"`
|
||||
EmitPointersForNullEnumTypes *bool `json:"emit_pointers_for_null_enum_types,omitempty" yaml:"emit_pointers_for_null_enum_types"`
|
||||
EmitEnumValidMethod bool `json:"emit_enum_valid_method,omitempty" yaml:"emit_enum_valid_method"`
|
||||
EmitAllEnumValues bool `json:"emit_all_enum_values,omitempty" yaml:"emit_all_enum_values"`
|
||||
EmitSqlAsComment bool `json:"emit_sql_as_comment,omitempty" yaml:"emit_sql_as_comment"`
|
||||
JSONTagsCaseStyle string `json:"json_tags_case_style,omitempty" yaml:"json_tags_case_style"`
|
||||
SQLPackage string `json:"sql_package" yaml:"sql_package"`
|
||||
SQLDriver string `json:"sql_driver" yaml:"sql_driver"`
|
||||
Overrides []golang.Override `json:"overrides" yaml:"overrides"`
|
||||
OutputBatchFileName string `json:"output_batch_file_name,omitempty" yaml:"output_batch_file_name"`
|
||||
OutputDBFileName string `json:"output_db_file_name,omitempty" yaml:"output_db_file_name"`
|
||||
OutputModelsFileName string `json:"output_models_file_name,omitempty" yaml:"output_models_file_name"`
|
||||
OutputModelsPath string `json:"output_models_path,omitempty" yaml:"output_models_path"`
|
||||
OutputModelsPackage string `json:"output_models_package,omitempty" yaml:"output_models_package"`
|
||||
OutputModelsImport string `json:"output_models_import,omitempty" yaml:"output_models_import"`
|
||||
OutputModelsEmit *bool `json:"output_models_emit,omitempty" yaml:"output_models_emit"`
|
||||
OutputQuerierFileName string `json:"output_querier_file_name,omitempty" yaml:"output_querier_file_name"`
|
||||
OutputCopyFromFileName string `json:"output_copyfrom_file_name,omitempty" yaml:"output_copyfrom_file_name"`
|
||||
OutputFilesSuffix string `json:"output_files_suffix,omitempty" yaml:"output_files_suffix"`
|
||||
StrictFunctionChecks bool `json:"strict_function_checks" yaml:"strict_function_checks"`
|
||||
StrictOrderBy *bool `json:"strict_order_by" yaml:"strict_order_by"`
|
||||
QueryParameterLimit *int32 `json:"query_parameter_limit,omitempty" yaml:"query_parameter_limit"`
|
||||
OmitSqlcVersion bool `json:"omit_sqlc_version,omitempty" yaml:"omit_sqlc_version"`
|
||||
OmitUnusedStructs bool `json:"omit_unused_structs,omitempty" yaml:"omit_unused_structs"`
|
||||
Rules []string `json:"rules" yaml:"rules"`
|
||||
BuildTags string `json:"build_tags,omitempty" yaml:"build_tags"`
|
||||
}
|
||||
|
||||
func v1ParseConfig(rd io.Reader) (Config, error) {
|
||||
dec := yaml.NewDecoder(rd)
|
||||
dec.KnownFields(true)
|
||||
var settings V1GenerateSettings
|
||||
var config Config
|
||||
if err := dec.Decode(&settings); err != nil {
|
||||
return config, err
|
||||
}
|
||||
if settings.Version == "" {
|
||||
return config, ErrMissingVersion
|
||||
}
|
||||
if settings.Version != "1" {
|
||||
return config, ErrUnknownVersion
|
||||
}
|
||||
if len(settings.Packages) == 0 {
|
||||
return config, ErrNoPackages
|
||||
}
|
||||
if err := settings.ValidateGlobalOverrides(); err != nil {
|
||||
return config, err
|
||||
}
|
||||
for j := range settings.Packages {
|
||||
if settings.Packages[j].Path == "" {
|
||||
return config, ErrNoPackagePath
|
||||
}
|
||||
|
||||
if settings.Packages[j].Name == "" {
|
||||
settings.Packages[j].Name = filepath.Base(settings.Packages[j].Path)
|
||||
}
|
||||
|
||||
if settings.Packages[j].Engine == "" {
|
||||
settings.Packages[j].Engine = EnginePostgreSQL
|
||||
}
|
||||
}
|
||||
|
||||
return settings.Translate(), nil
|
||||
}
|
||||
|
||||
func (c *V1GenerateSettings) ValidateGlobalOverrides() error {
|
||||
engines := map[Engine]struct{}{}
|
||||
for _, pkg := range c.Packages {
|
||||
if _, ok := engines[pkg.Engine]; !ok {
|
||||
engines[pkg.Engine] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
usesMultipleEngines := len(engines) > 1
|
||||
for _, oride := range c.Overrides {
|
||||
if usesMultipleEngines && oride.Engine == "" {
|
||||
return fmt.Errorf(`the "engine" field is required for global type overrides because your configuration uses multiple database engines`)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *V1GenerateSettings) Translate() Config {
|
||||
conf := Config{
|
||||
Version: c.Version,
|
||||
Cloud: c.Cloud,
|
||||
Rules: c.Rules,
|
||||
}
|
||||
|
||||
for _, pkg := range c.Packages {
|
||||
if pkg.StrictOrderBy == nil {
|
||||
defaultValue := true
|
||||
pkg.StrictOrderBy = &defaultValue
|
||||
}
|
||||
conf.SQL = append(conf.SQL, SQL{
|
||||
Name: pkg.Name,
|
||||
Engine: pkg.Engine,
|
||||
Database: pkg.Database,
|
||||
Schema: pkg.Schema,
|
||||
Queries: pkg.Queries,
|
||||
Rules: pkg.Rules,
|
||||
Analyzer: pkg.Analyzer,
|
||||
Gen: SQLGen{
|
||||
Go: &golang.Options{
|
||||
EmitInterface: pkg.EmitInterface,
|
||||
EmitJsonTags: pkg.EmitJSONTags,
|
||||
JsonTagsIdUppercase: pkg.JsonTagsIDUppercase,
|
||||
EmitDbTags: pkg.EmitDBTags,
|
||||
EmitPreparedQueries: pkg.EmitPreparedQueries,
|
||||
EmitExactTableNames: pkg.EmitExactTableNames,
|
||||
EmitEmptySlices: pkg.EmitEmptySlices,
|
||||
EmitExportedQueries: pkg.EmitExportedQueries,
|
||||
EmitResultStructPointers: pkg.EmitResultStructPointers,
|
||||
EmitParamsStructPointers: pkg.EmitParamsStructPointers,
|
||||
EmitMethodsWithDbArgument: pkg.EmitMethodsWithDBArgument,
|
||||
EmitPointersForNullTypes: pkg.EmitPointersForNullTypes,
|
||||
EmitPointersForNullEnumTypes: pkg.EmitPointersForNullEnumTypes,
|
||||
EmitEnumValidMethod: pkg.EmitEnumValidMethod,
|
||||
EmitAllEnumValues: pkg.EmitAllEnumValues,
|
||||
EmitSqlAsComment: pkg.EmitSqlAsComment,
|
||||
Package: pkg.Name,
|
||||
Out: pkg.Path,
|
||||
SqlPackage: pkg.SQLPackage,
|
||||
SqlDriver: pkg.SQLDriver,
|
||||
Overrides: pkg.Overrides,
|
||||
JsonTagsCaseStyle: pkg.JSONTagsCaseStyle,
|
||||
OutputBatchFileName: pkg.OutputBatchFileName,
|
||||
OutputDbFileName: pkg.OutputDBFileName,
|
||||
OutputModelsFileName: pkg.OutputModelsFileName,
|
||||
OutputModelsPath: pkg.OutputModelsPath,
|
||||
OutputModelsPackage: pkg.OutputModelsPackage,
|
||||
OutputModelsImport: pkg.OutputModelsImport,
|
||||
OutputModelsEmit: pkg.OutputModelsEmit,
|
||||
OutputQuerierFileName: pkg.OutputQuerierFileName,
|
||||
OutputCopyfromFileName: pkg.OutputCopyFromFileName,
|
||||
OutputFilesSuffix: pkg.OutputFilesSuffix,
|
||||
QueryParameterLimit: pkg.QueryParameterLimit,
|
||||
OmitSqlcVersion: pkg.OmitSqlcVersion,
|
||||
OmitUnusedStructs: pkg.OmitUnusedStructs,
|
||||
BuildTags: pkg.BuildTags,
|
||||
},
|
||||
},
|
||||
StrictFunctionChecks: pkg.StrictFunctionChecks,
|
||||
StrictOrderBy: pkg.StrictOrderBy,
|
||||
})
|
||||
}
|
||||
|
||||
if len(c.Overrides) > 0 || len(c.Rename) > 0 {
|
||||
conf.Overrides.Go = &golang.GlobalOptions{
|
||||
Overrides: c.Overrides,
|
||||
Rename: c.Rename,
|
||||
}
|
||||
}
|
||||
|
||||
return conf
|
||||
}
|
||||
361
internal/config/v_one.json
Normal file
361
internal/config/v_one.json
Normal file
@@ -0,0 +1,361 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"version"
|
||||
],
|
||||
"properties": {
|
||||
"version": {
|
||||
"const": "1"
|
||||
},
|
||||
"cloud": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"organization": {
|
||||
"type": "string"
|
||||
},
|
||||
"project": {
|
||||
"type": "string"
|
||||
},
|
||||
"hostname": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"packages": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"engine"
|
||||
],
|
||||
"properties": {
|
||||
"engine": {
|
||||
"enum": [
|
||||
"postgresql",
|
||||
"mysql",
|
||||
"sqlite"
|
||||
]
|
||||
},
|
||||
"schema": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"queries": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"database": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"uri": {
|
||||
"type": "string"
|
||||
},
|
||||
"managed": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
"analyzer": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"database": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
"strict_function_checks": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"strict_order_by": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"emit_interface": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"emit_json_tags": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"json_tags_id_uppercase": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"emit_db_tags": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"emit_prepared_queries": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"emit_exact_table_names": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"emit_empty_slices": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"emit_exported_queries": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"emit_result_struct_pointers": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"emit_params_struct_pointers": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"emit_methods_with_db_argument": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"emit_pointers_for_null_types": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"emit_pointers_for_null_enum_types": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"emit_enum_valid_method": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"emit_all_enum_values": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"emit_sql_as_comment": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"build_tags": {
|
||||
"type": "string"
|
||||
},
|
||||
"json_tags_case_style": {
|
||||
"type": "string"
|
||||
},
|
||||
"package": {
|
||||
"type": "string"
|
||||
},
|
||||
"out": {
|
||||
"type": "string"
|
||||
},
|
||||
"overrides": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"go_type": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"import": {
|
||||
"type": "string"
|
||||
},
|
||||
"package": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"type": "string"
|
||||
},
|
||||
"pointer": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"slice": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"spec": {
|
||||
"type": "string"
|
||||
},
|
||||
"builtin": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
"go_struct_tag": {
|
||||
"type": "string"
|
||||
},
|
||||
"db_type": {
|
||||
"type": "string"
|
||||
},
|
||||
"engine": {
|
||||
"enum": [
|
||||
"postgresql",
|
||||
"mysql",
|
||||
"sqlite"
|
||||
]
|
||||
},
|
||||
"nullable": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"unsigned": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"column": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"sql_package": {
|
||||
"type": "string"
|
||||
},
|
||||
"sql_driver": {
|
||||
"type": "string"
|
||||
},
|
||||
"output_batch_file_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"output_db_file_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"output_models_file_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"output_models_path": {
|
||||
"type": "string"
|
||||
},
|
||||
"output_models_package": {
|
||||
"type": "string"
|
||||
},
|
||||
"output_models_import": {
|
||||
"type": "string"
|
||||
},
|
||||
"output_models_emit": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"output_querier_file_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"output_copyfrom_file_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"output_files_suffix": {
|
||||
"type": "string"
|
||||
},
|
||||
"inflection_exclude_table_names": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"query_parameter_limit": {
|
||||
"type": "integer"
|
||||
},
|
||||
"omit_unused_structs": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"rules": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"overrides": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"go_type": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"import": {
|
||||
"type": "string"
|
||||
},
|
||||
"package": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"type": "string"
|
||||
},
|
||||
"pointer": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"slice": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"spec": {
|
||||
"type": "string"
|
||||
},
|
||||
"builtin": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
"go_struct_tag": {
|
||||
"type": "string"
|
||||
},
|
||||
"db_type": {
|
||||
"type": "string"
|
||||
},
|
||||
"engine": {
|
||||
"enum": [
|
||||
"postgresql",
|
||||
"mysql",
|
||||
"sqlite"
|
||||
]
|
||||
},
|
||||
"nullable": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"unsigned": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"column": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"rename": {
|
||||
"type": "object",
|
||||
"patternProperties": {
|
||||
".*": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"rules": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"rule": {
|
||||
"type": "string"
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
109
internal/config/v_two.go
Normal file
109
internal/config/v_two.go
Normal file
@@ -0,0 +1,109 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
yaml "gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
func v2ParseConfig(rd io.Reader) (Config, error) {
|
||||
dec := yaml.NewDecoder(rd)
|
||||
dec.KnownFields(true)
|
||||
var conf Config
|
||||
if err := dec.Decode(&conf); err != nil {
|
||||
return conf, err
|
||||
}
|
||||
if conf.Version == "" {
|
||||
return conf, ErrMissingVersion
|
||||
}
|
||||
if conf.Version != "2" {
|
||||
return conf, ErrUnknownVersion
|
||||
}
|
||||
if len(conf.SQL) == 0 {
|
||||
return conf, ErrNoPackages
|
||||
}
|
||||
if err := conf.validateGlobalOverrides(); err != nil {
|
||||
return conf, err
|
||||
}
|
||||
// TODO: Store built-in plugins somewhere else
|
||||
builtins := map[string]struct{}{
|
||||
"go": {},
|
||||
"json": {},
|
||||
}
|
||||
plugins := map[string]struct{}{}
|
||||
for i := range conf.Plugins {
|
||||
if conf.Plugins[i].Name == "" {
|
||||
return conf, ErrPluginNoName
|
||||
}
|
||||
if _, ok := builtins[conf.Plugins[i].Name]; ok {
|
||||
return conf, ErrPluginBuiltin
|
||||
}
|
||||
if _, ok := plugins[conf.Plugins[i].Name]; ok {
|
||||
return conf, ErrPluginExists
|
||||
}
|
||||
if conf.Plugins[i].Process == nil && conf.Plugins[i].WASM == nil {
|
||||
return conf, ErrPluginNoType
|
||||
}
|
||||
if conf.Plugins[i].Process != nil && conf.Plugins[i].WASM != nil {
|
||||
return conf, ErrPluginBothTypes
|
||||
}
|
||||
if conf.Plugins[i].Process != nil {
|
||||
if conf.Plugins[i].Process.Cmd == "" {
|
||||
return conf, ErrPluginProcessNoCmd
|
||||
}
|
||||
}
|
||||
plugins[conf.Plugins[i].Name] = struct{}{}
|
||||
}
|
||||
for j := range conf.SQL {
|
||||
if conf.SQL[j].Engine == "" {
|
||||
return conf, ErrMissingEngine
|
||||
}
|
||||
if conf.SQL[j].Gen.Go != nil {
|
||||
if conf.SQL[j].Gen.Go.Out == "" {
|
||||
return conf, ErrNoPackagePath
|
||||
}
|
||||
}
|
||||
if conf.SQL[j].Gen.JSON != nil {
|
||||
if conf.SQL[j].Gen.JSON.Out == "" {
|
||||
return conf, ErrNoOutPath
|
||||
}
|
||||
}
|
||||
for _, cg := range conf.SQL[j].Codegen {
|
||||
if cg.Plugin == "" {
|
||||
return conf, ErrPluginNoName
|
||||
}
|
||||
if cg.Out == "" {
|
||||
return conf, ErrNoOutPath
|
||||
}
|
||||
// TODO: Allow the use of built-in codegen from here
|
||||
if _, ok := plugins[cg.Plugin]; !ok {
|
||||
return conf, ErrPluginNotFound
|
||||
}
|
||||
}
|
||||
if conf.SQL[j].StrictOrderBy == nil {
|
||||
defaultValidate := true
|
||||
conf.SQL[j].StrictOrderBy = &defaultValidate
|
||||
}
|
||||
}
|
||||
return conf, nil
|
||||
}
|
||||
|
||||
func (c *Config) validateGlobalOverrides() error {
|
||||
engines := map[Engine]struct{}{}
|
||||
for _, pkg := range c.SQL {
|
||||
if _, ok := engines[pkg.Engine]; !ok {
|
||||
engines[pkg.Engine] = struct{}{}
|
||||
}
|
||||
}
|
||||
if c.Overrides.Go == nil {
|
||||
return nil
|
||||
}
|
||||
usesMultipleEngines := len(engines) > 1
|
||||
for _, oride := range c.Overrides.Go.Overrides {
|
||||
if usesMultipleEngines && oride.Engine == "" {
|
||||
return fmt.Errorf(`the "engine" field is required for global type overrides because your configuration uses multiple database engines`)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
463
internal/config/v_two.json
Normal file
463
internal/config/v_two.json
Normal file
@@ -0,0 +1,463 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"version"
|
||||
],
|
||||
"properties": {
|
||||
"version": {
|
||||
"const": "2"
|
||||
},
|
||||
"cloud": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"organization": {
|
||||
"type": "string"
|
||||
},
|
||||
"project": {
|
||||
"type": "string"
|
||||
},
|
||||
"hostname": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"sql": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"engine"
|
||||
],
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"engine": {
|
||||
"enum": [
|
||||
"postgresql",
|
||||
"mysql",
|
||||
"sqlite"
|
||||
]
|
||||
},
|
||||
"schema": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"queries": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"database": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"uri": {
|
||||
"type": "string"
|
||||
},
|
||||
"managed": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
"analyzer": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"database": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
"strict_function_checks": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"strict_order_by": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"gen": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"go": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"emit_interface": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"emit_json_tags": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"json_tags_id_uppercase": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"emit_db_tags": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"emit_prepared_queries": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"emit_exact_table_names": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"emit_empty_slices": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"emit_exported_queries": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"emit_result_struct_pointers": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"emit_params_struct_pointers": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"emit_methods_with_db_argument": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"emit_pointers_for_null_types": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"emit_pointers_for_null_enum_types": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"emit_enum_valid_method": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"emit_all_enum_values": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"emit_sql_as_comment": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"build_tags": {
|
||||
"type": "string"
|
||||
},
|
||||
"json_tags_case_style": {
|
||||
"type": "string"
|
||||
},
|
||||
"package": {
|
||||
"type": "string"
|
||||
},
|
||||
"out": {
|
||||
"type": "string"
|
||||
},
|
||||
"overrides": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"go_type": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"import": {
|
||||
"type": "string"
|
||||
},
|
||||
"package": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"type": "string"
|
||||
},
|
||||
"pointer": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"slice": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"spec": {
|
||||
"type": "string"
|
||||
},
|
||||
"builtin": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
"go_struct_tag": {
|
||||
"type": "string"
|
||||
},
|
||||
"db_type": {
|
||||
"type": "string"
|
||||
},
|
||||
"engine": {
|
||||
"enum": [
|
||||
"postgresql",
|
||||
"mysql",
|
||||
"sqlite"
|
||||
]
|
||||
},
|
||||
"nullable": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"unsigned": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"column": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"rename": {
|
||||
"type": "object",
|
||||
"patternProperties": {
|
||||
".*": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"sql_package": {
|
||||
"type": "string"
|
||||
},
|
||||
"sql_driver": {
|
||||
"type": "string"
|
||||
},
|
||||
"output_batch_file_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"output_db_file_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"output_models_file_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"output_models_path": {
|
||||
"type": "string"
|
||||
},
|
||||
"output_models_package": {
|
||||
"type": "string"
|
||||
},
|
||||
"output_models_import": {
|
||||
"type": "string"
|
||||
},
|
||||
"output_models_emit": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"output_querier_file_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"output_copyfrom_file_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"output_files_suffix": {
|
||||
"type": "string"
|
||||
},
|
||||
"inflection_exclude_table_names": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"query_parameter_limit": {
|
||||
"type": "integer"
|
||||
},
|
||||
"omit_unused_structs": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"json": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"out": {
|
||||
"type": "string"
|
||||
},
|
||||
"indent": {
|
||||
"type": "string"
|
||||
},
|
||||
"filename": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"codegen": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"out": {
|
||||
"type": "string"
|
||||
},
|
||||
"plugin": {
|
||||
"type": "string"
|
||||
},
|
||||
"options": {
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"rules": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"overrides": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"go": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"overrides": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"go_type": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"import": {
|
||||
"type": "string"
|
||||
},
|
||||
"package": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"type": "string"
|
||||
},
|
||||
"pointer": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"slice": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"spec": {
|
||||
"type": "string"
|
||||
},
|
||||
"builtin": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
"go_struct_tag": {
|
||||
"type": "string"
|
||||
},
|
||||
"db_type": {
|
||||
"type": "string"
|
||||
},
|
||||
"engine": {
|
||||
"enum": [
|
||||
"postgresql",
|
||||
"mysql",
|
||||
"sqlite"
|
||||
]
|
||||
},
|
||||
"nullable": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"unsigned": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"column": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"rename": {
|
||||
"type": "object",
|
||||
"patternProperties": {
|
||||
".*": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"plugins": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"name"
|
||||
],
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"env": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"process": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"cmd": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"wasm": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"url": {
|
||||
"type": "string"
|
||||
},
|
||||
"sha256": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"rules": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"rule": {
|
||||
"type": "string"
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
12
internal/config/validate.go
Normal file
12
internal/config/validate.go
Normal file
@@ -0,0 +1,12 @@
|
||||
package config
|
||||
|
||||
func Validate(c *Config) error {
|
||||
for _, sql := range c.SQL {
|
||||
if sql.Database != nil {
|
||||
if sql.Database.URI == "" && !sql.Database.Managed {
|
||||
return ErrInvalidDatabase
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
12
internal/constants/query.go
Normal file
12
internal/constants/query.go
Normal file
@@ -0,0 +1,12 @@
|
||||
package constants
|
||||
|
||||
// Flags
|
||||
const (
|
||||
QueryFlagParam = "@param"
|
||||
QueryFlagSqlcVetDisable = "@sqlc-vet-disable"
|
||||
)
|
||||
|
||||
// Rules
|
||||
const (
|
||||
QueryRuleDbPrepare = "sqlc/db-prepare"
|
||||
)
|
||||
82
internal/core/analysis.go
Normal file
82
internal/core/analysis.go
Normal file
@@ -0,0 +1,82 @@
|
||||
package core
|
||||
|
||||
type Command string
|
||||
|
||||
const (
|
||||
CommandSelect Command = "SELECT"
|
||||
CommandInsert Command = "INSERT"
|
||||
CommandUpdate Command = "UPDATE"
|
||||
CommandDelete Command = "DELETE"
|
||||
)
|
||||
|
||||
type PrepareResult struct {
|
||||
Command Command `json:"command,omitempty"`
|
||||
Columns []Column `json:"columns"`
|
||||
Parameters []Parameter `json:"parameters"`
|
||||
Stars []StarExpansion `json:"stars,omitempty"`
|
||||
}
|
||||
|
||||
// StarExpansion is what a star in a target list stands for. The analyzer
|
||||
// resolves the reference against the query's scope and reports the columns it
|
||||
// covers; rewriting the query text with them is the caller's to do, since only
|
||||
// it knows how the engine quotes an identifier.
|
||||
type StarExpansion struct {
|
||||
// Location is where the target the star belongs to starts, measured the
|
||||
// way the AST measures a node: from the beginning of the file the
|
||||
// statement was parsed from.
|
||||
Location int `json:"location"`
|
||||
|
||||
// Fields is the reference as it was written, with the star as its last
|
||||
// element: ["*"] for a bare star and ["foo", "*"] for a qualified one.
|
||||
Fields []string `json:"fields"`
|
||||
|
||||
// Alias is the output name the target was given, if any.
|
||||
Alias string `json:"alias,omitempty"`
|
||||
|
||||
Columns []StarColumn `json:"columns"`
|
||||
}
|
||||
|
||||
// StarColumn is a single column a star expanded to.
|
||||
type StarColumn struct {
|
||||
// Relation is the name the column's relation goes by in the query, which
|
||||
// is its alias when it was given one.
|
||||
Relation string `json:"relation,omitempty"`
|
||||
Name string `json:"name"`
|
||||
DataType string `json:"data_type,omitempty"`
|
||||
}
|
||||
|
||||
type ColumnSource struct {
|
||||
Schema string `json:"schema,omitempty"`
|
||||
Table string `json:"table,omitempty"`
|
||||
TableAlias string `json:"table_alias,omitempty"`
|
||||
Column string `json:"column,omitempty"`
|
||||
}
|
||||
|
||||
type Column struct {
|
||||
Name string `json:"name"`
|
||||
DataType string `json:"data_type"`
|
||||
// Type is the column's type as an expression, carrying what DataType
|
||||
// and IsArray flatten away: arguments, nesting and inner nullability.
|
||||
Type *TypeExpr `json:"type,omitempty"`
|
||||
TypeOID int64 `json:"type_oid,omitempty"`
|
||||
NotNull bool `json:"not_null"`
|
||||
IsArray bool `json:"is_array,omitempty"`
|
||||
SourceClassOID int64 `json:"source_class_oid,omitempty"`
|
||||
SourceAttributeOID int64 `json:"source_attribute_oid,omitempty"`
|
||||
Source *ColumnSource `json:"source,omitempty"`
|
||||
DeclType string `json:"decl_type,omitempty"`
|
||||
IsPrimaryKey bool `json:"is_primary_key,omitempty"`
|
||||
IsUnique bool `json:"is_unique,omitempty"`
|
||||
IsAutoIncrement bool `json:"is_auto_increment,omitempty"`
|
||||
}
|
||||
|
||||
type Parameter struct {
|
||||
Number int `json:"number"`
|
||||
Name string `json:"name,omitempty"`
|
||||
DataType string `json:"data_type,omitempty"`
|
||||
Type *TypeExpr `json:"type,omitempty"`
|
||||
TypeOID int64 `json:"type_oid,omitempty"`
|
||||
NotNull bool `json:"not_null"`
|
||||
IsArray bool `json:"is_array,omitempty"`
|
||||
Source *ColumnSource `json:"source,omitempty"`
|
||||
}
|
||||
389
internal/core/analyzer/analyzer.go
Normal file
389
internal/core/analyzer/analyzer.go
Normal file
@@ -0,0 +1,389 @@
|
||||
package analyzer
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/sqlc-dev/sqlc/internal/core"
|
||||
"github.com/sqlc-dev/sqlc/internal/sql/ast"
|
||||
)
|
||||
|
||||
func Prepare(cat *core.Catalog, stmt ast.Node) (core.PrepareResult, error) {
|
||||
if rs, ok := stmt.(*ast.RawStmt); ok {
|
||||
stmt = rs.Stmt
|
||||
}
|
||||
a := &analyzer{
|
||||
cat: cat,
|
||||
params: map[int]core.Parameter{},
|
||||
stars: &[]core.StarExpansion{},
|
||||
}
|
||||
switch s := stmt.(type) {
|
||||
case *ast.SelectStmt:
|
||||
if err := a.analyzeSelect(s); err != nil {
|
||||
return core.PrepareResult{}, err
|
||||
}
|
||||
a.command = core.CommandSelect
|
||||
case *ast.InsertStmt:
|
||||
if err := a.analyzeInsert(s); err != nil {
|
||||
return core.PrepareResult{}, err
|
||||
}
|
||||
a.command = core.CommandInsert
|
||||
case *ast.UpdateStmt:
|
||||
if err := a.analyzeUpdate(s); err != nil {
|
||||
return core.PrepareResult{}, err
|
||||
}
|
||||
a.command = core.CommandUpdate
|
||||
case *ast.DeleteStmt:
|
||||
if err := a.analyzeDelete(s); err != nil {
|
||||
return core.PrepareResult{}, err
|
||||
}
|
||||
a.command = core.CommandDelete
|
||||
default:
|
||||
return core.PrepareResult{}, fmt.Errorf("analyzer: unsupported statement %T", stmt)
|
||||
}
|
||||
return a.result(), nil
|
||||
}
|
||||
|
||||
type analyzer struct {
|
||||
cat *core.Catalog
|
||||
scope *scope
|
||||
columns []core.Column
|
||||
params map[int]core.Parameter
|
||||
command core.Command
|
||||
|
||||
// outer is the scope of the query this one is nested in, which a
|
||||
// correlated subquery refers to.
|
||||
outer *scope
|
||||
|
||||
// ctes are the relations a WITH clause defined, visible to this query and
|
||||
// to the ones nested in it.
|
||||
ctes map[string]scopeRel
|
||||
|
||||
// aliases are the names the target list gives its expressions. GROUP BY,
|
||||
// HAVING and ORDER BY may refer to a result column by its output name.
|
||||
aliases map[string]ast.Node
|
||||
|
||||
// resolving guards against an alias that refers to itself.
|
||||
resolving map[string]bool
|
||||
|
||||
// stars are the expansions every star in the statement asked for, shared
|
||||
// with the analyzers of the queries nested in it so one statement reports
|
||||
// all of them.
|
||||
stars *[]core.StarExpansion
|
||||
}
|
||||
|
||||
func (a *analyzer) recordStar(s core.StarExpansion) {
|
||||
if a.stars == nil {
|
||||
return
|
||||
}
|
||||
*a.stars = append(*a.stars, s)
|
||||
}
|
||||
|
||||
// subquery analyzes a nested SELECT. It shares the parameter set, so a
|
||||
// placeholder inside the subquery is reported with the rest, and it sees this
|
||||
// query's scope, so a correlated reference resolves.
|
||||
func (a *analyzer) subquery(s *ast.SelectStmt) (*analyzer, error) {
|
||||
sub := &analyzer{
|
||||
cat: a.cat,
|
||||
params: a.params,
|
||||
outer: a.scope,
|
||||
ctes: a.ctes,
|
||||
stars: a.stars,
|
||||
}
|
||||
if err := sub.analyzeSelect(s); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return sub, nil
|
||||
}
|
||||
|
||||
func (a *analyzer) subqueryColumns(s *ast.SelectStmt) ([]core.Column, error) {
|
||||
sub, err := a.subquery(s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return sub.columns, nil
|
||||
}
|
||||
|
||||
// derivedRel turns a subquery's result columns into a relation, so that the
|
||||
// query selecting from it resolves columns the same way as from a table.
|
||||
func derivedRel(alias string, cols []core.Column) scopeRel {
|
||||
rel := scopeRel{alias: alias, cols: make([]core.ClassColumn, 0, len(cols))}
|
||||
for _, col := range cols {
|
||||
rel.cols = append(rel.cols, core.ClassColumn{
|
||||
AttOID: col.SourceAttributeOID,
|
||||
Name: col.Name,
|
||||
TypeOID: col.TypeOID,
|
||||
Type: col.Type.WithNullable(false),
|
||||
NotNull: col.NotNull,
|
||||
})
|
||||
}
|
||||
return rel
|
||||
}
|
||||
|
||||
func (a *analyzer) result() core.PrepareResult {
|
||||
// A placeholder nothing constrained takes the dialect's type for one, when
|
||||
// the dialect has such a type.
|
||||
if oid, ok := a.cat.UntypedTypeOID(); ok {
|
||||
for n, p := range a.params {
|
||||
if p.TypeOID == 0 && p.Type == nil {
|
||||
t := exprType{typeOID: oid, nullable: true}
|
||||
p.TypeOID = oid
|
||||
p.DataType, p.IsArray = a.typeNameOf(t)
|
||||
p.NotNull = false
|
||||
p.Type = a.typeExprOf(t)
|
||||
a.params[n] = p
|
||||
}
|
||||
}
|
||||
}
|
||||
res := core.PrepareResult{
|
||||
Command: a.command,
|
||||
Columns: a.columns,
|
||||
Parameters: orderedParams(a.params),
|
||||
}
|
||||
if a.stars != nil {
|
||||
res.Stars = *a.stars
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func orderedParams(m map[int]core.Parameter) []core.Parameter {
|
||||
if len(m) == 0 {
|
||||
return nil
|
||||
}
|
||||
maxN := 0
|
||||
for n := range m {
|
||||
if n > maxN {
|
||||
maxN = n
|
||||
}
|
||||
}
|
||||
out := make([]core.Parameter, 0, len(m))
|
||||
for i := 1; i <= maxN; i++ {
|
||||
if p, ok := m[i]; ok {
|
||||
out = append(out, p)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (a *analyzer) analyzeSelect(s *ast.SelectStmt) error {
|
||||
if err := a.bindCTEs(s.WithClause); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// A set operation reports the columns of its first branch. The other
|
||||
// branches still get analyzed, for the placeholders they hold.
|
||||
if s.Larg != nil && s.Rarg != nil {
|
||||
if err := a.analyzeSetOperation(s); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
sc, err := a.buildScope(s.FromClause)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
a.scope = sc
|
||||
|
||||
a.bindAliases(s.TargetList)
|
||||
|
||||
for _, item := range listItems(s.FromClause) {
|
||||
if err := a.typeJoinConditions(item); err != nil {
|
||||
return fmt.Errorf("join: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if s.WhereClause != nil {
|
||||
if _, err := a.typeExpr(s.WhereClause); err != nil {
|
||||
return fmt.Errorf("where: %w", err)
|
||||
}
|
||||
}
|
||||
if items := listItems(s.GroupClause); items != nil {
|
||||
for _, g := range items {
|
||||
if _, err := a.typeExpr(g); err != nil {
|
||||
return fmt.Errorf("group by: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
if s.HavingClause != nil {
|
||||
if _, err := a.typeExpr(s.HavingClause); err != nil {
|
||||
return fmt.Errorf("having: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
targets := listItems(s.TargetList)
|
||||
if targets == nil {
|
||||
// A VALUES list produces rows without naming columns.
|
||||
if s.ValuesLists != nil {
|
||||
return a.typeValuesLists(s.ValuesLists)
|
||||
}
|
||||
return fmt.Errorf("select: empty target list")
|
||||
}
|
||||
a.columns = make([]core.Column, 0, len(targets))
|
||||
for _, t := range targets {
|
||||
rt, ok := t.(*ast.ResTarget)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if err := a.projectTarget(rt); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for _, item := range listItems(s.SortClause) {
|
||||
if sb, ok := item.(*ast.SortBy); ok {
|
||||
if _, err := a.typeExpr(sb.Node); err != nil {
|
||||
return fmt.Errorf("order by: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, n := range []ast.Node{s.LimitCount, s.LimitOffset} {
|
||||
if err := a.typeLimit(n); err != nil {
|
||||
return fmt.Errorf("limit: %w", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// typeLimit types a LIMIT or OFFSET count. A bare placeholder there holds
|
||||
// whatever the dialect counts rows in.
|
||||
func (a *analyzer) typeLimit(n ast.Node) error {
|
||||
if n == nil {
|
||||
return nil
|
||||
}
|
||||
if pr, ok := n.(*ast.ParamRef); ok {
|
||||
oid, err := a.cat.LimitTypeOID()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
a.inferParam(pr.Number, exprType{typeOID: oid})
|
||||
return nil
|
||||
}
|
||||
_, err := a.typeExpr(n)
|
||||
return err
|
||||
}
|
||||
|
||||
func (a *analyzer) typeValuesLists(l *ast.List) error {
|
||||
for _, row := range listItems(l) {
|
||||
if _, err := a.typeExpr(row); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// analyzeSetOperation types both sides of UNION, INTERSECT or EXCEPT and
|
||||
// reports the first branch's columns, the way the databases name the result.
|
||||
func (a *analyzer) analyzeSetOperation(s *ast.SelectStmt) error {
|
||||
left, err := a.subquery(s.Larg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := a.subquery(s.Rarg); err != nil {
|
||||
return err
|
||||
}
|
||||
a.columns = left.columns
|
||||
a.scope = left.scope
|
||||
return nil
|
||||
}
|
||||
|
||||
// bindCTEs analyzes a WITH clause, making each common table expression
|
||||
// available to the query as a relation.
|
||||
func (a *analyzer) bindCTEs(with *ast.WithClause) error {
|
||||
if with == nil {
|
||||
return nil
|
||||
}
|
||||
for _, item := range listItems(with.Ctes) {
|
||||
cte, ok := item.(*ast.CommonTableExpr)
|
||||
if !ok || cte.Ctename == nil {
|
||||
continue
|
||||
}
|
||||
sel, ok := cte.Ctequery.(*ast.SelectStmt)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
cols, err := a.subqueryColumns(sel)
|
||||
if err != nil {
|
||||
return fmt.Errorf("with %s: %w", *cte.Ctename, err)
|
||||
}
|
||||
rel := derivedRel(*cte.Ctename, cols)
|
||||
renameColumns(&rel, cte.Aliascolnames)
|
||||
if a.ctes == nil {
|
||||
a.ctes = map[string]scopeRel{}
|
||||
}
|
||||
a.ctes[*cte.Ctename] = rel
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// bindAliases records the output names the target list assigns, so a later
|
||||
// clause can refer to a result column by name.
|
||||
func (a *analyzer) bindAliases(targets *ast.List) {
|
||||
for _, item := range listItems(targets) {
|
||||
rt, ok := item.(*ast.ResTarget)
|
||||
if !ok || rt.Name == nil || *rt.Name == "" {
|
||||
continue
|
||||
}
|
||||
if a.aliases == nil {
|
||||
a.aliases = map[string]ast.Node{}
|
||||
}
|
||||
a.aliases[*rt.Name] = rt.Val
|
||||
}
|
||||
}
|
||||
|
||||
// typeAlias types the expression an output name stands for, which is how a
|
||||
// GROUP BY or HAVING clause refers to a result column.
|
||||
func (a *analyzer) typeAlias(name string) (exprType, bool, error) {
|
||||
val, ok := a.aliases[name]
|
||||
if !ok || a.resolving[name] {
|
||||
return exprType{}, false, nil
|
||||
}
|
||||
if a.resolving == nil {
|
||||
a.resolving = map[string]bool{}
|
||||
}
|
||||
a.resolving[name] = true
|
||||
defer delete(a.resolving, name)
|
||||
|
||||
t, err := a.typeExpr(val)
|
||||
if err != nil {
|
||||
return exprType{}, false, err
|
||||
}
|
||||
return t, true, nil
|
||||
}
|
||||
|
||||
// renameColumns applies the column aliases a derived relation was declared
|
||||
// with, as in "WITH t(a, b) AS (...)".
|
||||
func renameColumns(rel *scopeRel, names *ast.List) {
|
||||
for i, item := range listItems(names) {
|
||||
if i >= len(rel.cols) {
|
||||
break
|
||||
}
|
||||
if s, ok := item.(*ast.String); ok && s.Str != "" {
|
||||
rel.cols[i].Name = s.Str
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func listItems(l *ast.List) []ast.Node {
|
||||
if l == nil {
|
||||
return nil
|
||||
}
|
||||
return l.Items
|
||||
}
|
||||
|
||||
func (a *analyzer) typeJoinConditions(item ast.Node) error {
|
||||
je, ok := item.(*ast.JoinExpr)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
if err := a.typeJoinConditions(je.Larg); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := a.typeJoinConditions(je.Rarg); err != nil {
|
||||
return err
|
||||
}
|
||||
if je.Quals != nil {
|
||||
if _, err := a.typeExpr(je.Quals); err != nil {
|
||||
return fmt.Errorf("ON: %w", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
110
internal/core/analyzer/bench_test.go
Normal file
110
internal/core/analyzer/bench_test.go
Normal file
@@ -0,0 +1,110 @@
|
||||
package analyzer_test
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/sqlc-dev/sqlc/internal/core"
|
||||
"github.com/sqlc-dev/sqlc/internal/core/analyzer"
|
||||
coreschema "github.com/sqlc-dev/sqlc/internal/core/schema"
|
||||
"github.com/sqlc-dev/sqlc/internal/engine/googlesql"
|
||||
"github.com/sqlc-dev/sqlc/internal/sql/ast"
|
||||
)
|
||||
|
||||
const benchSchema = `
|
||||
CREATE TABLE users (
|
||||
id INT64 NOT NULL,
|
||||
name STRING NOT NULL,
|
||||
bio STRING,
|
||||
) PRIMARY KEY (id);
|
||||
|
||||
CREATE TABLE posts (
|
||||
id INT64 NOT NULL,
|
||||
user_id INT64 NOT NULL,
|
||||
title STRING(255),
|
||||
created TIMESTAMP NOT NULL,
|
||||
) PRIMARY KEY (id);
|
||||
`
|
||||
|
||||
const benchQueries = `
|
||||
SELECT * FROM users;
|
||||
SELECT COUNT(*) AS total FROM users;
|
||||
SELECT u.name, p.* FROM users u JOIN posts p ON p.user_id = u.id WHERE u.name = @name;
|
||||
SELECT id, name, bio FROM users WHERE id = @id AND name = @name;
|
||||
SELECT p.title, p.created FROM posts p WHERE p.user_id = @uid AND p.title = @t;
|
||||
`
|
||||
|
||||
func parseAll(t testing.TB, src string) []ast.Node {
|
||||
p := googlesql.NewParser()
|
||||
stmts, err := p.Parse(strings.NewReader(src))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
out := make([]ast.Node, 0, len(stmts))
|
||||
for i := range stmts {
|
||||
out = append(out, stmts[i].Raw)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// parseQueries mirrors the compiler pipeline. GoogleSQL is not preprocessed —
|
||||
// "@name" is native syntax the converter turns into a ParamRef — so parsing is
|
||||
// all it takes.
|
||||
func parseQueries(t testing.TB, src string) []ast.Node {
|
||||
return parseAll(t, src)
|
||||
}
|
||||
|
||||
func newBenchCatalog(t testing.TB) (*core.Catalog, []ast.Node) {
|
||||
cat, err := core.New(googlesql.Dialect())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, n := range parseAll(t, benchSchema) {
|
||||
if err := coreschema.Apply(cat, n); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
return cat, parseQueries(t, benchQueries)
|
||||
}
|
||||
|
||||
// BenchmarkCatalogNew measures per-compile catalog setup: open the SQLite
|
||||
// catalog, install the schema, and run the dialect seed.
|
||||
func BenchmarkCatalogNew(b *testing.B) {
|
||||
for b.Loop() {
|
||||
cat, err := core.New(googlesql.Dialect())
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
cat.Close()
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkApplySchema measures loading user DDL into the catalog.
|
||||
func BenchmarkApplySchema(b *testing.B) {
|
||||
stmts := parseAll(b, benchSchema)
|
||||
for b.Loop() {
|
||||
cat, err := core.New(googlesql.Dialect())
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
for _, n := range stmts {
|
||||
if err := coreschema.Apply(cat, n); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
cat.Close()
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkPrepare measures steady-state query analysis against a warm catalog.
|
||||
func BenchmarkPrepare(b *testing.B) {
|
||||
cat, queries := newBenchCatalog(b)
|
||||
defer cat.Close()
|
||||
for b.Loop() {
|
||||
for _, q := range queries {
|
||||
if _, err := analyzer.Prepare(cat, q); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
211
internal/core/analyzer/dml.go
Normal file
211
internal/core/analyzer/dml.go
Normal file
@@ -0,0 +1,211 @@
|
||||
package analyzer
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/sqlc-dev/sqlc/internal/core"
|
||||
"github.com/sqlc-dev/sqlc/internal/sql/ast"
|
||||
)
|
||||
|
||||
func (a *analyzer) analyzeInsert(s *ast.InsertStmt) error {
|
||||
if s.Relation == nil {
|
||||
return fmt.Errorf("insert: missing relation")
|
||||
}
|
||||
rel, err := a.bindRangeVar(s.Relation)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
a.scope = &scope{rels: []scopeRel{rel}}
|
||||
|
||||
targets, err := insertTargets(rel, s.Cols)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := a.bindInsertValues(s.SelectStmt, rel, targets); err != nil {
|
||||
return err
|
||||
}
|
||||
return a.projectReturning(s.ReturningList)
|
||||
}
|
||||
|
||||
func (a *analyzer) analyzeUpdate(s *ast.UpdateStmt) error {
|
||||
if err := a.bindCTEs(s.WithClause); err != nil {
|
||||
return err
|
||||
}
|
||||
sc, err := a.relationScope(s.Relations, s.FromClause, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
a.scope = sc
|
||||
target := sc.rels[0]
|
||||
|
||||
for _, item := range listItems(s.TargetList) {
|
||||
rt, ok := item.(*ast.ResTarget)
|
||||
if !ok || rt.Name == nil {
|
||||
continue
|
||||
}
|
||||
col, ok := findColumn(target, *rt.Name)
|
||||
if !ok {
|
||||
return fmt.Errorf("unknown column %q", *rt.Name)
|
||||
}
|
||||
if err := a.bindValue(target, &col, rt.Val); err != nil {
|
||||
return fmt.Errorf("set %s: %w", *rt.Name, err)
|
||||
}
|
||||
}
|
||||
|
||||
if s.WhereClause != nil {
|
||||
if _, err := a.typeExpr(s.WhereClause); err != nil {
|
||||
return fmt.Errorf("where: %w", err)
|
||||
}
|
||||
}
|
||||
return a.projectReturning(s.ReturningList)
|
||||
}
|
||||
|
||||
func (a *analyzer) analyzeDelete(s *ast.DeleteStmt) error {
|
||||
if err := a.bindCTEs(s.WithClause); err != nil {
|
||||
return err
|
||||
}
|
||||
sc, err := a.relationScope(s.Relations, s.UsingClause, s.FromClause)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
a.scope = sc
|
||||
|
||||
if s.WhereClause != nil {
|
||||
if _, err := a.typeExpr(s.WhereClause); err != nil {
|
||||
return fmt.Errorf("where: %w", err)
|
||||
}
|
||||
}
|
||||
return a.projectReturning(s.ReturningList)
|
||||
}
|
||||
|
||||
// relationScope builds the scope a DML statement operates on: the relations it
|
||||
// targets, whatever a USING or FROM clause joins in, and — for the engines that
|
||||
// report a multi-table DELETE that way — a single FROM node.
|
||||
func (a *analyzer) relationScope(relations, extra *ast.List, from ast.Node) (*scope, error) {
|
||||
sc := &scope{}
|
||||
defer a.binding(sc)()
|
||||
for _, item := range listItems(relations) {
|
||||
if err := a.appendFromItem(sc, item); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
for _, item := range listItems(extra) {
|
||||
if err := a.appendFromItem(sc, item); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if from != nil {
|
||||
if err := a.appendFromItem(sc, from); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if len(sc.rels) == 0 {
|
||||
return nil, fmt.Errorf("missing target relation")
|
||||
}
|
||||
return sc, nil
|
||||
}
|
||||
|
||||
func insertTargets(rel scopeRel, cols *ast.List) ([]core.ClassColumn, error) {
|
||||
items := listItems(cols)
|
||||
if len(items) == 0 {
|
||||
out := make([]core.ClassColumn, 0, len(rel.cols))
|
||||
for _, col := range rel.cols {
|
||||
if !col.Hidden {
|
||||
out = append(out, col)
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
out := make([]core.ClassColumn, 0, len(items))
|
||||
for _, item := range items {
|
||||
rt, ok := item.(*ast.ResTarget)
|
||||
if !ok || rt.Name == nil {
|
||||
return nil, fmt.Errorf("insert: unsupported column target %T", item)
|
||||
}
|
||||
col, ok := findColumn(rel, *rt.Name)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unknown column %q", *rt.Name)
|
||||
}
|
||||
out = append(out, col)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (a *analyzer) bindInsertValues(n ast.Node, rel scopeRel, targets []core.ClassColumn) error {
|
||||
if n == nil {
|
||||
return nil
|
||||
}
|
||||
sel, ok := n.(*ast.SelectStmt)
|
||||
if !ok {
|
||||
return fmt.Errorf("insert: unsupported source %T", n)
|
||||
}
|
||||
// INSERT ... SELECT inserts whatever the query returns. The rows are not
|
||||
// the statement's result, but the query still holds placeholders.
|
||||
if sel.ValuesLists == nil {
|
||||
_, err := a.subqueryColumns(sel)
|
||||
return err
|
||||
}
|
||||
for _, row := range listItems(sel.ValuesLists) {
|
||||
values, ok := row.(*ast.List)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
for i, v := range values.Items {
|
||||
var target *core.ClassColumn
|
||||
if i < len(targets) {
|
||||
target = &targets[i]
|
||||
}
|
||||
if err := a.bindValue(rel, target, v); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *analyzer) bindValue(rel scopeRel, target *core.ClassColumn, v ast.Node) error {
|
||||
if target != nil {
|
||||
switch value := v.(type) {
|
||||
case *ast.ParamRef:
|
||||
a.inferParam(value.Number, columnType(rel, *target))
|
||||
return nil
|
||||
case *ast.A_Const:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
_, err := a.typeExpr(v)
|
||||
return err
|
||||
}
|
||||
|
||||
func (a *analyzer) projectReturning(l *ast.List) error {
|
||||
for _, item := range listItems(l) {
|
||||
rt, ok := item.(*ast.ResTarget)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if err := a.projectTarget(rt); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func findColumn(rel scopeRel, name string) (core.ClassColumn, bool) {
|
||||
for _, col := range rel.cols {
|
||||
if col.Name == name {
|
||||
return col, true
|
||||
}
|
||||
}
|
||||
return core.ClassColumn{}, false
|
||||
}
|
||||
|
||||
func columnType(rel scopeRel, col core.ClassColumn) exprType {
|
||||
return exprType{
|
||||
typeOID: col.TypeOID,
|
||||
expr: col.Type,
|
||||
nullable: !col.NotNull,
|
||||
sourceClassOID: rel.classOID,
|
||||
sourceAttributeOID: col.AttOID,
|
||||
sourceTableAlias: rel.alias,
|
||||
}
|
||||
}
|
||||
1038
internal/core/analyzer/expr.go
Normal file
1038
internal/core/analyzer/expr.go
Normal file
File diff suppressed because it is too large
Load Diff
169
internal/core/analyzer/projection.go
Normal file
169
internal/core/analyzer/projection.go
Normal file
@@ -0,0 +1,169 @@
|
||||
package analyzer
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/sqlc-dev/sqlc/internal/core"
|
||||
"github.com/sqlc-dev/sqlc/internal/sql/ast"
|
||||
)
|
||||
|
||||
func (a *analyzer) projectTarget(rt *ast.ResTarget) error {
|
||||
// A column reference's field list is flattened once here and threaded
|
||||
// through the star check, the star expansion and the output name.
|
||||
var fields []string
|
||||
if cr, ok := rt.Val.(*ast.ColumnRef); ok {
|
||||
fields = flattenFields(cr.Fields)
|
||||
if isStar(fields) {
|
||||
a.emitStar(rt, fields)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
t, err := a.typeExpr(rt.Val)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// A placeholder selected directly is named by its alias and, when nothing
|
||||
// constrains it, typed as the dialect types such a placeholder.
|
||||
if pr, ok := rt.Val.(*ast.ParamRef); ok {
|
||||
if rt.Name != nil && *rt.Name != "" {
|
||||
if p := a.params[pr.Number]; p.Name == "" {
|
||||
p.Name = *rt.Name
|
||||
a.params[pr.Number] = p
|
||||
}
|
||||
}
|
||||
if t.typeOID == 0 && t.expr == nil {
|
||||
if oid, ok := a.cat.UntypedTypeOID(); ok {
|
||||
t = exprType{typeOID: oid, nullable: true}
|
||||
}
|
||||
}
|
||||
}
|
||||
col := core.Column{
|
||||
Name: targetName(rt, fields),
|
||||
TypeOID: t.typeOID,
|
||||
NotNull: !t.nullable,
|
||||
SourceClassOID: t.sourceClassOID,
|
||||
SourceAttributeOID: t.sourceAttributeOID,
|
||||
}
|
||||
col.DataType, col.IsArray = a.typeNameOf(t)
|
||||
col.Type = a.typeExprOf(t)
|
||||
a.decorateSource(&col, t.sourceAttributeOID, t.sourceTableAlias)
|
||||
if rt.Name == nil || *rt.Name == "" {
|
||||
// A column whose own name is dotted, as ClickHouse's n.a is, is
|
||||
// reported under that name rather than its last part.
|
||||
if col.Source != nil && strings.Contains(col.Source.Column, ".") {
|
||||
col.Name = col.Source.Column
|
||||
}
|
||||
a.qualifyDuplicate(&col, t.sourceTableAlias)
|
||||
}
|
||||
a.columns = append(a.columns, col)
|
||||
return nil
|
||||
}
|
||||
|
||||
// qualifyDuplicate names a column after its relation when an earlier result
|
||||
// column from another relation already has its name, in a dialect that
|
||||
// does so.
|
||||
func (a *analyzer) qualifyDuplicate(col *core.Column, alias string) {
|
||||
if alias == "" || !a.cat.QualifiesDuplicateColumns() {
|
||||
return
|
||||
}
|
||||
for _, prev := range a.columns {
|
||||
if prev.Name != col.Name {
|
||||
continue
|
||||
}
|
||||
prevAlias := ""
|
||||
if prev.Source != nil {
|
||||
prevAlias = prev.Source.TableAlias
|
||||
}
|
||||
if prevAlias != alias {
|
||||
col.Name = alias + "." + col.Name
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (a *analyzer) decorateSource(col *core.Column, attOID int64, tableAlias string) {
|
||||
if attOID == 0 {
|
||||
return
|
||||
}
|
||||
ad, err := a.cat.LookupAttribute(attOID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
col.Source = &core.ColumnSource{
|
||||
Schema: ad.Schema,
|
||||
Table: ad.Table,
|
||||
TableAlias: tableAlias,
|
||||
Column: ad.Column,
|
||||
}
|
||||
col.DeclType = ad.DeclType
|
||||
col.IsPrimaryKey = ad.IsPrimaryKey
|
||||
col.IsUnique = ad.IsUnique
|
||||
col.IsAutoIncrement = ad.AutoIncrement
|
||||
}
|
||||
|
||||
// targetName picks the output name for a target. fields is the already
|
||||
// flattened field list when rt.Val is a column reference, and nil otherwise.
|
||||
func targetName(rt *ast.ResTarget, fields []string) string {
|
||||
if rt.Name != nil && *rt.Name != "" {
|
||||
return *rt.Name
|
||||
}
|
||||
if len(fields) > 0 {
|
||||
return fields[len(fields)-1]
|
||||
}
|
||||
if fc, ok := rt.Val.(*ast.FuncCall); ok {
|
||||
if name := funcCallName(fc); name != "" {
|
||||
return name
|
||||
}
|
||||
}
|
||||
return "?column?"
|
||||
}
|
||||
|
||||
func isStar(fields []string) bool {
|
||||
return len(fields) > 0 && fields[len(fields)-1] == "*"
|
||||
}
|
||||
|
||||
func (a *analyzer) emitStar(rt *ast.ResTarget, fields []string) {
|
||||
relName := ""
|
||||
if len(fields) > 1 {
|
||||
relName = fields[0]
|
||||
}
|
||||
// The star is reported along with the columns it covers, so the query text
|
||||
// can be rewritten to name them.
|
||||
star := core.StarExpansion{Location: rt.Location, Fields: fields}
|
||||
if rt.Name != nil {
|
||||
star.Alias = *rt.Name
|
||||
}
|
||||
for _, rel := range a.scope.rels {
|
||||
if relName != "" && rel.alias != relName {
|
||||
continue
|
||||
}
|
||||
a.columns = slices.Grow(a.columns, len(rel.cols))
|
||||
star.Columns = slices.Grow(star.Columns, len(rel.cols))
|
||||
for _, c := range rel.cols {
|
||||
if c.Hidden {
|
||||
continue
|
||||
}
|
||||
col := core.Column{
|
||||
Name: c.Name,
|
||||
TypeOID: c.TypeOID,
|
||||
NotNull: c.NotNull,
|
||||
SourceClassOID: rel.classOID,
|
||||
SourceAttributeOID: c.AttOID,
|
||||
}
|
||||
t := exprType{typeOID: c.TypeOID, expr: c.Type, nullable: !c.NotNull}
|
||||
col.DataType, col.IsArray = a.typeNameOf(t)
|
||||
col.Type = a.typeExprOf(t)
|
||||
a.decorateSource(&col, c.AttOID, rel.alias)
|
||||
a.qualifyDuplicate(&col, rel.alias)
|
||||
a.columns = append(a.columns, col)
|
||||
star.Columns = append(star.Columns, core.StarColumn{
|
||||
Relation: rel.alias,
|
||||
Name: c.Name,
|
||||
DataType: col.DataType,
|
||||
})
|
||||
}
|
||||
}
|
||||
a.recordStar(star)
|
||||
}
|
||||
243
internal/core/analyzer/scope.go
Normal file
243
internal/core/analyzer/scope.go
Normal file
@@ -0,0 +1,243 @@
|
||||
package analyzer
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/sqlc-dev/sqlc/internal/core"
|
||||
"github.com/sqlc-dev/sqlc/internal/sql/ast"
|
||||
)
|
||||
|
||||
type scope struct {
|
||||
rels []scopeRel
|
||||
}
|
||||
|
||||
type scopeRel struct {
|
||||
alias string
|
||||
classOID int64
|
||||
// cols is the catalog's column list, held as-is rather than copied into a
|
||||
// scope-local column type.
|
||||
cols []core.ClassColumn
|
||||
}
|
||||
|
||||
func (a *analyzer) buildScope(from *ast.List) (*scope, error) {
|
||||
items := listItems(from)
|
||||
sc := &scope{rels: make([]scopeRel, 0, len(items))}
|
||||
defer a.binding(sc)()
|
||||
for _, item := range items {
|
||||
if err := a.appendFromItem(sc, item); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return sc, nil
|
||||
}
|
||||
|
||||
// binding makes the scope under construction the one the analyzer resolves
|
||||
// against, and returns the func that puts back the scope it replaced. A FROM
|
||||
// item can refer to the ones before it — a set-returning function takes its
|
||||
// arguments from them — so binding an item has to see what is bound so far
|
||||
// rather than no scope at all.
|
||||
func (a *analyzer) binding(sc *scope) func() {
|
||||
prev := a.scope
|
||||
a.scope = sc
|
||||
return func() { a.scope = prev }
|
||||
}
|
||||
|
||||
func (a *analyzer) appendFromItem(sc *scope, item ast.Node) error {
|
||||
switch v := item.(type) {
|
||||
case *ast.RangeVar:
|
||||
rel, err := a.bindRangeVar(v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sc.rels = append(sc.rels, rel)
|
||||
return nil
|
||||
case *ast.JoinExpr:
|
||||
if err := a.appendFromItem(sc, v.Larg); err != nil {
|
||||
return err
|
||||
}
|
||||
return a.appendFromItem(sc, v.Rarg)
|
||||
case *ast.RangeFunction:
|
||||
rel, err := a.bindRangeFunction(v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sc.rels = append(sc.rels, rel)
|
||||
return nil
|
||||
case *ast.RangeSubselect:
|
||||
rel, err := a.bindRangeSubselect(v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sc.rels = append(sc.rels, rel)
|
||||
return nil
|
||||
case *ast.List:
|
||||
// Some engines report a comma-separated FROM as a nested list.
|
||||
for _, item := range listItems(v) {
|
||||
if err := a.appendFromItem(sc, item); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("scope: unsupported FROM item %T", item)
|
||||
}
|
||||
}
|
||||
|
||||
// bindRangeFunction binds a function called in FROM. A set-returning function
|
||||
// stands in for a relation of a single column named after it.
|
||||
func (a *analyzer) bindRangeFunction(rf *ast.RangeFunction) (scopeRel, error) {
|
||||
call := findFuncCall(rf.Functions)
|
||||
if call == nil {
|
||||
return scopeRel{}, fmt.Errorf("range function: no function call")
|
||||
}
|
||||
name := funcCallName(call)
|
||||
overloads, err := a.cat.FindProcs(name, nil)
|
||||
if err != nil {
|
||||
return scopeRel{}, err
|
||||
}
|
||||
if len(overloads) == 0 {
|
||||
return scopeRel{}, fmt.Errorf("unknown function %q", name)
|
||||
}
|
||||
|
||||
// The arguments are typed against the scope built so far, which is what
|
||||
// gives a placeholder passed to the function its type.
|
||||
if _, err := a.typeFuncCall(call); err != nil {
|
||||
return scopeRel{}, err
|
||||
}
|
||||
|
||||
p := overloads[0]
|
||||
rel := scopeRel{
|
||||
alias: name,
|
||||
cols: []core.ClassColumn{{Name: name, TypeOID: p.ReturnTypeOID, NotNull: !p.ReturnNullable}},
|
||||
}
|
||||
if rf.Alias != nil && rf.Alias.Aliasname != nil && *rf.Alias.Aliasname != "" {
|
||||
rel.alias = *rf.Alias.Aliasname
|
||||
}
|
||||
return rel, nil
|
||||
}
|
||||
|
||||
func findFuncCall(l *ast.List) *ast.FuncCall {
|
||||
for _, item := range listItems(l) {
|
||||
switch v := item.(type) {
|
||||
case *ast.FuncCall:
|
||||
return v
|
||||
case *ast.List:
|
||||
if call := findFuncCall(v); call != nil {
|
||||
return call
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// bindRangeSubselect binds a subquery used as a relation in FROM.
|
||||
func (a *analyzer) bindRangeSubselect(rs *ast.RangeSubselect) (scopeRel, error) {
|
||||
sel, ok := rs.Subquery.(*ast.SelectStmt)
|
||||
if !ok {
|
||||
return scopeRel{}, fmt.Errorf("subquery: unsupported %T", rs.Subquery)
|
||||
}
|
||||
cols, err := a.subqueryColumns(sel)
|
||||
if err != nil {
|
||||
return scopeRel{}, err
|
||||
}
|
||||
alias := ""
|
||||
if rs.Alias != nil && rs.Alias.Aliasname != nil {
|
||||
alias = *rs.Alias.Aliasname
|
||||
}
|
||||
rel := derivedRel(alias, cols)
|
||||
if rs.Alias != nil {
|
||||
renameColumns(&rel, rs.Alias.Colnames)
|
||||
}
|
||||
return rel, nil
|
||||
}
|
||||
|
||||
func (a *analyzer) bindRangeVar(rv *ast.RangeVar) (scopeRel, error) {
|
||||
if rv.Relname == nil {
|
||||
return scopeRel{}, fmt.Errorf("range var: missing relation name")
|
||||
}
|
||||
relName := *rv.Relname
|
||||
|
||||
// A WITH clause defines a relation that shadows nothing but is not in the
|
||||
// catalog, so it is resolved before one.
|
||||
if cte, ok := a.ctes[relName]; ok && (rv.Schemaname == nil || *rv.Schemaname == "") {
|
||||
if rv.Alias != nil && rv.Alias.Aliasname != nil && *rv.Alias.Aliasname != "" {
|
||||
cte.alias = *rv.Alias.Aliasname
|
||||
}
|
||||
return cte, nil
|
||||
}
|
||||
|
||||
schema := ""
|
||||
if rv.Schemaname != nil {
|
||||
schema = *rv.Schemaname
|
||||
}
|
||||
if schema == "" {
|
||||
schema = "public"
|
||||
}
|
||||
nsOID, err := a.cat.NamespaceOID(schema)
|
||||
if err != nil {
|
||||
return scopeRel{}, fmt.Errorf("schema %q: %w", schema, err)
|
||||
}
|
||||
classOID, err := a.cat.ClassOID(nsOID, relName)
|
||||
if err != nil {
|
||||
return scopeRel{}, fmt.Errorf("relation %q.%q: %w", schema, relName, err)
|
||||
}
|
||||
rel := scopeRel{
|
||||
alias: relName,
|
||||
classOID: classOID,
|
||||
}
|
||||
if rv.Alias != nil && rv.Alias.Aliasname != nil && *rv.Alias.Aliasname != "" {
|
||||
rel.alias = *rv.Alias.Aliasname
|
||||
}
|
||||
|
||||
cols, err := a.cat.ClassColumns(classOID)
|
||||
if err != nil {
|
||||
return scopeRel{}, err
|
||||
}
|
||||
rel.cols = cols
|
||||
return rel, nil
|
||||
}
|
||||
|
||||
// resolveColumn finds a column in this query's scope, falling back to the scope
|
||||
// of the query it is nested in, which is what a correlated subquery refers to.
|
||||
func (a *analyzer) resolveColumn(relation, column string) (scopeRel, core.ClassColumn, bool, error) {
|
||||
rel, col, ok, err := a.scope.resolveColumn(relation, column)
|
||||
if err != nil || ok {
|
||||
return rel, col, ok, err
|
||||
}
|
||||
if a.outer != nil {
|
||||
return a.outer.resolveColumn(relation, column)
|
||||
}
|
||||
return rel, col, false, nil
|
||||
}
|
||||
|
||||
// resolveColumn finds the single column named column, optionally qualified by
|
||||
// relation. It reports an error when more than one relation in scope offers
|
||||
// that name.
|
||||
func (s *scope) resolveColumn(relation, column string) (rel scopeRel, col core.ClassColumn, ok bool, err error) {
|
||||
// A statement whose scope is not built yet offers no columns. Report the
|
||||
// column as unresolved and let the caller say so, rather than crashing on
|
||||
// the way to the same answer.
|
||||
if s == nil {
|
||||
return rel, col, false, nil
|
||||
}
|
||||
found := 0
|
||||
for _, r := range s.rels {
|
||||
if relation != "" && r.alias != relation {
|
||||
continue
|
||||
}
|
||||
for _, c := range r.cols {
|
||||
if c.Name != column {
|
||||
continue
|
||||
}
|
||||
found++
|
||||
if found > 1 {
|
||||
return scopeRel{}, core.ClassColumn{}, false, fmt.Errorf("ambiguous column reference %q", column)
|
||||
}
|
||||
rel, col = r, c
|
||||
}
|
||||
}
|
||||
if found == 0 {
|
||||
return scopeRel{}, core.ClassColumn{}, false, nil
|
||||
}
|
||||
return rel, col, true, nil
|
||||
}
|
||||
287
internal/core/attribute.go
Normal file
287
internal/core/attribute.go
Normal file
@@ -0,0 +1,287 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/sqlc-dev/sqlc/internal/core/catalogdb"
|
||||
)
|
||||
|
||||
type AttributeSpec struct {
|
||||
ClassOID int64
|
||||
Name string
|
||||
TypeOID int64
|
||||
Num int
|
||||
NotNull bool
|
||||
HasDefault bool
|
||||
DeclType string
|
||||
AutoIncrement bool
|
||||
IsPrimaryKey bool
|
||||
IsUnique bool
|
||||
// Hidden columns resolve by name but stay out of a star expansion and
|
||||
// the relation's model, like the column an sqlite fts5 table names
|
||||
// after itself.
|
||||
Hidden bool
|
||||
}
|
||||
|
||||
func (c *Catalog) CreateAttributeSpec(s AttributeSpec) error {
|
||||
err := c.q.CreateAttribute(context.Background(), catalogdb.CreateAttributeParams{
|
||||
ClassOid: s.ClassOID,
|
||||
Name: s.Name,
|
||||
TypeOid: s.TypeOID,
|
||||
NotNull: boolToInt64(s.NotNull),
|
||||
HasDefault: boolToInt64(s.HasDefault),
|
||||
Num: int64(s.Num),
|
||||
DeclType: s.DeclType,
|
||||
AutoIncrement: boolToInt64(s.AutoIncrement),
|
||||
IsPrimaryKey: boolToInt64(s.IsPrimaryKey),
|
||||
IsUnique: boolToInt64(s.IsUnique),
|
||||
Hidden: boolToInt64(s.Hidden),
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("create attribute %q on class %d: %w", s.Name, s.ClassOID, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Catalog) CreateAttribute(classOID int64, name string, typeOID int64, notNull bool, hasDefault bool, num int) error {
|
||||
return c.CreateAttributeSpec(AttributeSpec{
|
||||
ClassOID: classOID,
|
||||
Name: name,
|
||||
TypeOID: typeOID,
|
||||
Num: num,
|
||||
NotNull: notNull,
|
||||
HasDefault: hasDefault,
|
||||
})
|
||||
}
|
||||
|
||||
// DropAttribute removes a column from a relation.
|
||||
func (c *Catalog) DropAttribute(classOID int64, name string) error {
|
||||
err := c.q.DeleteAttribute(context.Background(), catalogdb.DeleteAttributeParams{
|
||||
ClassOid: classOID,
|
||||
Name: name,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("drop attribute %q on class %d: %w", name, classOID, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// RenameAttribute renames a column.
|
||||
func (c *Catalog) RenameAttribute(classOID int64, name, newName string) error {
|
||||
err := c.q.RenameAttribute(context.Background(), catalogdb.RenameAttributeParams{
|
||||
ClassOid: classOID,
|
||||
Name: name,
|
||||
NewName: newName,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("rename attribute %q on class %d: %w", name, classOID, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetAttributeType changes a column's type.
|
||||
func (c *Catalog) SetAttributeType(classOID int64, name string, typeOID int64, declType string) error {
|
||||
err := c.q.SetAttributeType(context.Background(), catalogdb.SetAttributeTypeParams{
|
||||
ClassOid: classOID,
|
||||
Name: name,
|
||||
TypeOid: typeOID,
|
||||
DeclType: declType,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("set type of attribute %q on class %d: %w", name, classOID, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetAttributeNotNull records whether a column accepts NULL.
|
||||
func (c *Catalog) SetAttributeNotNull(classOID int64, name string, notNull bool) error {
|
||||
err := c.q.SetAttributeNotNull(context.Background(), catalogdb.SetAttributeNotNullParams{
|
||||
ClassOid: classOID,
|
||||
Name: name,
|
||||
NotNull: boolToInt64(notNull),
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("set null of attribute %q on class %d: %w", name, classOID, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// NextAttributeNum is the ordinal a column added to the relation takes.
|
||||
func (c *Catalog) NextAttributeNum(classOID int64) (int, error) {
|
||||
num, err := c.q.MaxAttributeNum(context.Background(), classOID)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("attribute count for class %d: %w", classOID, err)
|
||||
}
|
||||
return int(num) + 1, nil
|
||||
}
|
||||
|
||||
func (c *Catalog) SetAttributePrimaryKey(classOID int64, columns []string) error {
|
||||
ctx := context.Background()
|
||||
for _, name := range columns {
|
||||
err := c.q.SetAttributePrimaryKey(ctx, catalogdb.SetAttributePrimaryKeyParams{
|
||||
ClassOid: classOID,
|
||||
Name: name,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("mark pk %s on class %d: %w", name, classOID, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Catalog) SetAttributeUnique(classOID int64, columns []string) error {
|
||||
ctx := context.Background()
|
||||
for _, name := range columns {
|
||||
err := c.q.SetAttributeUnique(ctx, catalogdb.SetAttributeUniqueParams{
|
||||
ClassOid: classOID,
|
||||
Name: name,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("mark unique %s on class %d: %w", name, classOID, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type ColumnInfo struct {
|
||||
Name string
|
||||
TypeName string
|
||||
TypeOID int64
|
||||
NotNull bool
|
||||
DeclType string
|
||||
AutoIncrement bool
|
||||
IsPrimaryKey bool
|
||||
IsUnique bool
|
||||
AttributeOID int64
|
||||
ClassOID int64
|
||||
}
|
||||
|
||||
func (c *Catalog) ResolveColumn(table, column string) (*ColumnInfo, error) {
|
||||
r, err := c.q.ResolveColumn(context.Background(), catalogdb.ResolveColumnParams{
|
||||
TableName: table,
|
||||
ColumnName: column,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("resolve %s.%s: %w", table, column, err)
|
||||
}
|
||||
info := ColumnInfo{
|
||||
AttributeOID: r.Oid,
|
||||
ClassOID: r.ClassOid,
|
||||
Name: r.Name,
|
||||
TypeName: r.TypeName,
|
||||
TypeOID: r.TypeOid,
|
||||
NotNull: r.NotNull != 0,
|
||||
DeclType: r.DeclType,
|
||||
AutoIncrement: r.AutoIncrement != 0,
|
||||
IsPrimaryKey: r.IsPrimaryKey != 0,
|
||||
IsUnique: r.IsUnique != 0,
|
||||
}
|
||||
return &info, nil
|
||||
}
|
||||
|
||||
func (c *Catalog) TableColumns(table string) ([]ColumnInfo, error) {
|
||||
rows, err := c.q.TableColumns(context.Background(), table)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("table columns %q: %w", table, err)
|
||||
}
|
||||
cols := make([]ColumnInfo, 0, len(rows))
|
||||
for _, r := range rows {
|
||||
cols = append(cols, ColumnInfo{
|
||||
AttributeOID: r.Oid,
|
||||
ClassOID: r.ClassOid,
|
||||
Name: r.Name,
|
||||
TypeName: r.TypeName,
|
||||
TypeOID: r.TypeOid,
|
||||
NotNull: r.NotNull != 0,
|
||||
DeclType: r.DeclType,
|
||||
AutoIncrement: r.AutoIncrement != 0,
|
||||
IsPrimaryKey: r.IsPrimaryKey != 0,
|
||||
IsUnique: r.IsUnique != 0,
|
||||
})
|
||||
}
|
||||
return cols, nil
|
||||
}
|
||||
|
||||
type ClassColumn struct {
|
||||
AttOID int64
|
||||
Name string
|
||||
TypeOID int64
|
||||
// Type is the column's type as an expression, set for a column of a
|
||||
// derived relation whose type the catalog holds no row for, or holds
|
||||
// only the family of.
|
||||
Type *TypeExpr
|
||||
NotNull bool
|
||||
Hidden bool
|
||||
}
|
||||
|
||||
// ClassColumns returns a relation's columns in ordinal order.
|
||||
func (c *Catalog) ClassColumns(classOID int64) ([]ClassColumn, error) {
|
||||
rows, err := c.q.ClassAttributes(context.Background(), classOID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("class columns %d: %w", classOID, err)
|
||||
}
|
||||
out := make([]ClassColumn, 0, len(rows))
|
||||
for _, r := range rows {
|
||||
out = append(out, ClassColumn{
|
||||
AttOID: r.Oid,
|
||||
Name: r.Name,
|
||||
TypeOID: r.TypeOid,
|
||||
NotNull: r.NotNull != 0,
|
||||
Hidden: r.Hidden != 0,
|
||||
})
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
type CodegenColumn struct {
|
||||
Name string
|
||||
TypeOID int64
|
||||
NotNull bool
|
||||
}
|
||||
|
||||
func (c *Catalog) ClassCodegenColumns(classOID int64) ([]CodegenColumn, error) {
|
||||
rows, err := c.q.ListClassColumns(context.Background(), classOID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("class codegen columns %d: %w", classOID, err)
|
||||
}
|
||||
out := make([]CodegenColumn, 0, len(rows))
|
||||
for _, r := range rows {
|
||||
out = append(out, CodegenColumn{
|
||||
Name: r.ColumnName,
|
||||
TypeOID: r.TypeOid,
|
||||
NotNull: r.NotNull != 0,
|
||||
})
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
type AttributeDetails struct {
|
||||
Schema string
|
||||
Table string
|
||||
Column string
|
||||
Num int
|
||||
DeclType string
|
||||
AutoIncrement bool
|
||||
IsPrimaryKey bool
|
||||
IsUnique bool
|
||||
NotNull bool
|
||||
}
|
||||
|
||||
func (c *Catalog) LookupAttribute(attOID int64) (AttributeDetails, error) {
|
||||
r, err := c.q.LookupAttribute(context.Background(), attOID)
|
||||
if err != nil {
|
||||
return AttributeDetails{}, fmt.Errorf("lookup attribute %d: %w", attOID, err)
|
||||
}
|
||||
return AttributeDetails{
|
||||
Schema: r.SchemaName,
|
||||
Table: r.TableName,
|
||||
Column: r.ColumnName,
|
||||
Num: int(r.Num),
|
||||
DeclType: r.DeclType,
|
||||
AutoIncrement: r.AutoIncrement != 0,
|
||||
IsPrimaryKey: r.IsPrimaryKey != 0,
|
||||
IsUnique: r.IsUnique != 0,
|
||||
NotNull: r.NotNull != 0,
|
||||
}, nil
|
||||
}
|
||||
161
internal/core/cache.go
Normal file
161
internal/core/cache.go
Normal file
@@ -0,0 +1,161 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
|
||||
"github.com/ncruces/go-sqlite3"
|
||||
"github.com/ncruces/go-sqlite3/ext/serdes"
|
||||
|
||||
"github.com/sqlc-dev/sqlc/internal/cache"
|
||||
"github.com/sqlc-dev/sqlc/internal/core/catalogdb"
|
||||
)
|
||||
|
||||
// The name of the sole output blob a CoreCatalog action produces.
|
||||
const catalogOutput = "catalog.db"
|
||||
|
||||
// rawConn is the driver's escape hatch to the SQLite connection underneath a
|
||||
// database/sql connection, which the serialization API needs.
|
||||
type rawConn interface {
|
||||
Raw() *sqlite3.Conn
|
||||
}
|
||||
|
||||
// NewCached returns the catalog a dialect and a schema produce, restored from
|
||||
// the cache when a run has produced it before.
|
||||
//
|
||||
// Producing one means seeding the dialect — thousands of rows, since
|
||||
// PostgreSQL's functions and system catalogs alone run to five figures — and
|
||||
// then parsing the schema and applying its DDL. The result is the same every
|
||||
// time, so the finished database is stored in the cache and opened directly on
|
||||
// the next run, turning all of that work into opening a file.
|
||||
// It is a single action: the dialect and the schema together are what the
|
||||
// catalog is, so they are one key, and a hit means apply is never called.
|
||||
//
|
||||
// The dialect data is embedded in the sqlc binary, whose digest is an input to
|
||||
// every action, so the dialect only has to be named. The schema is hashed in
|
||||
// the order it is applied, since DDL order decides what the catalog holds.
|
||||
//
|
||||
// A catalog whose apply fails is returned but not cached, so that a caller can
|
||||
// report the failure against the catalog it got. A nil catalog means the
|
||||
// catalog could not be created at all. A cache that cannot be opened, read or
|
||||
// written is not an error: the catalog is simply built the long way.
|
||||
func NewCached(dialect string, schema []string, apply func(*Catalog) error, opts ...Option) (*Catalog, error) {
|
||||
store, action := openAction(dialect, schema)
|
||||
if store != nil {
|
||||
defer store.Close()
|
||||
if cat, err := restore(store, action); err == nil {
|
||||
return cat, nil
|
||||
} else if !errors.Is(err, cache.ErrNotFound) {
|
||||
slog.Debug("restoring the catalog from the cache failed", "err", err)
|
||||
}
|
||||
}
|
||||
|
||||
cat, err := New(opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := apply(cat); err != nil {
|
||||
return cat, err
|
||||
}
|
||||
if store != nil {
|
||||
if err := save(store, action, cat); err != nil {
|
||||
slog.Debug("saving the catalog to the cache failed", "err", err)
|
||||
}
|
||||
}
|
||||
return cat, nil
|
||||
}
|
||||
|
||||
// openAction opens the cache and digests the action that produces a catalog. A
|
||||
// nil store means the cache is unavailable and the catalog has to be built.
|
||||
func openAction(dialect string, schema []string) (*cache.Cache, cache.Digest) {
|
||||
store, err := cache.Open()
|
||||
if err != nil {
|
||||
slog.Debug("opening the cache failed", "err", err)
|
||||
return nil, cache.Digest{}
|
||||
}
|
||||
action := store.NewAction("CoreCatalog").AddInput("dialect", []byte(dialect))
|
||||
for _, s := range schema {
|
||||
action.AddInput("schema", []byte(s))
|
||||
}
|
||||
return store, action.Digest()
|
||||
}
|
||||
|
||||
// restore opens the catalog a previous run cached. The stored blob is opened
|
||||
// where it lies rather than copied: analysis only ever reads the catalog, so
|
||||
// every run that wants this one reads the same file.
|
||||
func restore(store *cache.Cache, action cache.Digest) (*Catalog, error) {
|
||||
result, err := store.Actions.Get(action)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
path, ok := store.CAS.Filename(result.Outputs[catalogOutput])
|
||||
if !ok {
|
||||
return nil, cache.ErrNotFound
|
||||
}
|
||||
|
||||
db, err := openFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
stmts := newStmtCache(db)
|
||||
cat := &Catalog{db: db, stmts: stmts, q: catalogdb.New(stmts)}
|
||||
|
||||
// The seeds recorded the dialect on the catalog as they ran. A restored
|
||||
// catalog reads it back from the database instead.
|
||||
row, err := cat.q.SeededDialect(context.Background())
|
||||
if err != nil {
|
||||
cat.Close()
|
||||
return nil, fmt.Errorf("core: restored catalog has no dialect: %w", err)
|
||||
}
|
||||
cat.dialectOID = row.Oid
|
||||
return cat, nil
|
||||
}
|
||||
|
||||
// save stores a finished catalog for the next run.
|
||||
func save(store *cache.Cache, action cache.Digest, cat *Catalog) error {
|
||||
blob, err := serialize(cat.db)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
digest, err := store.CAS.Put(blob)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return store.Actions.Put(action, &cache.ActionResult{
|
||||
Outputs: map[string]cache.Digest{catalogOutput: digest},
|
||||
})
|
||||
}
|
||||
|
||||
// serialize returns the bytes a database being built in memory would be
|
||||
// written to disk as, which is what gets stored.
|
||||
func serialize(db *sql.DB) ([]byte, error) {
|
||||
var blob []byte
|
||||
err := withRawConn(db, func(c *sqlite3.Conn) error {
|
||||
var err error
|
||||
blob, err = serdes.Serialize(c, "main")
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("core: serialize catalog: %w", err)
|
||||
}
|
||||
return blob, nil
|
||||
}
|
||||
|
||||
func withRawConn(db *sql.DB, fn func(*sqlite3.Conn) error) error {
|
||||
conn, err := db.Conn(context.Background())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer conn.Close()
|
||||
return conn.Raw(func(driverConn any) error {
|
||||
r, ok := driverConn.(rawConn)
|
||||
if !ok {
|
||||
return fmt.Errorf("driver does not expose its SQLite connection")
|
||||
}
|
||||
return fn(r.Raw())
|
||||
})
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user