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

This commit is contained in:
upstream-mirror
2026-09-16 02:17:34 +05:00
commit 0f4df23290
7340 changed files with 238743 additions and 0 deletions

View 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
}

File diff suppressed because it is too large Load Diff

View 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
}

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
}

View File

@@ -0,0 +1,7 @@
package cmd
import (
_ "github.com/ncruces/go-sqlite3/driver"
_ "github.com/sqlc-dev/sqlc/internal/sqlite3ext"
)

View 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
}
}

View 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)
}

View 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:]
}

View 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
}

View 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"
}
}

View 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
}

View 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"
}
}

View 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
}
}

View 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
}

View 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
}

View 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
}

View 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)
})
}

View 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,
}
}

View 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"
}

View 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
}

View 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, ".")
}

View 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
}
}

View 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
}

View 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")
}
}

View 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"
}
}

View 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()
}
}

View File

@@ -0,0 +1,7 @@
package golang
import "embed"
//go:embed templates/*
//go:embed templates/*/*
var templates embed.FS

View File

@@ -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}}

View 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}}

View 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}}

View 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}}

View 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}}

View 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}}

View 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}}

View 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}}

View 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}}

View 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}}

View 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
}

View File

@@ -0,0 +1,7 @@
package json
type opts struct {
Out string `json:"out"`
Indent string `json:"indent,omitempty"`
Filename string `json:"filename,omitempty"`
}

View 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
}

View 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// ")
}

View 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")
}
}

View 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
}

View 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
View 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)
}
}

View 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
View 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
View 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
}

View 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
}

View 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
}

View 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
View 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
}

View 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
}

View 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
}

View 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
}

View 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
}

View File

@@ -0,0 +1,10 @@
package compiler
import (
"github.com/sqlc-dev/sqlc/internal/sql/catalog"
)
type Result struct {
Catalog *catalog.Catalog
Queries []*Query
}

View 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
}

View 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
}

View 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"})
})
}

View 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
View 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
}

View 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")
}
}

View 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
}

View 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
View 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
View 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
View 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
View 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
View 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"
}
}
}
}
}
}

View 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
}

View 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
View 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"`
}

View 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
}

View 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)
}
}
}
}

View 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,
}
}

File diff suppressed because it is too large Load Diff

View 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)
}

View 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
View 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
View 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