DEPO на Firebird в мастере: подготовка базы одним действием, как для MSSQL
Мастер, шаг «Учёт DEPO», и «Обмен с DEPO» в настройках: СУБД «Firebird» (Fancy на Firebird 2.5). - Подключение: драйвер nakagami/firebirdsql, путь к файлу базы или псевдоним, WIN1251. Запросы bj к DEPO (T-SQL) подключение переводит само (fancyfb.go): механически (dbo., TOP, ISNULL, GETDATE, LEN, N, exec) вне строк и комментариев, точные тексты — fsql/fb.go (каталоги, LIKE [_], COUNT(*) OVER, SELECT без FROM, PATINDEX); @имена — позиционные ?, GUID — в верхнем регистре. Системные запросы из кода — в fsql/depocatalog.go. - «Подготовить» логином SYSDBA (depo_setup_fb.go): проверка базы и UDF Fancy, логин SYSDBA_BJ (Fancy даёт права на документы логинам SYSDBA%; для Firebird — обычный пользователь), оператор-робот Fancy, кодировщик M2M, привязки FTV_M2M_SETTINGS по признакам Fancy, таблицы, процедуры, права; обработку запускает bj. «Получить скрипт» — то же для isql в WIN1251; письмо администратору, установка процедур, плашка обновления. - Скрипты Firebird встроены (depoprocs/firebird, тест сверяет с deploy/); количества — DOUBLE PRECISION: из диалекта 3 в базу диалекта 1 процедуру с NUMERIC(18,7) не создать. - Проверено на копии базы Fancy (отдельный экземпляр bj): мастер с нуля — все проверки, BJ_PING; тестовый режим DEPO — К1 Бубликов, зачисление и списание до исполнения без ошибок; очередь, справочник НРД. Тест FBLive готовит на базе все запросы bj к DEPO: 94 из 101, оставшиеся 7 — ЛК и ручной ввод (следующий этап). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q7eREaYwUt1i4fNLZcGnjn
This commit is contained in:
@@ -0,0 +1,470 @@
|
||||
package lkgateway
|
||||
|
||||
// Fancy DEPO на Firebird (27.10.2026). Fancy работает и на Firebird 2.5 — те же таблицы и процедуры ядра, что в
|
||||
// MSSQL-версии (MSSQL-версия Fancy — перевод Firebird-версии). Весь SQL bj к DEPO написан на T-SQL; чтобы не
|
||||
// держать второй текст в каждом месте вызова, подключение к Firebird переводит запросы само (fbConn):
|
||||
// - точный текст Firebird — для запросов, которые механически не переводятся (fsql.FB, ключ — текст T-SQL);
|
||||
// - остальное механически, вне строк и комментариев: без dbo., TOP n — FIRST n, ISNULL — COALESCE,
|
||||
// GETDATE() — CURRENT_TIMESTAMP, LEN — CHAR_LENGTH, N'…' — '…', «exec имя» — EXECUTE PROCEDURE имя;
|
||||
// - именованные параметры @имя (sql.Named) — позиционные ? в порядке появления в тексте;
|
||||
// - GUID в параметрах — в верхнем регистре (в таблицах обмена Firebird GUID — строка в верхнем регистре,
|
||||
// uniqueidentifier MSSQL сравнивается без учёта регистра); время «2006-01-02T15:04:05» — с пробелом вместо T.
|
||||
// База Fancy на Firebird обычно в диалекте 1 и кодировке NONE с данными в WIN1251 — подключение в WIN1251.
|
||||
// Таблицы, процедуры и права для Firebird ставит depo_setup_fb.go; DDL сюда не попадает.
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql/driver"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"git.zetit.ru/zuevav/Bridge-and-Join-s/internal/lkgateway/fsql"
|
||||
"github.com/nakagami/firebirdsql"
|
||||
)
|
||||
|
||||
// fbDSN — строка подключения драйвера firebirdsql: путь к файлу базы на сервере Firebird или псевдоним.
|
||||
func fbDSN(s FancyMSSQLSettings, db string) string {
|
||||
q := url.Values{}
|
||||
q.Set("charset", "WIN1251")
|
||||
u := &url.URL{
|
||||
Scheme: "firebird",
|
||||
User: url.UserPassword(s.User, s.Password),
|
||||
Host: fmt.Sprintf("%s:%d", s.Server, s.DefaultPort()),
|
||||
Path: "/" + db,
|
||||
RawQuery: q.Encode(),
|
||||
}
|
||||
return u.String()
|
||||
}
|
||||
|
||||
// validFBPath — путь к файлу базы Firebird на сервере (D:\Fancy\DEPO.GDB, /var/db/depo.fdb) или псевдоним
|
||||
// из aliases.conf: без кавычек, точки с запятой и управляющих символов (путь уходит в строку подключения).
|
||||
func validFBPath(s string) bool {
|
||||
if strings.TrimSpace(s) == "" || len(s) > 255 {
|
||||
return false
|
||||
}
|
||||
for _, c := range s {
|
||||
if c < ' ' || strings.ContainsRune(`'";?#`, c) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// fbIsLoginFailure — Firebird отверг логин или пароль.
|
||||
func fbIsLoginFailure(err error) bool {
|
||||
var fe *firebirdsql.FbError
|
||||
if !errors.As(err, &fe) {
|
||||
return false
|
||||
}
|
||||
for _, c := range fe.GDSCodes {
|
||||
if c == firebirdsql.ISCLogin || c == firebirdsql.ISCLoginError {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// fbConnector — подключения к Firebird с переводом запросов.
|
||||
type fbConnector struct{ base driver.Connector }
|
||||
|
||||
func (c fbConnector) Driver() driver.Driver { return c.base.Driver() }
|
||||
|
||||
func (c fbConnector) Connect(ctx context.Context) (driver.Conn, error) {
|
||||
conn, err := c.base.Connect(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &fbConn{c: conn}, nil
|
||||
}
|
||||
|
||||
// fbConn — подключение драйвера firebirdsql, которому database/sql отдаёт T-SQL bj: текст и параметры
|
||||
// переводятся в fbRewrite. Остальные методы — как у драйвера.
|
||||
type fbConn struct{ c driver.Conn }
|
||||
|
||||
func (f *fbConn) Prepare(query string) (driver.Stmt, error) {
|
||||
return f.PrepareContext(context.Background(), query)
|
||||
}
|
||||
|
||||
func (f *fbConn) PrepareContext(ctx context.Context, query string) (driver.Stmt, error) {
|
||||
q, names := query, []string(nil)
|
||||
if !fbNative(query) {
|
||||
q, names = fbParams(fbSQL(query))
|
||||
}
|
||||
var st driver.Stmt
|
||||
var err error
|
||||
if pc, ok := f.c.(driver.ConnPrepareContext); ok {
|
||||
st, err = pc.PrepareContext(ctx, q)
|
||||
} else {
|
||||
st, err = f.c.Prepare(q)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fbErr(query, q, err)
|
||||
}
|
||||
return &fbStmt{st: st, names: names}, nil
|
||||
}
|
||||
|
||||
func (f *fbConn) Close() error { return f.c.Close() }
|
||||
|
||||
func (f *fbConn) Begin() (driver.Tx, error) {
|
||||
return f.BeginTx(context.Background(), driver.TxOptions{})
|
||||
}
|
||||
|
||||
func (f *fbConn) BeginTx(ctx context.Context, opts driver.TxOptions) (driver.Tx, error) {
|
||||
if bt, ok := f.c.(driver.ConnBeginTx); ok {
|
||||
return bt.BeginTx(ctx, opts)
|
||||
}
|
||||
return f.c.Begin() //nolint:staticcheck // у драйвера без ConnBeginTx другого способа нет
|
||||
}
|
||||
|
||||
func (f *fbConn) ExecContext(ctx context.Context, query string, args []driver.NamedValue) (driver.Result, error) {
|
||||
ex, ok := f.c.(driver.ExecerContext)
|
||||
if !ok {
|
||||
return nil, driver.ErrSkip
|
||||
}
|
||||
q, a, err := fbRewrite(query, args)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res, err := ex.ExecContext(ctx, q, a)
|
||||
return res, fbErr(query, q, err)
|
||||
}
|
||||
|
||||
func (f *fbConn) QueryContext(ctx context.Context, query string, args []driver.NamedValue) (driver.Rows, error) {
|
||||
qr, ok := f.c.(driver.QueryerContext)
|
||||
if !ok {
|
||||
return nil, driver.ErrSkip
|
||||
}
|
||||
q, a, err := fbRewrite(query, args)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rows, err := qr.QueryContext(ctx, q, a)
|
||||
return rows, fbErr(query, q, err)
|
||||
}
|
||||
|
||||
func (f *fbConn) Ping(ctx context.Context) error {
|
||||
if p, ok := f.c.(driver.Pinger); ok {
|
||||
return p.Ping(ctx)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *fbConn) CheckNamedValue(nv *driver.NamedValue) error {
|
||||
if c, ok := f.c.(driver.NamedValueChecker); ok {
|
||||
return c.CheckNamedValue(nv)
|
||||
}
|
||||
return driver.ErrSkip
|
||||
}
|
||||
|
||||
func (f *fbConn) IsValid() bool {
|
||||
if v, ok := f.c.(driver.Validator); ok {
|
||||
return v.IsValid()
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (f *fbConn) ResetSession(ctx context.Context) error {
|
||||
if r, ok := f.c.(driver.SessionResetter); ok {
|
||||
return r.ResetSession(ctx)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// fbStmt — подготовленный запрос: параметры при выполнении расставляются по именам из текста.
|
||||
type fbStmt struct {
|
||||
st driver.Stmt
|
||||
names []string
|
||||
}
|
||||
|
||||
func (s *fbStmt) Close() error { return s.st.Close() }
|
||||
func (s *fbStmt) NumInput() int { return -1 }
|
||||
|
||||
func (s *fbStmt) Exec(args []driver.Value) (driver.Result, error) {
|
||||
return s.ExecContext(context.Background(), fbValues(args))
|
||||
}
|
||||
|
||||
func (s *fbStmt) Query(args []driver.Value) (driver.Rows, error) {
|
||||
return s.QueryContext(context.Background(), fbValues(args))
|
||||
}
|
||||
|
||||
func (s *fbStmt) ExecContext(ctx context.Context, args []driver.NamedValue) (driver.Result, error) {
|
||||
a, err := fbArgs(s.names, args)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if ex, ok := s.st.(driver.StmtExecContext); ok {
|
||||
return ex.ExecContext(ctx, a)
|
||||
}
|
||||
return s.st.Exec(fbPlain(a)) //nolint:staticcheck // драйвер без StmtExecContext
|
||||
}
|
||||
|
||||
func (s *fbStmt) QueryContext(ctx context.Context, args []driver.NamedValue) (driver.Rows, error) {
|
||||
a, err := fbArgs(s.names, args)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if q, ok := s.st.(driver.StmtQueryContext); ok {
|
||||
return q.QueryContext(ctx, a)
|
||||
}
|
||||
return s.st.Query(fbPlain(a)) //nolint:staticcheck // драйвер без StmtQueryContext
|
||||
}
|
||||
|
||||
func fbValues(v []driver.Value) []driver.NamedValue {
|
||||
out := make([]driver.NamedValue, len(v))
|
||||
for i, x := range v {
|
||||
out[i] = driver.NamedValue{Ordinal: i + 1, Value: x}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func fbPlain(a []driver.NamedValue) []driver.Value {
|
||||
out := make([]driver.Value, len(a))
|
||||
for i, x := range a {
|
||||
out[i] = x.Value
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// fbRewrite — текст и параметры запроса T-SQL для Firebird. DDL и блоки PSQL (установка, depo_setup_fb.go)
|
||||
// написаны для Firebird — уходят как есть.
|
||||
func fbRewrite(query string, args []driver.NamedValue) (string, []driver.NamedValue, error) {
|
||||
if fbNative(query) {
|
||||
return query, args, nil
|
||||
}
|
||||
q, names := fbParams(fbSQL(query))
|
||||
a, err := fbArgs(names, args)
|
||||
return q, a, err
|
||||
}
|
||||
|
||||
var fbNativeRe = regexp.MustCompile(`(?i)^\s*(CREATE|ALTER|RECREATE|DROP|GRANT|REVOKE|EXECUTE\s+BLOCK|COMMENT\s+ON)\b`)
|
||||
|
||||
// fbNative — команда уже на Firebird (установка таблиц, процедур, прав).
|
||||
func fbNative(q string) bool { return fbNativeRe.MatchString(fbStripComments(q)) }
|
||||
|
||||
var (
|
||||
fbExecRe = regexp.MustCompile(`(?is)^\s*exec(?:ute)?\s+(?:\[?dbo\]?\.)?\[?([A-Za-z0-9_]+)\]?\s*;?\s*$`)
|
||||
fbObjectIDRe = regexp.MustCompile(`(?i)^SELECT OBJECT_ID\('(?:dbo\.)?([A-Za-z0-9_]+)'\)$`)
|
||||
fbDboRe = regexp.MustCompile(`(?i)\[?\bdbo\]?\.`)
|
||||
fbIsNullRe = regexp.MustCompile(`(?i)\bISNULL\s*\(`)
|
||||
fbGetDateRe = regexp.MustCompile(`(?i)\bGETDATE\s*\(\s*\)`)
|
||||
fbLenRe = regexp.MustCompile(`(?i)\bLEN\s*\(`)
|
||||
fbTopRe = regexp.MustCompile(`(?i)\bSELECT(\s+DISTINCT)?\s+TOP\s*(\(\s*@?\w+\s*\)|\d+)`)
|
||||
fbGUIDRe = regexp.MustCompile(`^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$`)
|
||||
fbISOTimeRe = regexp.MustCompile(`^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(:\d{2}(\.\d+)?)?$`)
|
||||
)
|
||||
|
||||
// fbSQL — текст запроса для Firebird: точный перевод из fsql.FB, иначе механический.
|
||||
func fbSQL(q string) string {
|
||||
if t, ok := fsql.FB[q]; ok {
|
||||
return t
|
||||
}
|
||||
if m := fbExecRe.FindStringSubmatch(q); m != nil {
|
||||
return "EXECUTE PROCEDURE " + m[1]
|
||||
}
|
||||
if m := fbObjectIDRe.FindStringSubmatch(q); m != nil { // depoTableExists: NULL — таблицы нет
|
||||
return "SELECT (SELECT FIRST 1 1 FROM RDB$RELATIONS WHERE RDB$RELATION_NAME = '" + strings.ToUpper(m[1]) + "') FROM RDB$DATABASE"
|
||||
}
|
||||
return fbOutsideLiterals(q, func(code string) string {
|
||||
code = fbDboRe.ReplaceAllString(code, "")
|
||||
code = fbIsNullRe.ReplaceAllString(code, "COALESCE(")
|
||||
code = fbGetDateRe.ReplaceAllString(code, "CURRENT_TIMESTAMP")
|
||||
code = fbLenRe.ReplaceAllString(code, "CHAR_LENGTH(")
|
||||
return fbTopRe.ReplaceAllStringFunc(code, func(s string) string {
|
||||
m := fbTopRe.FindStringSubmatch(s)
|
||||
return "SELECT FIRST " + m[2] + m[1] // в Firebird FIRST — до DISTINCT
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// fbOutsideLiterals — замены только в коде: строки, имена в кавычках и комментарии не трогаются; N перед
|
||||
// строкой (N'…' — национальная строка T-SQL) убирается.
|
||||
func fbOutsideLiterals(q string, fix func(string) string) string {
|
||||
var b, code strings.Builder
|
||||
emit := func(lit string) {
|
||||
c := code.String()
|
||||
code.Reset()
|
||||
if strings.HasPrefix(lit, "'") && (strings.HasSuffix(c, "N") || strings.HasSuffix(c, "n")) &&
|
||||
(len(c) == 1 || !fbIdentChar(c[len(c)-2])) {
|
||||
c = c[:len(c)-1]
|
||||
}
|
||||
b.WriteString(fix(c))
|
||||
b.WriteString(lit)
|
||||
}
|
||||
for i := 0; i < len(q); i++ {
|
||||
c := q[i]
|
||||
switch {
|
||||
case c == '\'' || c == '"':
|
||||
j := i + 1
|
||||
for j < len(q) {
|
||||
if q[j] == c {
|
||||
if j+1 < len(q) && q[j+1] == c {
|
||||
j += 2
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
j++
|
||||
}
|
||||
if j >= len(q) {
|
||||
j = len(q) - 1
|
||||
}
|
||||
emit(q[i : j+1])
|
||||
i = j
|
||||
case c == '-' && i+1 < len(q) && q[i+1] == '-':
|
||||
j := strings.IndexByte(q[i:], '\n')
|
||||
if j < 0 {
|
||||
j = len(q) - i
|
||||
}
|
||||
emit(q[i : i+j])
|
||||
i += j - 1
|
||||
case c == '/' && i+1 < len(q) && q[i+1] == '*':
|
||||
j := strings.Index(q[i+2:], "*/")
|
||||
end := len(q)
|
||||
if j >= 0 {
|
||||
end = i + 2 + j + 2
|
||||
}
|
||||
emit(q[i:end])
|
||||
i = end - 1
|
||||
default:
|
||||
code.WriteByte(c)
|
||||
}
|
||||
}
|
||||
b.WriteString(fix(code.String()))
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// fbParams — @имя вне строк и комментариев заменяются на ?; возвращает имена по порядку (повторы — повторно).
|
||||
func fbParams(q string) (string, []string) {
|
||||
if !strings.Contains(q, "@") {
|
||||
return q, nil
|
||||
}
|
||||
var b strings.Builder
|
||||
var names []string
|
||||
for i := 0; i < len(q); i++ {
|
||||
c := q[i]
|
||||
switch {
|
||||
case c == '\'' || c == '"': // строка или имя в кавычках — как есть до закрывающей
|
||||
j := i + 1
|
||||
for j < len(q) {
|
||||
if q[j] == c {
|
||||
if j+1 < len(q) && q[j+1] == c {
|
||||
j += 2
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
j++
|
||||
}
|
||||
if j >= len(q) {
|
||||
j = len(q) - 1
|
||||
}
|
||||
b.WriteString(q[i : j+1])
|
||||
i = j
|
||||
case c == '-' && i+1 < len(q) && q[i+1] == '-':
|
||||
j := strings.IndexByte(q[i:], '\n')
|
||||
if j < 0 {
|
||||
j = len(q) - i
|
||||
}
|
||||
b.WriteString(q[i : i+j])
|
||||
i += j - 1
|
||||
case c == '/' && i+1 < len(q) && q[i+1] == '*':
|
||||
j := strings.Index(q[i+2:], "*/")
|
||||
end := len(q)
|
||||
if j >= 0 {
|
||||
end = i + 2 + j + 2
|
||||
}
|
||||
b.WriteString(q[i:end])
|
||||
i = end - 1
|
||||
case c == '@' && i+1 < len(q) && fbIdentStart(q[i+1]):
|
||||
j := i + 1
|
||||
for j < len(q) && fbIdentChar(q[j]) {
|
||||
j++
|
||||
}
|
||||
names = append(names, q[i+1:j])
|
||||
b.WriteByte('?')
|
||||
i = j - 1
|
||||
default:
|
||||
b.WriteByte(c)
|
||||
}
|
||||
}
|
||||
return b.String(), names
|
||||
}
|
||||
|
||||
func fbIdentStart(c byte) bool { return c == '_' || c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' }
|
||||
func fbIdentChar(c byte) bool { return fbIdentStart(c) || c >= '0' && c <= '9' }
|
||||
|
||||
// fbArgs — значения для ? по именам из текста: sql.Named — по имени, @p1…@pN без имени — по номеру.
|
||||
// Без имён в тексте параметры передаются как есть (запрос уже с ?).
|
||||
func fbArgs(names []string, args []driver.NamedValue) ([]driver.NamedValue, error) {
|
||||
if len(names) == 0 {
|
||||
out := make([]driver.NamedValue, len(args))
|
||||
for i, a := range args {
|
||||
out[i] = driver.NamedValue{Ordinal: i + 1, Value: fbValue(a.Value)}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
out := make([]driver.NamedValue, 0, len(names))
|
||||
for _, n := range names {
|
||||
v, ok := fbArg(n, args)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("параметр @%s не передан", n)
|
||||
}
|
||||
out = append(out, driver.NamedValue{Ordinal: len(out) + 1, Value: fbValue(v)})
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func fbArg(name string, args []driver.NamedValue) (driver.Value, bool) {
|
||||
for _, a := range args {
|
||||
if a.Name != "" && strings.EqualFold(a.Name, name) {
|
||||
return a.Value, true
|
||||
}
|
||||
}
|
||||
if len(name) > 1 && (name[0] == 'p' || name[0] == 'P') {
|
||||
if n, err := strconv.Atoi(name[1:]); err == nil && n >= 1 {
|
||||
for _, a := range args {
|
||||
if a.Name == "" && a.Ordinal == n {
|
||||
return a.Value, true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// fbValue — GUID в верхнем регистре, время ISO 8601 — с пробелом вместо T (Firebird «T» не разбирает).
|
||||
func fbValue(v driver.Value) driver.Value {
|
||||
s, ok := v.(string)
|
||||
if !ok {
|
||||
return v
|
||||
}
|
||||
switch {
|
||||
case fbGUIDRe.MatchString(s):
|
||||
return strings.ToUpper(s)
|
||||
case fbISOTimeRe.MatchString(s):
|
||||
return strings.Replace(s, "T", " ", 1)
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
var fbErrLogged sync.Map
|
||||
|
||||
// fbErr — ошибка Firebird; для разбора в журнал (раз на запрос) — исходный текст T-SQL и что ушло в Firebird.
|
||||
func fbErr(orig, sent string, err error) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
var fe *firebirdsql.FbError
|
||||
if errors.As(err, &fe) && fe.SQLCode == -104 { // синтаксис: перевод запроса не подошёл
|
||||
if _, seen := fbErrLogged.LoadOrStore(orig, true); !seen {
|
||||
log.Printf("depo-fb: запрос не принят Firebird: %v\n T-SQL: %s\n Firebird: %s", err, depoStmtHead(orig), sent)
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
Reference in New Issue
Block a user